Skip to content

refactor+feat: stdin pipe, direct inbox, table output, batch ops - #16

Closed
crhan wants to merge 4 commits into
stephendolan:mainfrom
crhan:optimize/eliminate-tempfile-and-batch-ops
Closed

crhan wants to merge 4 commits into
stephendolan:mainfrom
crhan:optimize/eliminate-tempfile-and-batch-ops

Conversation

@crhan

@crhan crhan commented Mar 12, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Commit 1: refactor — stdin pipe and direct inbox access

  • Replace temp file execution with stdin pipe: executeJXA() uses spawn() to pipe scripts via stdin instead of writing/reading/deleting temp files. Simplifies code, removes 5 unused imports.
  • Remove window dependency for inbox commands: listInboxTasks() and getInboxCount() use the inbox global object directly instead of getPerspectiveTasks('Inbox'). Inbox commands now work headlessly (no OmniFocus window needed).
  • Note: Benchmarking shows the stdin vs temp file performance difference is negligible (~650ms per call is dominated by osascript startup + Apple Events IPC). This change is about code simplification and removing the window dependency, not performance.

Commit 2: feat — human-readable table output

  • TTY (interactive terminal) → table output by default
  • Pipe / redirect → JSON output (backward compatible)
  • --json / -j flag forces JSON in terminal
  • --compact / -c unchanged (minified JSON)
  • Supported formats: task, project, tag, folder, perspective tables; key-value stats; plain text messages

Commit 3: feat — batch complete and delete

  • of task complete id1 id2 id3 — mark multiple tasks completed in a single JXA call
  • of task delete id1 id2 id3 — delete multiple tasks in a single JXA call
  • Alias: of task done, of task rm
  • Completing/deleting 10 tasks: ~650ms (one osascript call) instead of ~6.5s (ten calls)

What was investigated but not done

Custom perspective without window: Omni Automation does not expose perspective filter rules as a queryable API. The archivedFilterRules property is inaccessible, and fileWrapper is a serialization format, not a query interface. Custom perspectives still require an open window to retrieve tasks.

Test plan

  • of inbox count / of inbox list work without an open window
  • of task list, of project list, of tag list, of folder list display tables in TTY
  • of perspective list, of tag stats, of task stats display formatted output
  • of task list | jq outputs JSON (pipe detection works)
  • of --json task list forces JSON in TTY
  • of task complete "taskA" "taskB" batch-completes in one call
  • of task delete "taskA" "taskB" batch-deletes in one call
  • Single-task of task delete "taskA" still works
  • bun run test — 15/15 passing
  • bun run lint — 0 warnings, 0 errors

- Replace temp file based osascript execution with stdin pipe via spawn(),
  eliminating fs write/delete overhead on every JXA invocation.
- Rewrite listInboxTasks() to use the `inbox` global object directly
  instead of getPerspectiveTasks('Inbox'), removing the requirement for
  an open OmniFocus window.
- Optimize getInboxCount() to count tasks in-process without serializing
  every task object through JSON.
- Remove unused imports: writeFile, unlink, tmpdir, join, promisify.
@crhan crhan changed the title refactor: replace temp file with stdin pipe and optimize inbox queries refactor: use stdin pipe and direct inbox access Mar 12, 2026
crhan added 2 commits March 13, 2026 07:57
When stdout is a TTY (interactive terminal), commands now display
results as formatted tables instead of raw JSON. Pipe or redirect
output retains JSON for machine consumption. Use --json/-j to force
JSON output in a terminal.

Supported table formats:
- task list/view/search/inbox list: flag, name, project, tags, due, added
- project list/view: name, status, folder, remaining/total tasks, tags
- tag list/view: name, task count, remaining, status, parent, activity
- folder list/view: hierarchical name, status, project counts
- perspective list: name
- stats commands: key-value format
- inbox count: plain text "Count: N"

Behavior:
- TTY → table (default), --json forces JSON
- Pipe/redirect → JSON (always), backward compatible
- --compact → JSON (single line), unchanged
Add `of task complete` (alias `done`) and update `of task delete` to
accept multiple task IDs/names in a single invocation. Both execute in
a single JXA call, avoiding per-task process fork overhead.

Examples:
  of task complete id1 id2 id3
  of task done "task name A" "task name B"
  of task delete id1 id2 id3
  of task rm "task name A" "task name B"

This enables efficient bulk cleanup — completing or deleting 10 tasks
takes ~650ms (one osascript call) instead of ~6.5s (ten calls).
@crhan crhan changed the title refactor: use stdin pipe and direct inbox access refactor+feat: stdin pipe, direct inbox, table output, batch ops Mar 13, 2026
44 new tests covering:
- Type guards (isTask, isProject, isTag, isFolder, isPerspective, isRecord)
- Table utilities (pad, relativeDate, shortDate, renderTable)
- Table formatters (formatTaskTable, formatProjectTable, formatTagTable,
  formatFolderTable, formatPerspectiveTable, formatKeyValue)

Exported pure functions from output.ts to enable direct testing.

Total test count: 15 (display) + 44 (output) = 59.
stephendolan added a commit that referenced this pull request Oct 7, 2026
…ed (#48)

executeJXA wrote every script to $TMPDIR/omnifocus-<Date.now()>.js. Calls
in the same millisecond, which the MCP server produces whenever a client
makes parallel tool calls, shared that file: one call ran the other's
script, or failed with osascript error -43 after a sibling call deleted
it. Two parallel update_task calls could apply one update twice, skip the
other, and report success for both.

Scripts now go to `osascript -l JavaScript -` over stdin. execFile keeps
UTF-8-safe decoding, the timeout, and maxBuffer. A no-op stdin error
listener keeps an early osascript exit from crashing the process with
EPIPE; the exit status still reports the failure.

Adapted from #16.

Co-authored-by: ruohan.chen <crhan123@gmail.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
stephendolan added a commit that referenced this pull request Oct 7, 2026
`inbox list` and `inbox count` went through getPerspectiveTasks('Inbox'),
which fails without an open window and switches the user's front window
to the Inbox perspective on every call. They now traverse the `inbox`
collection with inbox.apply, which includes subtasks, and keep incomplete,
non-dropped tasks, matching `task list`'s default filter. The CLI, MCP,
and README descriptions say "remaining" so callers know results no longer
follow the window's view settings.

Adapted from #16, which iterated `inbox` directly and so skipped subtasks.

Co-authored-by: ruohan.chen <crhan123@gmail.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@stephendolan

Copy link
Copy Markdown
Owner

Thanks for digging into this. Two pieces of your first commit landed in main with you as co-author, so I'm closing this one:

I'm passing on the rest:

  • Tables by default would change the CLI's output contract. It's JSON-first for scripts and agents, and anything running under a TTY would start getting tables.
  • completeTasks marks each task complete as it resolves it, so a bad third ID leaves the first two completed and reports only the error. Resolving every ID before changing anything would fix that if you want to revisit batch operations.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants