Commit a5e983770ec3
Changed files (4)
dots
.config
claude
skills
Org
tools
dots/.config/claude/skills/Org/tools/tests/batch-functions-test.el
@@ -386,5 +386,58 @@ Priority mapping: '1'=1, '2'=2, '3'=3, '4'=4, '5'=5."
(should (= (alist-get 'priority task2) 1))
(should-not (alist-get 'priority task3))))))))
+;;; Tests for Time Tracking
+
+(ert-deftest test-org-batch-clock-in ()
+ "Test clocking in to a task."
+ (batch-test--with-temp-org-file
+ "* Work\n** TODO Test Task\n"
+ (lambda (temp-file)
+ (let ((result (org-batch-clock-in temp-file "Test Task")))
+ (should result)
+ ;; Verify clock entry was added
+ (with-temp-buffer
+ (insert-file-contents temp-file)
+ (should (string-match-p "CLOCK: \\[" (buffer-string))))))))
+
+(ert-deftest test-org-batch-clock-out ()
+ "Test clocking out of a task."
+ (batch-test--with-temp-org-file
+ "* Work\n** TODO Test Task\n:LOGBOOK:\nCLOCK: [2025-12-23 Mon 10:00]\n:END:\n"
+ (lambda (temp-file)
+ (let ((result (org-batch-clock-out temp-file)))
+ (should result)
+ ;; Verify clock was closed with end time
+ (with-temp-buffer
+ (insert-file-contents temp-file)
+ (should (string-match-p "CLOCK: \\[.*?\\]--\\[.*?\\] =>" (buffer-string))))))))
+
+(ert-deftest test-org-batch-get-active-clock ()
+ "Test getting active clock."
+ (batch-test--with-temp-org-file
+ "* Work\n** TODO Test Task\n:LOGBOOK:\nCLOCK: [2025-12-23 Mon 10:00]\n:END:\n"
+ (lambda (temp-file)
+ (let ((result (org-batch-get-active-clock temp-file)))
+ (should result)
+ (should (string= (alist-get 'heading result) "Test Task"))
+ (should (alist-get 'clock_start result))))))
+
+(ert-deftest test-org-batch-get-active-clock-none ()
+ "Test getting active clock when none exists."
+ (batch-test--with-temp-org-file
+ "* Work\n** TODO Test Task\n"
+ (lambda (temp-file)
+ (let ((result (org-batch-get-active-clock temp-file)))
+ (should-not result)))))
+
+(ert-deftest test-org-batch-get-clocked-time ()
+ "Test getting total clocked time for a task."
+ (batch-test--with-temp-org-file
+ "* Work\n** TODO Test Task\n:LOGBOOK:\nCLOCK: [2025-12-23 Mon 10:00]--[2025-12-23 Mon 11:30] => 1:30\n:END:\n"
+ (lambda (temp-file)
+ (let ((minutes (org-batch-get-clocked-time temp-file "Test Task")))
+ (should (> minutes 0))
+ (should (= minutes 90)))))) ; 1:30 = 90 minutes
+
(provide 'batch-functions-test)
;;; batch-functions-test.el ends here
dots/.config/claude/skills/Org/tools/batch-functions.el
@@ -694,6 +694,86 @@ Returns count of updated tasks."
(write-region (point-min) (point-max) file))
count))
+;;; Time Tracking
+
+(defun org-batch-clock-in (file heading)
+ "Clock in to 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\\|DONE\\|CANX\\)? ?\\(?:\\[#[1-5]\\] \\)?"
+ (regexp-quote heading))))
+ (when (re-search-forward heading-regexp nil t)
+ (org-back-to-heading)
+ (org-clock-in)
+ (write-region (point-min) (point-max) file)
+ (setq found t))
+ found)))
+
+(defun org-batch-clock-out (file)
+ "Clock out of currently clocked task in FILE.
+Returns t on success, nil if no active clock found."
+ (with-temp-buffer
+ (insert-file-contents file)
+ (org-mode)
+ (goto-char (point-min))
+ (let ((found nil))
+ ;; Find active clock line (has start time but no end time)
+ (when (re-search-forward "^\\([ \t]*CLOCK: \\)\\(\\[.*?\\]\\)$" nil t)
+ (let ((indent (match-string 1))
+ (start-time (match-string 2))
+ (end-time (format-time-string "[%Y-%m-%d %a %H:%M]")))
+ ;; Calculate duration
+ (let* ((start-ts (org-parse-time-string start-time))
+ (start-encoded (apply #'encode-time start-ts))
+ (end-encoded (current-time))
+ (duration-seconds (float-time (time-subtract end-encoded start-encoded)))
+ (hours (floor (/ duration-seconds 3600)))
+ (minutes (floor (/ (mod duration-seconds 3600) 60))))
+ ;; Replace the line with closed clock entry
+ (replace-match (format "%s%s--%s => %2d:%02d" indent start-time end-time hours minutes))
+ (write-region (point-min) (point-max) file)
+ (setq found t))))
+ found)))
+
+(defun org-batch-get-active-clock (file)
+ "Get currently active clock in FILE.
+Returns alist with heading and clock-in time, or nil if no active clock."
+ (with-temp-buffer
+ (insert-file-contents file)
+ (org-mode)
+ (goto-char (point-min))
+ (let ((result nil))
+ ;; Find active clock line (no end time)
+ (when (re-search-forward "^[ \t]*CLOCK: \\(\\[.*?\\]\\)$" nil t)
+ (let ((clock-start (match-string 1)))
+ (org-back-to-heading)
+ (let ((heading (org-element-property :raw-value (org-element-at-point))))
+ (setq result `((heading . ,heading)
+ (clock_start . ,clock-start))))))
+ result)))
+
+(defun org-batch-get-clocked-time (file heading)
+ "Get total clocked time for HEADING in FILE.
+Returns minutes as integer."
+ (with-temp-buffer
+ (insert-file-contents file)
+ (org-mode)
+ (goto-char (point-min))
+ (let ((heading-regexp (concat "^\\*+ \\(?:TODO\\|NEXT\\|STRT\\|WAIT\\|DONE\\|CANX\\)? ?\\(?:\\[#[1-5]\\] \\)?"
+ (regexp-quote heading)))
+ (total-minutes 0))
+ (when (re-search-forward heading-regexp nil t)
+ (org-back-to-heading)
+ (save-restriction
+ (org-narrow-to-subtree)
+ (org-clock-sum)
+ (setq total-minutes (get-text-property (point) :org-clock-minutes))))
+ (or total-minutes 0))))
+
;;; Output Functions
(defun org-batch-output-json (success data &optional error)
dots/.config/claude/skills/Org/tools/org-manager
@@ -187,6 +187,23 @@ BULK OPERATIONS:
Priority: 1-5 (1=highest, 5=lowest)
Example: org-manager bulk-set-priority todos.org TODO 1
+TIME TRACKING:
+ clock-in <file> <heading>
+ Start time tracking on a task
+ Creates CLOCK entry in :LOGBOOK: drawer
+
+ clock-out <file>
+ Stop time tracking on currently clocked task
+ Records total time spent
+
+ get-active-clock <file>
+ Get currently clocked task (if any)
+ Returns heading and clock-in time
+
+ get-clocked-time <file> <heading>
+ Get total time spent on a task (in minutes)
+ Sums all CLOCK entries for the task
+
DENOTE COMMANDS:
denote-create <title> <tags> [--signature=SIG] [--category=CAT] [--directory=DIR] [--content=FILE]
Create a denote-formatted note with proper naming and frontmatter
@@ -811,6 +828,70 @@ cmd_bulk_set_priority() {
run_elisp "$elisp"
}
+# Time tracking commands
+
+cmd_clock_in() {
+ local file="$1"
+ local heading="$2"
+
+ [[ -f "$file" ]] || error "File not found: $file"
+ [[ -n "$heading" ]] || error "Heading required"
+
+ local elisp="(progn
+ (let ((result (org-batch-clock-in \"$file\" \"$heading\")))
+ (if result
+ (org-batch-output-json t (list :clocked_in t :heading \"$heading\"))
+ (org-batch-output-error \"Heading not found: $heading\")))
+ (kill-emacs 0))"
+
+ run_elisp "$elisp"
+}
+
+cmd_clock_out() {
+ local file="$1"
+
+ [[ -f "$file" ]] || error "File not found: $file"
+
+ local elisp="(progn
+ (let ((result (org-batch-clock-out \"$file\")))
+ (if result
+ (org-batch-output-json t (list :clocked_out t))
+ (org-batch-output-error \"No active clock found\")))
+ (kill-emacs 0))"
+
+ run_elisp "$elisp"
+}
+
+cmd_get_active_clock() {
+ local file="$1"
+
+ [[ -f "$file" ]] || error "File not found: $file"
+
+ local elisp="(progn
+ (let ((result (org-batch-get-active-clock \"$file\")))
+ (if result
+ (org-batch-output-json t result)
+ (org-batch-output-json t nil)))
+ (kill-emacs 0))"
+
+ run_elisp "$elisp"
+}
+
+cmd_get_clocked_time() {
+ local file="$1"
+ local heading="$2"
+
+ [[ -f "$file" ]] || error "File not found: $file"
+ [[ -n "$heading" ]] || error "Heading required"
+
+ local elisp="(progn
+ (let ((minutes (org-batch-get-clocked-time \"$file\" \"$heading\")))
+ (org-batch-output-json t (list :heading \"$heading\" :minutes minutes)))
+ (kill-emacs 0))"
+
+ run_elisp "$elisp"
+}
+
# Denote commands
cmd_denote_create() {
@@ -1045,6 +1126,18 @@ main() {
bulk-set-priority)
cmd_bulk_set_priority "$@"
;;
+ clock-in)
+ cmd_clock_in "$@"
+ ;;
+ clock-out)
+ cmd_clock_out "$@"
+ ;;
+ get-active-clock)
+ cmd_get_active_clock "$@"
+ ;;
+ get-clocked-time)
+ cmd_get_clocked_time "$@"
+ ;;
denote-create)
cmd_denote_create "$@"
;;
dots/.config/claude/skills/Org/SKILL.md
@@ -97,6 +97,21 @@ Provide reliable, programmatic access to org-mode files using Emacs batch mode a
./tools/org-manager bulk-set-priority ~/desktop/org/todos.org "TODO" 1
```
+#### Time Tracking
+```bash
+# Start time tracking on a task
+./tools/org-manager clock-in ~/desktop/org/todos.org "Implement feature X"
+
+# Stop time tracking (clocks out of currently active task)
+./tools/org-manager clock-out ~/desktop/org/todos.org
+
+# Check what task is currently being tracked
+./tools/org-manager get-active-clock ~/desktop/org/todos.org
+
+# Get total time spent on a task (returns minutes)
+./tools/org-manager get-clocked-time ~/desktop/org/todos.org "Implement feature X"
+```
+
#### Denote Operations
```bash
# Create denote-formatted note