auto-update-daily-20260202
  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