flake-update-20260505
1;;; denote-batch-functions.el --- Batch operations for denote notes -*- lexical-binding: t; no-byte-compile: t; -*-
2
3;; Copyright (C) 2025 Vincent Demeester
4
5;; This file provides batch mode functions for creating and manipulating
6;; denote-formatted notes from the command line using the denote package.
7;;
8;; NOTE: This file requires the denote package to be installed in your Emacs
9;; configuration. It cannot be byte-compiled in isolation.
10
11;;; Commentary:
12
13;; These functions enable Claude Code and other tools to create denote notes
14;; programmatically using Emacs batch mode. They wrap the denote package's
15;; functions for non-interactive use.
16;;
17;; Usage:
18;; emacs --batch -l denote-batch-functions.el \
19;; --eval "(denote-batch-create-note \"Title\" '(tag1 tag2))"
20
21;;; Code:
22
23(require 'denote)
24(require 'json)
25
26;; Ensure denote-directory is set
27(unless (boundp 'denote-directory)
28 (setq denote-directory "~/desktop/org/notes/"))
29
30;; Helper to output JSON
31(defun denote-batch--output-json (data)
32 "Output DATA as JSON to stdout."
33 (princ (json-encode data))
34 (princ "\n"))
35
36;; Main function: Create denote note using denote package
37(defun denote-batch-create-note (title keywords &optional signature category directory)
38 "Create a denote note with TITLE and KEYWORDS using denote package.
39KEYWORDS can be a list of strings or symbols (will be converted to strings).
40Optional SIGNATURE for automated notes (e.g., \"pkai\").
41Optional CATEGORY is stored in frontmatter.
42Optional DIRECTORY (defaults to denote-directory).
43
44Returns JSON with created file path."
45 (condition-case err
46 (let* ((denote-directory (or directory denote-directory))
47 ;; Convert keywords to strings if they're symbols
48 (keywords-list (mapcar (lambda (k)
49 (if (symbolp k)
50 (symbol-name k)
51 k))
52 keywords))
53 ;; Use denote to create the note
54 (filepath (denote title keywords-list 'org denote-directory nil nil signature nil)))
55
56 ;; Add category to frontmatter if provided
57 (when (and filepath category)
58 (with-current-buffer (find-file-noselect filepath)
59 (goto-char (point-min))
60 ;; Find end of frontmatter
61 (when (re-search-forward "^#\\+identifier:" nil t)
62 (end-of-line)
63 (insert (format "\n#+category: %s" category)))
64 (save-buffer)
65 (kill-buffer)))
66
67 ;; Return JSON result
68 (denote-batch--output-json
69 (list :success t
70 :filepath filepath
71 :message (format "Created note: %s" (file-name-nondirectory filepath)))))
72 (error
73 (denote-batch--output-json
74 (list :success :json-false
75 :error (error-message-string err))))))
76
77;; Create note with content from file
78(defun denote-batch-create-note-from-file (title keywords content-file &optional signature category directory)
79 "Create denote note with TITLE and KEYWORDS, reading content from CONTENT-FILE.
80KEYWORDS can be a list of strings or symbols (will be converted to strings).
81Uses denote package for creation, then appends content from file.
82Optional SIGNATURE, CATEGORY, DIRECTORY same as denote-batch-create-note."
83 (condition-case err
84 (let* ((denote-directory (or directory denote-directory))
85 ;; Convert keywords to strings if they're symbols
86 (keywords-list (mapcar (lambda (k)
87 (if (symbolp k)
88 (symbol-name k)
89 k))
90 keywords))
91 ;; Create the note using denote
92 (filepath (denote title keywords-list 'org denote-directory nil nil signature nil)))
93
94 ;; Add category if provided
95 (when category
96 (with-current-buffer (find-file-noselect filepath)
97 (goto-char (point-min))
98 (when (re-search-forward "^#\\+identifier:" nil t)
99 (end-of-line)
100 (insert (format "\n#+category: %s" category)))
101 (save-buffer)
102 (kill-buffer)))
103
104 ;; Append content from file
105 (when (file-exists-p content-file)
106 (with-current-buffer (find-file-noselect filepath)
107 (goto-char (point-max))
108 (insert-file-contents content-file)
109 (save-buffer)
110 (kill-buffer)))
111
112 ;; Return JSON result
113 (denote-batch--output-json
114 (list :success t
115 :filepath filepath
116 :message (format "Created note: %s" (file-name-nondirectory filepath)))))
117 (error
118 (denote-batch--output-json
119 (list :success :json-false
120 :error (error-message-string err))))))
121
122;; Add content to existing denote note
123(defun denote-batch-append-content (filepath content)
124 "Append CONTENT to existing denote note at FILEPATH."
125 (condition-case err
126 (progn
127 (unless (file-exists-p filepath)
128 (error "File does not exist: %s" filepath))
129 (with-current-buffer (find-file-noselect filepath)
130 (goto-char (point-max))
131 ;; Ensure we're on a new line
132 (unless (bolp)
133 (insert "\n"))
134 (insert "\n" content "\n")
135 (save-buffer)
136 (kill-buffer))
137 (denote-batch--output-json
138 (list :success t
139 :filepath filepath
140 :message "Content appended")))
141 (error
142 (denote-batch--output-json
143 (list :success :json-false
144 :error (error-message-string err))))))
145
146;; Update denote note using denote-rename functions
147(defun denote-batch-update-frontmatter (filepath &optional new-title new-keywords new-category)
148 "Update frontmatter of denote note at FILEPATH.
149Optional NEW-TITLE to change title.
150Optional NEW-KEYWORDS (list of symbols) to change keywords.
151Optional NEW-CATEGORY to update category.
152
153Uses denote-rename-file-using-front-matter when possible."
154 (condition-case err
155 (progn
156 (unless (file-exists-p filepath)
157 (error "File does not exist: %s" filepath))
158
159 (with-current-buffer (find-file-noselect filepath)
160 ;; Update title in frontmatter
161 (when new-title
162 (goto-char (point-min))
163 (when (re-search-forward "^#\\+title:[ \t]*\\(.*\\)$" nil t)
164 (replace-match new-title nil nil nil 1)))
165
166 ;; Update keywords in frontmatter
167 (when new-keywords
168 (goto-char (point-min))
169 (when (re-search-forward "^#\\+filetags:[ \t]*\\(.*\\)$" nil t)
170 (let ((tags-string (concat ":" (mapconcat #'symbol-name new-keywords ":") ":")))
171 (replace-match tags-string nil nil nil 1))))
172
173 ;; Update category
174 (when new-category
175 (goto-char (point-min))
176 (if (re-search-forward "^#\\+category:[ \t]*\\(.*\\)$" nil t)
177 (replace-match new-category nil nil nil 1)
178 ;; Add category if it doesn't exist
179 (when (re-search-forward "^#\\+identifier:" nil t)
180 (end-of-line)
181 (insert (format "\n#+category: %s" new-category)))))
182
183 (save-buffer)
184
185 ;; Use denote-rename-file-using-front-matter to update filename
186 (when (or new-title new-keywords)
187 (denote-rename-file-using-front-matter filepath))
188
189 (kill-buffer))
190
191 (denote-batch--output-json
192 (list :success t
193 :filepath filepath
194 :message "Frontmatter updated")))
195 (error
196 (denote-batch--output-json
197 (list :success :json-false
198 :error (error-message-string err))))))
199
200;; Read denote note metadata using denote functions
201(defun denote-batch-read-metadata (filepath)
202 "Read metadata from denote note at FILEPATH using denote functions.
203Returns JSON with title, keywords, identifier, signature, date, and category."
204 (condition-case err
205 (progn
206 (unless (file-exists-p filepath)
207 (error "File does not exist: %s" filepath))
208
209 ;; Use denote's built-in metadata retrieval
210 (let* ((file-type (denote-filetype-heuristics filepath))
211 (title (denote-retrieve-title-value filepath file-type))
212 (keywords (denote-extract-keywords-from-path filepath))
213 (identifier (denote-retrieve-filename-identifier filepath))
214 (signature (denote-retrieve-filename-signature filepath))
215 (date-string nil)
216 (category nil))
217
218 ;; Get date and category from frontmatter
219 (with-temp-buffer
220 (insert-file-contents filepath)
221 (goto-char (point-min))
222 (when (re-search-forward "^#\\+date:[ \t]*\\(.*\\)$" nil t)
223 (setq date-string (match-string 1)))
224 (goto-char (point-min))
225 (when (re-search-forward "^#\\+category:[ \t]*\\(.*\\)$" nil t)
226 (setq category (match-string 1))))
227
228 ;; Return JSON
229 (denote-batch--output-json
230 (list :success t
231 :title title
232 :keywords keywords
233 :identifier identifier
234 :signature (or signature "")
235 :date date-string
236 :category (or category "")
237 :filepath filepath))))
238 (error
239 (denote-batch--output-json
240 (list :success :json-false
241 :error (error-message-string err))))))
242
243(provide 'denote-batch-functions)
244
245;;; denote-batch-functions.el ends here