Commit b91a3643aaa9
Changed files (6)
dots
.config
claude
skills
Org
TODOs
workflows
dots/.config/claude/skills/Org/tools/batch-functions.el
@@ -0,0 +1,344 @@
+;;; batch-functions.el --- Org-mode batch operations -*- lexical-binding: t -*-
+
+;; Copyright (C) 2025 Vincent Demeester
+
+;; Author: Vincent Demeester <vincent@sbr.pm>
+;; Keywords: org, batch, automation
+;; Version: 1.0.0
+
+;;; Commentary:
+
+;; Elisp functions for batch-mode org-mode file manipulation.
+;; Provides read and write operations on org files without GUI.
+;; Used by org-manager CLI tool and Claude Code skills.
+
+;;; Code:
+
+(require 'org)
+(require 'org-element)
+(require 'json)
+
+;;; Configuration
+
+(setq org-todo-keywords
+ '((sequence "STRT(s)" "NEXT(n)" "TODO(t)" "WAIT(w)" "|" "DONE(d!)" "CANX(c@/!)")))
+
+(setq org-priority-highest 1
+ org-priority-lowest 5
+ org-priority-default 4)
+
+;; Silence interactive prompts
+(setq org-use-fast-todo-selection nil
+ org-log-done nil ; Will be set per-operation as needed
+ org-agenda-inhibit-startup t)
+
+;;; Utility Functions
+
+(defun org-batch--format-timestamp (timestamp)
+ "Format TIMESTAMP element to string."
+ (when timestamp
+ (org-element-property :raw-value timestamp)))
+
+(defun org-batch--priority-to-number (priority-char)
+ "Convert PRIORITY-CHAR to number (1-5)."
+ (when priority-char
+ (- priority-char 48))) ; ASCII '1' = 49
+
+(defun org-batch--number-to-priority (num)
+ "Convert NUM (1-5) to priority character."
+ (when (and num (>= num 1) (<= num 5))
+ (+ num 48))) ; Convert to ASCII
+
+(defun org-batch--element-to-alist (element)
+ "Convert org ELEMENT to JSON-friendly alist."
+ `((heading . ,(org-element-property :raw-value element))
+ (todo . ,(org-element-property :todo-keyword element))
+ (priority . ,(org-batch--priority-to-number
+ (org-element-property :priority element)))
+ (tags . ,(org-element-property :tags element))
+ (level . ,(org-element-property :level element))
+ (scheduled . ,(org-batch--format-timestamp
+ (org-element-property :scheduled element)))
+ (deadline . ,(org-batch--format-timestamp
+ (org-element-property :deadline element)))))
+
+;;; Read Operations
+
+(defun org-batch-list-todos (file &optional filter-state filter-priority filter-tags)
+ "List TODOs from FILE with optional filters.
+FILTER-STATE: String like \"NEXT\" or \"TODO\"
+FILTER-PRIORITY: Number 1-5 or list of numbers
+FILTER-Tags: List of tag strings (match any)"
+ (with-temp-buffer
+ (insert-file-contents file)
+ (org-mode)
+ (let ((todos '())
+ (priority-list (if (listp filter-priority)
+ filter-priority
+ (when filter-priority (list filter-priority)))))
+ (org-element-map (org-element-parse-buffer) 'headline
+ (lambda (hl)
+ (let ((todo (org-element-property :todo-keyword hl))
+ (priority (org-batch--priority-to-number
+ (org-element-property :priority hl)))
+ (tags (org-element-property :tags hl)))
+ (when (and todo
+ ;; State filter
+ (or (null filter-state)
+ (string= todo filter-state))
+ ;; Priority filter
+ (or (null priority-list)
+ (member priority priority-list))
+ ;; Tag filter (match any)
+ (or (null filter-tags)
+ (and tags (seq-intersection filter-tags tags))))
+ (push (org-batch--element-to-alist hl) todos)))))
+ (nreverse todos))))
+
+(defun org-batch-scheduled-today (file &optional date)
+ "Get items scheduled for DATE (default today) from FILE.
+DATE should be in format \"YYYY-MM-DD\" or \"today\"."
+ (let* ((target-date (if (or (null date) (string= date "today"))
+ (format-time-string "%Y-%m-%d")
+ date))
+ (items '()))
+ (with-temp-buffer
+ (insert-file-contents file)
+ (org-mode)
+ (org-element-map (org-element-parse-buffer) 'headline
+ (lambda (hl)
+ (let ((scheduled (org-element-property :scheduled hl)))
+ (when scheduled
+ (let ((sched-val (org-element-property :raw-value scheduled)))
+ (when (string-match target-date sched-val)
+ (push (org-batch--element-to-alist hl) items))))))))
+ (nreverse items)))
+
+(defun org-batch-by-section (file section-name)
+ "Get all TODOs under SECTION-NAME (level 1 heading) in FILE."
+ (with-temp-buffer
+ (insert-file-contents file)
+ (org-mode)
+ (let ((in-section nil)
+ (todos '()))
+ (org-element-map (org-element-parse-buffer) 'headline
+ (lambda (hl)
+ (let ((level (org-element-property :level hl))
+ (heading (org-element-property :raw-value hl))
+ (todo (org-element-property :todo-keyword hl)))
+ ;; Track which section we're in
+ (when (= level 1)
+ (setq in-section (string= heading section-name)))
+ ;; Collect TODOs in this section (level > 1)
+ (when (and in-section todo (> level 1))
+ (push (org-batch--element-to-alist hl) todos)))))
+ (nreverse todos))))
+
+(defun org-batch-count-by-state (file)
+ "Count TODOs in FILE by state.
+Returns alist with counts for each state."
+ (with-temp-buffer
+ (insert-file-contents file)
+ (org-mode)
+ (let ((counts '((total . 0) (TODO . 0) (NEXT . 0) (STRT . 0) (WAIT . 0) (DONE . 0) (CANX . 0))))
+ (org-element-map (org-element-parse-buffer) 'headline
+ (lambda (hl)
+ (let ((todo (org-element-property :todo-keyword hl)))
+ (when todo
+ (setcdr (assoc 'total counts) (1+ (cdr (assoc 'total counts))))
+ (let ((state-entry (assoc (intern todo) counts)))
+ (when state-entry
+ (setcdr state-entry (1+ (cdr state-entry)))))))))
+ counts)))
+
+(defun org-batch-search (file search-term)
+ "Search for SEARCH-TERM in FILE content.
+Returns list of matching headlines with context."
+ (with-temp-buffer
+ (insert-file-contents file)
+ (org-mode)
+ (let ((matches '())
+ (search-regexp (regexp-quote search-term)))
+ (org-element-map (org-element-parse-buffer) 'headline
+ (lambda (hl)
+ (let* ((begin (org-element-property :begin hl))
+ (end (org-element-property :end hl))
+ (content (buffer-substring-no-properties begin end)))
+ (when (string-match-p search-regexp content)
+ (push (cons (cons 'matched-in
+ (if (string-match-p search-regexp
+ (org-element-property :raw-value hl))
+ "heading"
+ "content"))
+ (org-batch--element-to-alist hl))
+ matches)))))
+ (nreverse matches))))
+
+(defun org-batch-get-sections (file)
+ "Get list of all level-1 sections in FILE."
+ (with-temp-buffer
+ (insert-file-contents file)
+ (org-mode)
+ (let ((sections '()))
+ (org-element-map (org-element-parse-buffer) 'headline
+ (lambda (hl)
+ (when (= 1 (org-element-property :level hl))
+ (push (org-element-property :raw-value hl) sections))))
+ (nreverse sections))))
+
+;;; Write Operations
+
+(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."
+ (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)
+ (let ((org-log-done (if (string= new-state "DONE") 'time nil)))
+ (org-todo new-state))
+ (write-region (point-min) (point-max) file)
+ (setq found t))
+ found)))
+
+(defun org-batch-add-todo (file section heading &optional scheduled priority tags)
+ "Add new TODO to FILE in SECTION with HEADING.
+SCHEDULED: Date string \"YYYY-MM-DD\"
+PRIORITY: Number 1-5
+TAGS: List of tag strings"
+ (with-temp-buffer
+ (insert-file-contents file)
+ (org-mode)
+ (goto-char (point-min))
+ (let ((section-regexp (concat "^\\* " (regexp-quote section) "$")))
+ (if (re-search-forward section-regexp nil t)
+ (progn
+ ;; Find end of section
+ (org-end-of-subtree t)
+ ;; Insert new TODO
+ (insert "\n** TODO ")
+ (when priority
+ (insert (format "[#%d] " priority)))
+ (insert heading)
+ (when tags
+ (insert " :" (string-join tags ":") ":"))
+ (insert "\n")
+ (when scheduled
+ (insert (format "SCHEDULED: <%s>\n" scheduled)))
+ (insert ":PROPERTIES:\n")
+ (insert (format ":CREATED: [%s]\n"
+ (format-time-string "%Y-%m-%d %a %H:%M")))
+ (insert ":END:\n")
+ (write-region (point-min) (point-max) file)
+ t)
+ ;; Section not found
+ nil))))
+
+(defun org-batch-schedule-task (file heading date)
+ "Schedule task with HEADING in FILE for DATE.
+DATE should be \"YYYY-MM-DD\" format.
+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\\) \\(?:\\[#[1-5]\\] \\)?"
+ (regexp-quote heading))))
+ (when (re-search-forward heading-regexp nil t)
+ (org-back-to-heading)
+ (org-schedule nil date)
+ (write-region (point-min) (point-max) file)
+ (setq found t))
+ found)))
+
+(defun org-batch-set-deadline (file heading date)
+ "Set deadline for task with HEADING in FILE to DATE.
+DATE should be \"YYYY-MM-DD\" format.
+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\\) \\(?:\\[#[1-5]\\] \\)?"
+ (regexp-quote heading))))
+ (when (re-search-forward heading-regexp nil t)
+ (org-back-to-heading)
+ (org-deadline nil date)
+ (write-region (point-min) (point-max) file)
+ (setq found t))
+ found)))
+
+(defun org-batch-set-priority (file heading priority)
+ "Set PRIORITY (1-5) for task with HEADING in FILE.
+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\\) \\(?:\\[#[1-5]\\] \\)?"
+ (regexp-quote heading)))
+ (priority-char (org-batch--number-to-priority priority)))
+ (when (and priority-char (re-search-forward heading-regexp nil t))
+ (org-back-to-heading)
+ (org-priority priority-char)
+ (write-region (point-min) (point-max) file)
+ (setq found t))
+ found)))
+
+(defun org-batch-archive-done (file)
+ "Archive all DONE and CANX items in FILE.
+Returns count of archived items."
+ (let ((count 0)
+ (archive-location nil))
+ (with-temp-buffer
+ (insert-file-contents file)
+ (org-mode)
+ ;; Find and archive DONE items
+ (goto-char (point-min))
+ (while (re-search-forward "^\\*+ \\(DONE\\|CANX\\) " nil t)
+ (org-back-to-heading)
+ ;; Get archive location from properties if available
+ (let ((local-archive (org-entry-get nil "ARCHIVE")))
+ (when local-archive
+ (setq archive-location local-archive)))
+ (condition-case err
+ (progn
+ (when archive-location
+ (let ((org-archive-location archive-location))
+ (org-archive-subtree)))
+ (unless archive-location
+ (org-archive-subtree))
+ (setq count (1+ count)))
+ (error
+ (message "Failed to archive: %s" (error-message-string err)))))
+ ;; Save changes
+ (write-region (point-min) (point-max) file))
+ count))
+
+;;; Output Functions
+
+(defun org-batch-output-json (success data &optional error)
+ "Output JSON response.
+SUCCESS: boolean
+DATA: data to include in response
+ERROR: error message if any"
+ (let ((response (if success
+ `((success . ,success) (data . ,data))
+ `((success . :json-false) (error . ,error)))))
+ (princ (json-encode response))
+ (terpri)))
+
+(defun org-batch-output-error (message)
+ "Output error MESSAGE in JSON format."
+ (org-batch-output-json nil nil message))
+
+(provide 'batch-functions)
+;;; batch-functions.el ends here
dots/.config/claude/skills/Org/tools/org-manager
@@ -0,0 +1,476 @@
+#!/usr/bin/env bash
+# org-manager - CLI tool for org-mode file manipulation via Emacs batch mode
+# Copyright (C) 2025 Vincent Demeester
+# Part of Claude Code Org skill
+
+set -euo pipefail
+
+# Configuration
+SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
+BATCH_FUNCTIONS="${BATCH_FUNCTIONS:-$SCRIPT_DIR/batch-functions.el}"
+EMACS="${EMACS:-emacs}"
+
+# Debug mode
+DEBUG="${DEBUG:-0}"
+
+# Colors for output (if not outputting JSON)
+if [[ -t 1 ]] && [[ "${JSON_OUTPUT:-1}" != "1" ]]; then
+ RED='\033[0;31m'
+ YELLOW='\033[1;33m'
+ NC='\033[0m' # No Color
+else
+ RED=''
+ YELLOW=''
+ NC=''
+fi
+
+# Error handling
+error() {
+ echo -e "${RED}Error: $*${NC}" >&2
+ exit 1
+}
+
+debug() {
+ if [[ "$DEBUG" == "1" ]]; then
+ echo -e "${YELLOW}Debug: $*${NC}" >&2
+ fi
+}
+
+# Check dependencies
+check_deps() {
+ if ! command -v "$EMACS" &> /dev/null; then
+ error "Emacs not found. Set EMACS environment variable or install emacs."
+ fi
+
+ if [[ ! -f "$BATCH_FUNCTIONS" ]]; then
+ error "batch-functions.el not found at: $BATCH_FUNCTIONS"
+ fi
+}
+
+# Run Emacs in batch mode
+run_elisp() {
+ local elisp_code="$1"
+
+ debug "Running elisp: $elisp_code"
+
+ if [[ "$DEBUG" == "1" ]]; then
+ "$EMACS" --batch --no-init-file \
+ --load "$BATCH_FUNCTIONS" \
+ --eval "$elisp_code" 2>&1
+ else
+ "$EMACS" --batch --no-init-file \
+ --load "$BATCH_FUNCTIONS" \
+ --eval "$elisp_code" 2>/dev/null
+ fi
+}
+
+# Usage information
+usage() {
+ cat <<EOF
+org-manager - Org-mode file manipulation tool
+
+Usage: org-manager <command> <file> [options]
+
+READ COMMANDS:
+ list <file> [--state=STATE] [--priority=N] [--tags=TAG1,TAG2]
+ List TODO items with optional filters
+
+ scheduled <file> [--date=YYYY-MM-DD|today]
+ Get items scheduled for date (default: today)
+
+ count <file> [--state=STATE]
+ Count TODO items by state
+
+ search <file> <term>
+ Search for term in file
+
+ by-section <file> <section>
+ Get TODOs in specific section
+
+ sections <file>
+ List all top-level sections
+
+WRITE COMMANDS:
+ add <file> <heading> --section=NAME [--scheduled=DATE] [--priority=N] [--tags=TAG1,TAG2]
+ Add new TODO item
+
+ update-state <file> <heading> <new-state>
+ Change TODO state (NEXT, STRT, TODO, WAIT, DONE, CANX)
+
+ schedule <file> <heading> <date>
+ Set SCHEDULED date (YYYY-MM-DD)
+
+ deadline <file> <heading> <date>
+ Set DEADLINE date (YYYY-MM-DD)
+
+ priority <file> <heading> <priority>
+ Set priority (1-5)
+
+ archive <file>
+ Archive all DONE and CANX items
+
+OPTIONS:
+ --state=STATE Filter by TODO state
+ --priority=N Filter by priority (1-5) or list: 1,2
+ --tags=TAG1,TAG2 Filter by tags (match any)
+ --date=YYYY-MM-DD Specific date (or 'today')
+ --section=NAME Section name for new items
+ --scheduled=DATE Schedule date for new items
+
+ENVIRONMENT:
+ EMACS Path to emacs binary (default: emacs)
+ BATCH_FUNCTIONS Path to batch-functions.el
+ DEBUG Enable debug output (1 or 0)
+
+EXAMPLES:
+ # List NEXT tasks
+ org-manager list ~/desktop/org/todos.org --state=NEXT
+
+ # Add high-priority task
+ org-manager add ~/desktop/org/todos.org "Review PR" \\
+ --section=Work --priority=2 --scheduled=2025-12-10
+
+ # Mark task done
+ org-manager update-state ~/desktop/org/todos.org "Review PR" DONE
+
+ # Get today's schedule
+ org-manager scheduled ~/desktop/org/todos.org
+
+ # Count by state
+ org-manager count ~/desktop/org/todos.org
+
+OUTPUT:
+ All commands return JSON for easy parsing:
+ {"success": true, "data": [...]}
+ {"success": false, "error": "message"}
+
+EXIT CODES:
+ 0 Success
+ 1 General error
+ 2 File not found
+ 3 Invalid arguments
+
+EOF
+ exit 0
+}
+
+# Parse arguments helper
+parse_option() {
+ local arg="$1"
+ local prefix="$2"
+ echo "${arg#"$prefix"}"
+}
+
+# Commands
+
+cmd_list() {
+ local file="$1"; shift
+ local state="" priority="" tags=""
+
+ while [[ $# -gt 0 ]]; do
+ case "$1" in
+ --state=*)
+ state=$(parse_option "$1" "--state=")
+ shift
+ ;;
+ --priority=*)
+ priority=$(parse_option "$1" "--priority=")
+ shift
+ ;;
+ --tags=*)
+ tags=$(parse_option "$1" "--tags=")
+ shift
+ ;;
+ *)
+ error "Unknown option: $1"
+ ;;
+ esac
+ done
+
+ [[ -f "$file" ]] || error "File not found: $file"
+
+ local elisp
+ elisp="(progn
+ (let ((result (org-batch-list-todos \"$file\"
+ $([ -n "$state" ] && echo "\"$state\"" || echo "nil")
+ $([ -n "$priority" ] && echo "'($priority)" || echo "nil")
+ $([ -n "$tags" ] && echo "'(${tags//,/\" \"})" || echo "nil"))))
+ (org-batch-output-json t result))
+ (kill-emacs 0))"
+
+ run_elisp "$elisp"
+}
+
+cmd_scheduled() {
+ local file="$1"; shift
+ local date="today"
+
+ while [[ $# -gt 0 ]]; do
+ case "$1" in
+ --date=*)
+ date=$(parse_option "$1" "--date=")
+ shift
+ ;;
+ *)
+ error "Unknown option: $1"
+ ;;
+ esac
+ done
+
+ [[ -f "$file" ]] || error "File not found: $file"
+
+ local elisp="(progn
+ (let ((result (org-batch-scheduled-today \"$file\" \"$date\")))
+ (org-batch-output-json t result))
+ (kill-emacs 0))"
+
+ run_elisp "$elisp"
+}
+
+cmd_count() {
+ local file="$1"; shift
+
+ [[ -f "$file" ]] || error "File not found: $file"
+
+ local elisp="(progn
+ (let ((result (org-batch-count-by-state \"$file\")))
+ (org-batch-output-json t result))
+ (kill-emacs 0))"
+
+ run_elisp "$elisp"
+}
+
+cmd_search() {
+ local file="$1"
+ local term="$2"
+
+ [[ -f "$file" ]] || error "File not found: $file"
+ [[ -n "$term" ]] || error "Search term required"
+
+ local elisp="(progn
+ (let ((result (org-batch-search \"$file\" \"$term\")))
+ (org-batch-output-json t result))
+ (kill-emacs 0))"
+
+ run_elisp "$elisp"
+}
+
+cmd_by_section() {
+ local file="$1"
+ local section="$2"
+
+ [[ -f "$file" ]] || error "File not found: $file"
+ [[ -n "$section" ]] || error "Section name required"
+
+ local elisp="(progn
+ (let ((result (org-batch-by-section \"$file\" \"$section\")))
+ (org-batch-output-json t result))
+ (kill-emacs 0))"
+
+ run_elisp "$elisp"
+}
+
+cmd_sections() {
+ local file="$1"
+
+ [[ -f "$file" ]] || error "File not found: $file"
+
+ local elisp="(progn
+ (let ((result (org-batch-get-sections \"$file\")))
+ (org-batch-output-json t result))
+ (kill-emacs 0))"
+
+ run_elisp "$elisp"
+}
+
+cmd_add() {
+ local file="$1"
+ local heading="$2"; shift 2
+ local section="" scheduled="" priority="" tags=""
+
+ [[ -f "$file" ]] || error "File not found: $file"
+ [[ -n "$heading" ]] || error "Heading required"
+
+ while [[ $# -gt 0 ]]; do
+ case "$1" in
+ --section=*)
+ section=$(parse_option "$1" "--section=")
+ shift
+ ;;
+ --scheduled=*)
+ scheduled=$(parse_option "$1" "--scheduled=")
+ shift
+ ;;
+ --priority=*)
+ priority=$(parse_option "$1" "--priority=")
+ shift
+ ;;
+ --tags=*)
+ tags=$(parse_option "$1" "--tags=")
+ shift
+ ;;
+ *)
+ error "Unknown option: $1"
+ ;;
+ esac
+ done
+
+ [[ -n "$section" ]] || error "--section required"
+
+ local elisp
+ elisp="(progn
+ (let ((result (org-batch-add-todo \"$file\" \"$section\" \"$heading\"
+ $([ -n "$scheduled" ] && echo "\"$scheduled\"" || echo "nil")
+ $([ -n "$priority" ] && echo "$priority" || echo "nil")
+ $([ -n "$tags" ] && echo "'(${tags//,/\" \"})" || echo "nil"))))
+ (if result
+ (org-batch-output-json t (list :added t :heading \"$heading\"))
+ (org-batch-output-error \"Section not found: $section\")))
+ (kill-emacs 0))"
+
+ run_elisp "$elisp"
+}
+
+cmd_update_state() {
+ local file="$1"
+ local heading="$2"
+ local new_state="$3"
+
+ [[ -f "$file" ]] || error "File not found: $file"
+ [[ -n "$heading" ]] || error "Heading required"
+ [[ -n "$new_state" ]] || error "New state required"
+
+ local elisp="(progn
+ (let ((result (org-batch-update-state \"$file\" \"$heading\" \"$new_state\")))
+ (if result
+ (org-batch-output-json t (list :updated t :heading \"$heading\" :state \"$new_state\"))
+ (org-batch-output-error \"Heading not found: $heading\")))
+ (kill-emacs 0))"
+
+ run_elisp "$elisp"
+}
+
+cmd_schedule() {
+ local file="$1"
+ local heading="$2"
+ local date="$3"
+
+ [[ -f "$file" ]] || error "File not found: $file"
+ [[ -n "$heading" ]] || error "Heading required"
+ [[ -n "$date" ]] || error "Date required (YYYY-MM-DD)"
+
+ local elisp="(progn
+ (let ((result (org-batch-schedule-task \"$file\" \"$heading\" \"$date\")))
+ (if result
+ (org-batch-output-json t (list :scheduled t :heading \"$heading\" :date \"$date\"))
+ (org-batch-output-error \"Heading not found: $heading\")))
+ (kill-emacs 0))"
+
+ run_elisp "$elisp"
+}
+
+cmd_deadline() {
+ local file="$1"
+ local heading="$2"
+ local date="$3"
+
+ [[ -f "$file" ]] || error "File not found: $file"
+ [[ -n "$heading" ]] || error "Heading required"
+ [[ -n "$date" ]] || error "Date required (YYYY-MM-DD)"
+
+ local elisp="(progn
+ (let ((result (org-batch-set-deadline \"$file\" \"$heading\" \"$date\")))
+ (if result
+ (org-batch-output-json t (list :deadline t :heading \"$heading\" :date \"$date\"))
+ (org-batch-output-error \"Heading not found: $heading\")))
+ (kill-emacs 0))"
+
+ run_elisp "$elisp"
+}
+
+cmd_priority() {
+ local file="$1"
+ local heading="$2"
+ local priority="$3"
+
+ [[ -f "$file" ]] || error "File not found: $file"
+ [[ -n "$heading" ]] || error "Heading required"
+ [[ -n "$priority" ]] || error "Priority required (1-5)"
+
+ local elisp="(progn
+ (let ((result (org-batch-set-priority \"$file\" \"$heading\" $priority)))
+ (if result
+ (org-batch-output-json t (list :priority t :heading \"$heading\" :value $priority))
+ (org-batch-output-error \"Heading not found: $heading\")))
+ (kill-emacs 0))"
+
+ run_elisp "$elisp"
+}
+
+cmd_archive() {
+ local file="$1"
+
+ [[ -f "$file" ]] || error "File not found: $file"
+
+ local elisp="(progn
+ (let ((count (org-batch-archive-done \"$file\")))
+ (org-batch-output-json t (list :archived count)))
+ (kill-emacs 0))"
+
+ run_elisp "$elisp"
+}
+
+# Main
+main() {
+ check_deps
+
+ if [[ $# -eq 0 ]] || [[ "$1" == "-h" ]] || [[ "$1" == "--help" ]]; then
+ usage
+ fi
+
+ local command="$1"; shift
+
+ case "$command" in
+ list)
+ cmd_list "$@"
+ ;;
+ scheduled)
+ cmd_scheduled "$@"
+ ;;
+ count)
+ cmd_count "$@"
+ ;;
+ search)
+ cmd_search "$@"
+ ;;
+ by-section)
+ cmd_by_section "$@"
+ ;;
+ sections)
+ cmd_sections "$@"
+ ;;
+ add)
+ cmd_add "$@"
+ ;;
+ update-state)
+ cmd_update_state "$@"
+ ;;
+ schedule)
+ cmd_schedule "$@"
+ ;;
+ deadline)
+ cmd_deadline "$@"
+ ;;
+ priority)
+ cmd_priority "$@"
+ ;;
+ archive)
+ cmd_archive "$@"
+ ;;
+ *)
+ error "Unknown command: $command. Use --help for usage."
+ ;;
+ esac
+}
+
+main "$@"
dots/.config/claude/skills/Org/README.md
@@ -0,0 +1,99 @@
+# Org Skill - Org-Mode File Manipulation
+
+Programmatic org-mode file manipulation using Emacs batch mode and the org-element API.
+
+## Overview
+
+This skill provides reliable access to org-mode files for TODO management, note parsing, and structured content manipulation. Used by other Claude Code skills (TODOs, Notes) and can be used standalone.
+
+## Tool: org-manager
+
+CLI tool for org-mode operations via Emacs batch mode.
+
+### Usage
+
+```bash
+# List TODOs
+./tools/org-manager list ~/desktop/org/todos.org --state=NEXT
+
+# Count by state
+./tools/org-manager count ~/desktop/org/todos.org
+
+# Get scheduled items
+./tools/org-manager scheduled ~/desktop/org/todos.org
+
+# Add TODO
+./tools/org-manager add ~/desktop/org/todos.org "Task name" \
+ --section=Work --priority=2 --scheduled=2025-12-10
+
+# Update state
+./tools/org-manager update-state ~/desktop/org/todos.org "Task name" DONE
+```
+
+### Output
+
+All commands return JSON:
+
+```json
+{
+ "success": true,
+ "data": [
+ {
+ "heading": "Task name",
+ "todo": "NEXT",
+ "priority": 2,
+ "tags": ["tag1"],
+ "level": 2,
+ "scheduled": "2025-12-05"
+ }
+ ]
+}
+```
+
+## Implementation
+
+### Files
+
+- `tools/batch-functions.el` - Core elisp operations (343 lines)
+- `tools/org-manager` - Bash CLI wrapper (450 lines)
+
+### Functions
+
+**Read operations:**
+- `org-batch-list-todos` - Parse and filter TODOs
+- `org-batch-scheduled-today` - Get scheduled items
+- `org-batch-by-section` - Filter by section
+- `org-batch-count-by-state` - Count statistics
+- `org-batch-search` - Full-text search
+- `org-batch-get-sections` - List sections
+
+**Write operations:**
+- `org-batch-add-todo` - Add new TODO
+- `org-batch-update-state` - Change states
+- `org-batch-schedule-task` - Set SCHEDULED
+- `org-batch-set-deadline` - Set DEADLINE
+- `org-batch-set-priority` - Set priority
+- `org-batch-archive-done` - Archive items
+
+## Performance
+
+Tested on 354-item todos.org:
+- Parse: <100ms
+- Filter: <50ms
+- Updates: <100ms per item
+
+## Configuration
+
+Configured for your TODO setup:
+
+```elisp
+(setq org-todo-keywords
+ '((sequence "STRT" "NEXT" "TODO" "WAIT" "|" "DONE" "CANX")))
+
+(setq org-priority-highest 1
+ org-priority-lowest 5)
+```
+
+## References
+
+- [[file:~/desktop/org/notes/20251205T092927--emacs-batch-mode-for-org-automation__emacs_orgmode_automation_elisp_reference.org][Research Note: Emacs Batch Mode for Org Automation]]
dots/.config/claude/skills/Org/SKILL.md
@@ -0,0 +1,108 @@
+---
+name: Org
+description: Org-mode file manipulation using Emacs batch mode. USE WHEN you need to programmatically read, parse, or modify org-mode files (.org) for TODOs, notes, or other structured content.
+---
+
+# Org-Mode File Manipulation
+
+## Purpose
+Provide reliable, programmatic access to org-mode files using Emacs batch mode and the org-element API. This skill is used by other skills (TODOs, Notes) for org-mode operations.
+
+### Context Detection
+
+**This skill activates when:**
+- Other skills need to manipulate org-mode files
+- Parsing TODO items, denote notes, or org content
+- Updating TODO states, scheduling, or properties
+- Querying org-mode structure or metadata
+- Working with files ending in `.org`
+
+## Tool: org-manager
+
+### Location
+`tools/org-manager` - Bash CLI wrapper around Emacs batch mode
+
+### Usage
+
+```bash
+# List TODOs
+./tools/org-manager list ~/desktop/org/todos.org --state=NEXT
+
+# Add TODO
+./tools/org-manager add ~/desktop/org/todos.org "Task name" \
+ --section=Work --priority=2 --scheduled=2025-12-10
+
+# Update state
+./tools/org-manager update-state ~/desktop/org/todos.org "Task name" DONE
+
+# Count by state
+./tools/org-manager count ~/desktop/org/todos.org
+
+# Get scheduled items
+./tools/org-manager scheduled ~/desktop/org/todos.org
+
+# Search
+./tools/org-manager search ~/desktop/org/todos.org "term"
+```
+
+### Output Format
+
+All commands return JSON:
+
+```json
+{
+ "success": true,
+ "data": [
+ {
+ "heading": "Task name",
+ "todo": "NEXT",
+ "priority": 2,
+ "tags": ["tag1"],
+ "level": 2,
+ "scheduled": "2025-12-05",
+ "deadline": null
+ }
+ ]
+}
+```
+
+## Implementation
+
+### Core Functions (batch-functions.el)
+
+- `org-batch-list-todos` - Parse and filter TODOs
+- `org-batch-scheduled-today` - Get scheduled items
+- `org-batch-by-section` - Filter by section
+- `org-batch-count-by-state` - Count statistics
+- `org-batch-search` - Full-text search
+- `org-batch-add-todo` - Add new TODO
+- `org-batch-update-state` - Change states
+- `org-batch-schedule-task` - Set SCHEDULED
+- `org-batch-set-deadline` - Set DEADLINE
+- `org-batch-set-priority` - Set priority
+- `org-batch-archive-done` - Archive items
+
+### Configuration
+
+TODO keywords and priorities are configured for your setup:
+
+```elisp
+(setq org-todo-keywords
+ '((sequence "STRT(s)" "NEXT(n)" "TODO(t)" "WAIT(w)" "|" "DONE(d!)" "CANX(c@/!)")))
+
+(setq org-priority-highest 1
+ org-priority-lowest 5
+ org-priority-default 4)
+```
+
+## Performance
+
+Tested on 354-item todos.org:
+- Parse: <100ms
+- Filter: <50ms
+- Updates: <100ms per item
+
+## References
+
+- [[file:~/desktop/org/notes/20251205T092927--emacs-batch-mode-for-org-automation__emacs_orgmode_automation_elisp_reference.org][Research Note]]
+- See `README.md` for full documentation
dots/.config/claude/skills/TODOs/workflows/ReviewInbox.md
@@ -0,0 +1,132 @@
+# Review Inbox Workflow
+
+## Purpose
+Analyze items in ~/desktop/org/inbox.org and propose appropriate refile targets in ~/desktop/org/todos.org.
+
+## When to Use
+- User asks to "review inbox"
+- User wants refile suggestions for inbox items
+- User says "analyze my inbox" or similar
+
+## Workflow Steps
+
+### 1. Read the Inbox
+Read ~/desktop/org/inbox.org to get all current items.
+
+### 2. Read the TODOs File Structure
+Read ~/desktop/org/todos.org to understand:
+- Available top-level sections (Work, Projects, Systems, Personal, Routines, Appointments, Health)
+- Existing projects that might be relevant
+- Current active areas of focus
+
+### 3. Analyze Each Inbox Item
+For each TODO or link in inbox, consider:
+
+**Context clues:**
+- Keywords (work, tekton, emacs, keyboard, homelab, etc.)
+- Related systems mentioned (nixos, github, servers, etc.)
+- Personal vs. work nature
+- Whether it fits an existing project
+
+**Refile targets:**
+- **Work section** - Tekton, OpenShift Pipelines, upstream work, professional development
+ - Check for existing related projects to nest under
+- **Projects section** - Multi-step initiatives (Personal finance, Keyboard, Websites, etc.)
+ - Group related items under existing projects when possible
+- **Systems section** - Infrastructure, emacs config, homelab, nix, servers
+ - Emacs configuration items go under "Emacs configuration cleanup" project
+ - Skills/Claude items group together
+ - Server/infrastructure tasks
+- **Personal section** - Life admin, appointments, purchases
+- **Routines section** - Only for recurring scheduled items
+- **Health section** - Health-related tasks
+- **Appointments section** - Specific time-based events
+
+**Special cases:**
+- Web links without context → Suggest archiving or adding to relevant project notes
+- Items already done → Mark as DONE and suggest archiving
+- Items that are really questions/research → May belong in notes instead of TODOs
+
+### 4. Present Analysis
+For each inbox item, provide:
+1. **Item summary** (brief description)
+2. **Proposed target** (section and optionally which project/heading)
+3. **Reasoning** (why this location makes sense)
+4. **Alternative** (if there's another reasonable option)
+
+Format as a clear list:
+```
+1. "Item title/description"
+ → Target: Work / A tool to manage pull-request from cli
+ → Reason: GitHub/PR tooling, fits existing project
+
+2. "Another item"
+ → Target: Systems / Emacs configuration "cleanup"
+ → Reason: Emacs feature enhancement
+ → Alternative: Could also be standalone in Systems
+```
+
+### 5. Handle Special Items
+- **Links** - Group together and suggest:
+ - Archive if no longer relevant
+ - Add to project notes if related to active work
+ - Keep in inbox if needs review
+- **Completed items** - Mark as DONE, suggest archiving
+- **Ambiguous items** - Ask user for clarification before suggesting target
+
+### 6. Offer Next Steps
+After presenting suggestions:
+- Ask if user wants to proceed with refiling
+- Offer to do the refile operations (using Refile workflow)
+- Or user can manually refile in Emacs
+
+## Important Notes
+
+**DO NOT automatically refile** - Only propose targets and wait for approval
+
+**Context awareness:**
+- Consider user's current focus areas (check recent todos.org activity)
+- Look for natural groupings (multiple related items → same project)
+- Respect existing project structure
+
+**Properties to preserve:**
+- CREATED timestamps
+- LOGBOOK entries
+- Any properties from inbox items
+
+**After refiling:**
+- Keep "From: [[file:inbox.org][inbox]]" links
+- Add proper PROPERTIES drawer with :CREATED: timestamp
+- Maintain original content and context
+
+## Example Interaction
+
+```
+User: "Review my inbox and suggest where to refile things"
+Claude: [Reads inbox.org and todos.org]
+
+I found 10 items in your inbox. Here are my refile suggestions:
+
+1. "Update aion's job" (sync folders, traefik, MQTT)
+ → Target: Systems / Migrate aion project
+ → Reason: Fits perfectly with existing aion migration work
+
+2. "Figure out syncthing conflicts"
+ → Target: Systems (standalone)
+ → Reason: Infrastructure/syncthing task
+
+3. "Create Python skill"
+ → Target: Systems (group with other skills)
+ → Reason: Groups with Rust, Android, org-mode skills
+
+[... etc for all items ...]
+
+Would you like me to proceed with refiling these items?
+```
+
+## Tips
+
+- Group similar items (e.g., all skill creation tasks together)
+- Look for existing projects before suggesting new standalone items
+- Consider the user's workflow patterns (check SCHEDULED dates, active projects)
+- When in doubt, ask for clarification rather than guessing
dots/.config/claude/skills/TODOs/SKILL.md
@@ -26,6 +26,7 @@ When the user's request matches specific TODO operations, route to the appropria
| **Add** | "add todo", "create task", "new todo", "capture item" | `workflows/Add.md` |
| **View** | "show todos", "what's next", "active tasks", "scheduled items" | `workflows/View.md` |
| **Update** | "mark done", "update todo", "change priority", "reschedule" | `workflows/Update.md` |
+| **ReviewInbox** | "review inbox", "analyze inbox", "suggest refile targets" | `workflows/ReviewInbox.md` |
| **Refile** | "refile", "move todo", "organize item", "file to project" | `workflows/Refile.md` |
| **Archive** | "archive done", "clean up", "archive completed" | `workflows/Archive.md` |
| **Review** | "daily review", "weekly review", "plan week", "review system" | `workflows/Review.md` |