Commit 3a21926c31e7
Changed files (4)
dots
.config
claude
skills
Org
TODOs
dots/.config/claude/skills/Org/tools/batch-functions.el
@@ -298,6 +298,101 @@ Returns the text content without the heading line or properties."
;;; Write Operations
+(defun org-batch--adjust-heading-levels (content parent-level)
+ "Adjust heading levels in CONTENT to be relative to PARENT-LEVEL.
+Converts markdown headers (#, ##, ###) and org headers (*, **, ***)
+to the appropriate level relative to the parent heading.
+Parent level 2 (**) means # becomes ***, ## becomes ****, etc."
+ (with-temp-buffer
+ (insert content)
+ (goto-char (point-min))
+ ;; First, convert markdown headings to org format with adjusted levels
+ (while (re-search-forward "^\\(#+\\)\\( .*\\)$" nil t)
+ (let* ((markdown-level (length (match-string 1)))
+ (header-text (match-string 2))
+ ;; Subheading should be parent + markdown level
+ (new-level (+ parent-level markdown-level))
+ (org-stars (make-string new-level ?*)))
+ ;; Replace with a temporary marker to avoid re-processing
+ (replace-match (concat "ORG_HEADING_MARKER:" org-stars header-text))))
+ ;; Now process existing org headings (* Header) - adjust their level
+ (goto-char (point-min))
+ (while (re-search-forward "^\\(\\*+\\)\\( .*\\)$" nil t)
+ (let* ((org-level (length (match-string 1)))
+ (header-text (match-string 2))
+ ;; Subheading should be parent + org level
+ (new-level (+ parent-level org-level))
+ (org-stars (make-string new-level ?*)))
+ (replace-match (concat "ORG_HEADING_MARKER:" org-stars header-text))))
+ ;; Remove the temporary markers
+ (goto-char (point-min))
+ (while (re-search-forward "ORG_HEADING_MARKER:" nil t)
+ (replace-match ""))
+ (buffer-string)))
+
+(defun org-batch-append-content (file heading content)
+ "Append CONTENT to TODO with HEADING in FILE.
+Adds content at the end of the heading's body, before any subheadings.
+Automatically adjusts heading levels in content (# becomes ###, etc).
+Returns t on success, nil if heading not found."
+ (with-temp-buffer
+ (insert-file-contents file)
+ (org-mode)
+ (goto-char (point-min))
+ (let ((found nil)
+ (heading-regexp (concat "^\\*+ \\(?:TODO\\|NEXT\\|STRT\\|WAIT\\|DONE\\|CANX\\)? ?\\(?:\\[#[1-5]\\] \\)?"
+ (regexp-quote heading))))
+ (when (re-search-forward heading-regexp nil t)
+ (org-back-to-heading)
+ ;; Get parent heading level for content adjustment
+ (let* ((parent-level (org-current-level))
+ (section-end (save-excursion
+ (org-end-of-subtree t t)
+ (point)))
+ ;; Adjust content heading levels
+ (adjusted-content (org-batch--adjust-heading-levels content parent-level)))
+ ;; Find the insertion point:
+ ;; - After properties drawer
+ ;; - After SCHEDULED/DEADLINE lines
+ ;; - Before any subheadings
+ ;; - At end of existing content
+ (forward-line 1)
+ ;; Skip properties drawer
+ (when (looking-at "^[ \t]*:PROPERTIES:")
+ (re-search-forward "^[ \t]*:END:" section-end t)
+ (forward-line 1))
+ ;; Skip SCHEDULED/DEADLINE/CLOSED lines
+ (while (looking-at "^[ \t]*\\(?:SCHEDULED\\|DEADLINE\\|CLOSED\\):")
+ (forward-line 1))
+ ;; Skip logbook drawer if present
+ (when (looking-at "^[ \t]*:LOGBOOK:")
+ (re-search-forward "^[ \t]*:END:" section-end t)
+ (forward-line 1))
+ ;; Find end of content (before any subheading)
+ (let ((content-end (save-excursion
+ (if (re-search-forward "^\\*" section-end t)
+ (match-beginning 0)
+ section-end))))
+ (goto-char content-end)
+ ;; Skip back over trailing blank lines
+ (skip-chars-backward "\n\t ")
+ (unless (bolp) (forward-line 1))
+ ;; Ensure we have a blank line before content if there's existing content
+ (unless (or (= (point) (save-excursion (org-back-to-heading) (forward-line 1) (point)))
+ (looking-back "\\`\\|^[ \t]*\n" nil))
+ (insert "\n"))
+ ;; Insert the adjusted content
+ (insert adjusted-content)
+ ;; Ensure content ends with newline
+ (unless (bolp) (insert "\n"))
+ ;; Add blank line after content if subheadings follow
+ (when (looking-at "^\\*")
+ (unless (looking-back "\n\n" nil)
+ (insert "\n")))
+ (write-region (point-min) (point-max) file)
+ (setq found t)))
+ found))))
+
(defun org-batch-update-state (file heading new-state)
"Update TODO state for HEADING in FILE to NEW-STATE.
Returns t on success, nil if heading not found."
dots/.config/claude/skills/Org/tools/org-manager
@@ -133,6 +133,12 @@ WRITE COMMANDS:
add <file> <heading> --section=NAME [--scheduled=DATE] [--priority=N] [--tags=TAG1,TAG2]
Add new TODO item
+ append-content <file> <heading> <content-file>
+ Append content from file to existing TODO
+ Content should be in org-mode format (not markdown)
+ Adds content after properties/scheduling, before subheadings
+ Automatically adjusts heading levels (# or * → relative to parent)
+
update-state <file> <heading> <new-state>
Change TODO state (NEXT, STRT, TODO, WAIT, DONE, CANX)
@@ -587,6 +593,33 @@ cmd_add() {
run_elisp "$elisp"
}
+cmd_append_content() {
+ local file="$1"
+ local heading="$2"
+ local content_file="$3"
+
+ [[ -f "$file" ]] || error "File not found: $file"
+ [[ -n "$heading" ]] || error "Heading required"
+ [[ -f "$content_file" ]] || error "Content file not found: $content_file"
+
+ # Read content from file
+ local content
+ content=$(<"$content_file")
+
+ # Escape quotes and backslashes for elisp
+ content="${content//\\/\\\\}"
+ content="${content//\"/\\\"}"
+
+ local elisp="(progn
+ (let ((result (org-batch-append-content \"$file\" \"$heading\" \"$content\")))
+ (if result
+ (org-batch-output-json t (list :content-appended t :heading \"$heading\"))
+ (org-batch-output-error \"Heading not found: $heading\")))
+ (kill-emacs 0))"
+
+ run_elisp "$elisp"
+}
+
cmd_update_state() {
local file="$1"
local heading="$2"
@@ -1332,6 +1365,9 @@ main() {
add)
cmd_add "$@"
;;
+ append-content)
+ cmd_append_content "$@"
+ ;;
update-state)
cmd_update_state "$@"
;;
dots/.config/claude/skills/TODOs/SKILL.md
@@ -321,9 +321,20 @@ This skill integrates with the **Org skill** for programmatic org-mode manipulat
### Tool Location
`~/.config/claude/skills/Org/tools/org-manager`
-### Common Operations
+**ALWAYS use org-manager for TODO operations.** It provides reliable, JSON-formatted operations on org-mode files via Emacs batch mode.
-**List TODOs:**
+### Quick Reference
+
+All commands return JSON: `{"success": true, "data": [...]}`
+
+Use `jq` for parsing:
+```bash
+org-manager list ~/desktop/org/todos.org --state=NEXT | jq -r '.data[] | "[\(.todo)] \(.heading)"'
+```
+
+### READ COMMANDS
+
+**List TODOs with filters:**
```bash
# All NEXT tasks
org-manager list ~/desktop/org/todos.org --state=NEXT
@@ -347,6 +358,12 @@ org-manager by-section ~/desktop/org/todos.org "Work"
org-manager add ~/desktop/org/todos.org "Task description" \
--section=Work --priority=2 --scheduled=2025-12-10
+# Append content from file to existing TODO
+# Content is added after properties/scheduling, before subheadings
+# IMPORTANT: Content should be in org-mode format
+# Heading levels are automatically adjusted (# becomes ***, * becomes ***, etc.)
+org-manager append-content ~/desktop/org/todos.org "Task heading" /tmp/notes.org
+
# Update state
org-manager update-state ~/desktop/org/todos.org "Task heading" DONE
@@ -360,6 +377,15 @@ org-manager priority ~/desktop/org/todos.org "Task heading" 2
org-manager archive ~/desktop/org/todos.org
```
+**IMPORTANT NOTE about append-content:**
+- Content files should be in **org-mode format**, not markdown
+- Use org syntax: `*bold*`, `/italic/`, `[[url][text]]`, `=code=`
+- Heading level adjustment is provided as a convenience:
+ - Markdown headings (`#`) will be converted to org headings (`*`)
+ - Org headings (`*`) will have their level adjusted
+ - Other markdown syntax (bold, links, code) will NOT be converted
+- Best practice: Write content in org-mode format from the start
+
**Statistics:**
```bash
# Count by state
CLAUDE.md
@@ -51,6 +51,8 @@ Secrets are managed using agenix:
Everything should happen using `make` (and `Makefile` accross the repository). You can use `make help` to figure out what it does.
+**IMPORTANT: Never use `home-manager switch` or `nixos-rebuild` commands directly. Always use `make switch` or the appropriate make targets.**
+
### Building and Deploying Systems
```bash