@scardis/omnifocus-mcp 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/commands/opsx/apply.md +152 -0
- package/.claude/commands/opsx/archive.md +157 -0
- package/.claude/commands/opsx/bulk-archive.md +242 -0
- package/.claude/commands/opsx/continue.md +114 -0
- package/.claude/commands/opsx/explore.md +173 -0
- package/.claude/commands/opsx/ff.md +97 -0
- package/.claude/commands/opsx/new.md +69 -0
- package/.claude/commands/opsx/onboard.md +550 -0
- package/.claude/commands/opsx/propose.md +106 -0
- package/.claude/commands/opsx/sync.md +134 -0
- package/.claude/commands/opsx/verify.md +164 -0
- package/.claude/skills/openspec-apply-change/SKILL.md +156 -0
- package/.claude/skills/openspec-archive-change/SKILL.md +114 -0
- package/.claude/skills/openspec-bulk-archive-change/SKILL.md +246 -0
- package/.claude/skills/openspec-continue-change/SKILL.md +118 -0
- package/.claude/skills/openspec-explore/SKILL.md +288 -0
- package/.claude/skills/openspec-ff-change/SKILL.md +101 -0
- package/.claude/skills/openspec-new-change/SKILL.md +74 -0
- package/.claude/skills/openspec-onboard/SKILL.md +554 -0
- package/.claude/skills/openspec-propose/SKILL.md +110 -0
- package/.claude/skills/openspec-sync-specs/SKILL.md +138 -0
- package/.claude/skills/openspec-verify-change/SKILL.md +168 -0
- package/CONTRIBUTING.md +83 -0
- package/LICENSE +21 -0
- package/README.md +198 -0
- package/dist/runtime/bridge.d.ts +16 -0
- package/dist/runtime/bridge.d.ts.map +1 -0
- package/dist/runtime/bridge.js +76 -0
- package/dist/runtime/bridge.js.map +1 -0
- package/dist/runtime/index.d.ts +5 -0
- package/dist/runtime/index.d.ts.map +1 -0
- package/dist/runtime/index.js +5 -0
- package/dist/runtime/index.js.map +1 -0
- package/dist/runtime/jxaShim.d.ts +21 -0
- package/dist/runtime/jxaShim.d.ts.map +1 -0
- package/dist/runtime/jxaShim.js +55 -0
- package/dist/runtime/jxaShim.js.map +1 -0
- package/dist/runtime/resultProtocol.d.ts +66 -0
- package/dist/runtime/resultProtocol.d.ts.map +1 -0
- package/dist/runtime/resultProtocol.js +52 -0
- package/dist/runtime/resultProtocol.js.map +1 -0
- package/dist/runtime/snippetLoader.d.ts +4 -0
- package/dist/runtime/snippetLoader.d.ts.map +1 -0
- package/dist/runtime/snippetLoader.js +68 -0
- package/dist/runtime/snippetLoader.js.map +1 -0
- package/dist/schemas/enums.d.ts +9 -0
- package/dist/schemas/enums.d.ts.map +1 -0
- package/dist/schemas/enums.js +26 -0
- package/dist/schemas/enums.js.map +1 -0
- package/dist/schemas/index.d.ts +3 -0
- package/dist/schemas/index.d.ts.map +1 -0
- package/dist/schemas/index.js +3 -0
- package/dist/schemas/index.js.map +1 -0
- package/dist/schemas/shapes.d.ts +726 -0
- package/dist/schemas/shapes.d.ts.map +1 -0
- package/dist/schemas/shapes.js +221 -0
- package/dist/schemas/shapes.js.map +1 -0
- package/dist/server.d.ts +2 -0
- package/dist/server.d.ts.map +1 -0
- package/dist/server.js +50 -0
- package/dist/server.js.map +1 -0
- package/dist/tools/completeProject.d.ts +24 -0
- package/dist/tools/completeProject.d.ts.map +1 -0
- package/dist/tools/completeProject.js +17 -0
- package/dist/tools/completeProject.js.map +1 -0
- package/dist/tools/completeTask.d.ts +25 -0
- package/dist/tools/completeTask.d.ts.map +1 -0
- package/dist/tools/completeTask.js +17 -0
- package/dist/tools/completeTask.js.map +1 -0
- package/dist/tools/createFolder.d.ts +20 -0
- package/dist/tools/createFolder.d.ts.map +1 -0
- package/dist/tools/createFolder.js +13 -0
- package/dist/tools/createFolder.js.map +1 -0
- package/dist/tools/createProject.d.ts +59 -0
- package/dist/tools/createProject.d.ts.map +1 -0
- package/dist/tools/createProject.js +13 -0
- package/dist/tools/createProject.js.map +1 -0
- package/dist/tools/createTag.d.ts +20 -0
- package/dist/tools/createTag.d.ts.map +1 -0
- package/dist/tools/createTag.js +13 -0
- package/dist/tools/createTag.js.map +1 -0
- package/dist/tools/createTask.d.ts +116 -0
- package/dist/tools/createTask.d.ts.map +1 -0
- package/dist/tools/createTask.js +13 -0
- package/dist/tools/createTask.js.map +1 -0
- package/dist/tools/deleteFolder.d.ts +30 -0
- package/dist/tools/deleteFolder.d.ts.map +1 -0
- package/dist/tools/deleteFolder.js +18 -0
- package/dist/tools/deleteFolder.js.map +1 -0
- package/dist/tools/deleteProject.d.ts +30 -0
- package/dist/tools/deleteProject.d.ts.map +1 -0
- package/dist/tools/deleteProject.js +18 -0
- package/dist/tools/deleteProject.js.map +1 -0
- package/dist/tools/deleteTag.d.ts +30 -0
- package/dist/tools/deleteTag.d.ts.map +1 -0
- package/dist/tools/deleteTag.js +18 -0
- package/dist/tools/deleteTag.js.map +1 -0
- package/dist/tools/deleteTask.d.ts +31 -0
- package/dist/tools/deleteTask.d.ts.map +1 -0
- package/dist/tools/deleteTask.js +18 -0
- package/dist/tools/deleteTask.js.map +1 -0
- package/dist/tools/dropProject.d.ts +24 -0
- package/dist/tools/dropProject.d.ts.map +1 -0
- package/dist/tools/dropProject.js +17 -0
- package/dist/tools/dropProject.js.map +1 -0
- package/dist/tools/dropTask.d.ts +25 -0
- package/dist/tools/dropTask.d.ts.map +1 -0
- package/dist/tools/dropTask.js +17 -0
- package/dist/tools/dropTask.js.map +1 -0
- package/dist/tools/editFolder.d.ts +20 -0
- package/dist/tools/editFolder.d.ts.map +1 -0
- package/dist/tools/editFolder.js +13 -0
- package/dist/tools/editFolder.js.map +1 -0
- package/dist/tools/editProject.d.ts +59 -0
- package/dist/tools/editProject.d.ts.map +1 -0
- package/dist/tools/editProject.js +13 -0
- package/dist/tools/editProject.js.map +1 -0
- package/dist/tools/editTag.d.ts +31 -0
- package/dist/tools/editTag.d.ts.map +1 -0
- package/dist/tools/editTag.js +13 -0
- package/dist/tools/editTag.js.map +1 -0
- package/dist/tools/editTask.d.ts +79 -0
- package/dist/tools/editTask.d.ts.map +1 -0
- package/dist/tools/editTask.js +13 -0
- package/dist/tools/editTask.js.map +1 -0
- package/dist/tools/getFolder.d.ts +24 -0
- package/dist/tools/getFolder.d.ts.map +1 -0
- package/dist/tools/getFolder.js +17 -0
- package/dist/tools/getFolder.js.map +1 -0
- package/dist/tools/getProject.d.ts +24 -0
- package/dist/tools/getProject.d.ts.map +1 -0
- package/dist/tools/getProject.js +17 -0
- package/dist/tools/getProject.js.map +1 -0
- package/dist/tools/getTag.d.ts +24 -0
- package/dist/tools/getTag.d.ts.map +1 -0
- package/dist/tools/getTag.js +17 -0
- package/dist/tools/getTag.js.map +1 -0
- package/dist/tools/getTask.d.ts +24 -0
- package/dist/tools/getTask.d.ts.map +1 -0
- package/dist/tools/getTask.js +17 -0
- package/dist/tools/getTask.js.map +1 -0
- package/dist/tools/index.d.ts +732 -0
- package/dist/tools/index.d.ts.map +1 -0
- package/dist/tools/index.js +84 -0
- package/dist/tools/index.js.map +1 -0
- package/dist/tools/listFolders.d.ts +50 -0
- package/dist/tools/listFolders.d.ts.map +1 -0
- package/dist/tools/listFolders.js +21 -0
- package/dist/tools/listFolders.js.map +1 -0
- package/dist/tools/listProjects.d.ts +70 -0
- package/dist/tools/listProjects.d.ts.map +1 -0
- package/dist/tools/listProjects.js +21 -0
- package/dist/tools/listProjects.js.map +1 -0
- package/dist/tools/listTags.d.ts +50 -0
- package/dist/tools/listTags.d.ts.map +1 -0
- package/dist/tools/listTags.js +21 -0
- package/dist/tools/listTags.js.map +1 -0
- package/dist/tools/listTasks.d.ts +156 -0
- package/dist/tools/listTasks.d.ts.map +1 -0
- package/dist/tools/listTasks.js +36 -0
- package/dist/tools/listTasks.js.map +1 -0
- package/dist/tools/moveProject.d.ts +20 -0
- package/dist/tools/moveProject.d.ts.map +1 -0
- package/dist/tools/moveProject.js +13 -0
- package/dist/tools/moveProject.js.map +1 -0
- package/dist/tools/moveTask.d.ts +31 -0
- package/dist/tools/moveTask.d.ts.map +1 -0
- package/dist/tools/moveTask.js +13 -0
- package/dist/tools/moveTask.js.map +1 -0
- package/dist/tools/resolveName.d.ts +36 -0
- package/dist/tools/resolveName.d.ts.map +1 -0
- package/dist/tools/resolveName.js +26 -0
- package/dist/tools/resolveName.js.map +1 -0
- package/openspec/changes/archive/2026-04-09-bootstrap-omnifocus-mcp/.openspec.yaml +2 -0
- package/openspec/changes/archive/2026-04-09-bootstrap-omnifocus-mcp/design.md +162 -0
- package/openspec/changes/archive/2026-04-09-bootstrap-omnifocus-mcp/proposal.md +49 -0
- package/openspec/changes/archive/2026-04-09-bootstrap-omnifocus-mcp/specs/attachments/spec.md +9 -0
- package/openspec/changes/archive/2026-04-09-bootstrap-omnifocus-mcp/specs/batch-operations/spec.md +9 -0
- package/openspec/changes/archive/2026-04-09-bootstrap-omnifocus-mcp/specs/database-inspection/spec.md +9 -0
- package/openspec/changes/archive/2026-04-09-bootstrap-omnifocus-mcp/specs/execution-runtime/spec.md +69 -0
- package/openspec/changes/archive/2026-04-09-bootstrap-omnifocus-mcp/specs/folder-management/spec.md +25 -0
- package/openspec/changes/archive/2026-04-09-bootstrap-omnifocus-mcp/specs/forecast/spec.md +9 -0
- package/openspec/changes/archive/2026-04-09-bootstrap-omnifocus-mcp/specs/identity-resolution/spec.md +45 -0
- package/openspec/changes/archive/2026-04-09-bootstrap-omnifocus-mcp/specs/perspective-management/spec.md +9 -0
- package/openspec/changes/archive/2026-04-09-bootstrap-omnifocus-mcp/specs/project-management/spec.md +25 -0
- package/openspec/changes/archive/2026-04-09-bootstrap-omnifocus-mcp/specs/recurrence/spec.md +9 -0
- package/openspec/changes/archive/2026-04-09-bootstrap-omnifocus-mcp/specs/settings/spec.md +9 -0
- package/openspec/changes/archive/2026-04-09-bootstrap-omnifocus-mcp/specs/tag-management/spec.md +25 -0
- package/openspec/changes/archive/2026-04-09-bootstrap-omnifocus-mcp/specs/task-management/spec.md +29 -0
- package/openspec/changes/archive/2026-04-09-bootstrap-omnifocus-mcp/specs/url-automation/spec.md +9 -0
- package/openspec/changes/archive/2026-04-09-bootstrap-omnifocus-mcp/specs/window-state/spec.md +9 -0
- package/openspec/changes/archive/2026-04-09-bootstrap-omnifocus-mcp/tasks.md +84 -0
- package/openspec/changes/archive/2026-04-09-folder-crud/.openspec.yaml +2 -0
- package/openspec/changes/archive/2026-04-09-folder-crud/design.md +58 -0
- package/openspec/changes/archive/2026-04-09-folder-crud/proposal.md +28 -0
- package/openspec/changes/archive/2026-04-09-folder-crud/specs/folder-write/spec.md +45 -0
- package/openspec/changes/archive/2026-04-09-folder-crud/tasks.md +41 -0
- package/openspec/changes/archive/2026-04-09-folder-tag-list-filtering/.openspec.yaml +2 -0
- package/openspec/changes/archive/2026-04-09-folder-tag-list-filtering/design.md +38 -0
- package/openspec/changes/archive/2026-04-09-folder-tag-list-filtering/proposal.md +30 -0
- package/openspec/changes/archive/2026-04-09-folder-tag-list-filtering/specs/folder-management/spec.md +21 -0
- package/openspec/changes/archive/2026-04-09-folder-tag-list-filtering/specs/tag-management/spec.md +21 -0
- package/openspec/changes/archive/2026-04-09-folder-tag-list-filtering/tasks.md +35 -0
- package/openspec/changes/archive/2026-04-09-move-operations/.openspec.yaml +2 -0
- package/openspec/changes/archive/2026-04-09-move-operations/design.md +43 -0
- package/openspec/changes/archive/2026-04-09-move-operations/proposal.md +25 -0
- package/openspec/changes/archive/2026-04-09-move-operations/specs/move-operations/spec.md +41 -0
- package/openspec/changes/archive/2026-04-09-move-operations/tasks.md +40 -0
- package/openspec/changes/archive/2026-04-09-project-crud/.openspec.yaml +2 -0
- package/openspec/changes/archive/2026-04-09-project-crud/design.md +60 -0
- package/openspec/changes/archive/2026-04-09-project-crud/proposal.md +29 -0
- package/openspec/changes/archive/2026-04-09-project-crud/specs/project-write/spec.md +74 -0
- package/openspec/changes/archive/2026-04-09-project-crud/tasks.md +48 -0
- package/openspec/changes/archive/2026-04-09-project-filtering/.openspec.yaml +2 -0
- package/openspec/changes/archive/2026-04-09-project-filtering/design.md +52 -0
- package/openspec/changes/archive/2026-04-09-project-filtering/proposal.md +26 -0
- package/openspec/changes/archive/2026-04-09-project-filtering/specs/project-filtering/spec.md +66 -0
- package/openspec/changes/archive/2026-04-09-project-filtering/specs/project-management/spec.md +13 -0
- package/openspec/changes/archive/2026-04-09-project-filtering/tasks.md +41 -0
- package/openspec/changes/archive/2026-04-09-tag-crud/.openspec.yaml +2 -0
- package/openspec/changes/archive/2026-04-09-tag-crud/design.md +45 -0
- package/openspec/changes/archive/2026-04-09-tag-crud/proposal.md +28 -0
- package/openspec/changes/archive/2026-04-09-tag-crud/specs/tag-write/spec.md +49 -0
- package/openspec/changes/archive/2026-04-09-tag-crud/tasks.md +41 -0
- package/openspec/changes/archive/2026-04-09-task-crud/.openspec.yaml +2 -0
- package/openspec/changes/archive/2026-04-09-task-crud/design.md +62 -0
- package/openspec/changes/archive/2026-04-09-task-crud/proposal.md +29 -0
- package/openspec/changes/archive/2026-04-09-task-crud/specs/task-management/spec.md +17 -0
- package/openspec/changes/archive/2026-04-09-task-crud/specs/task-write/spec.md +89 -0
- package/openspec/changes/archive/2026-04-09-task-crud/tasks.md +55 -0
- package/openspec/changes/archive/2026-04-09-task-filtering/.openspec.yaml +2 -0
- package/openspec/changes/archive/2026-04-09-task-filtering/design.md +61 -0
- package/openspec/changes/archive/2026-04-09-task-filtering/proposal.md +26 -0
- package/openspec/changes/archive/2026-04-09-task-filtering/specs/task-filtering/spec.md +63 -0
- package/openspec/changes/archive/2026-04-09-task-filtering/specs/task-management/spec.md +17 -0
- package/openspec/changes/archive/2026-04-09-task-filtering/tasks.md +42 -0
- package/openspec/changes/archive/2026-04-10-planned-date/.openspec.yaml +2 -0
- package/openspec/changes/archive/2026-04-10-planned-date/design.md +27 -0
- package/openspec/changes/archive/2026-04-10-planned-date/proposal.md +29 -0
- package/openspec/changes/archive/2026-04-10-planned-date/specs/task-management/spec.md +29 -0
- package/openspec/changes/archive/2026-04-10-planned-date/specs/task-write/spec.md +69 -0
- package/openspec/changes/archive/2026-04-10-planned-date/tasks.md +26 -0
- package/openspec/changes/archive/2026-04-10-task-recurrence/.openspec.yaml +2 -0
- package/openspec/changes/archive/2026-04-10-task-recurrence/design.md +81 -0
- package/openspec/changes/archive/2026-04-10-task-recurrence/proposal.md +28 -0
- package/openspec/changes/archive/2026-04-10-task-recurrence/specs/recurrence/spec.md +47 -0
- package/openspec/changes/archive/2026-04-10-task-recurrence/specs/task-management/spec.md +25 -0
- package/openspec/changes/archive/2026-04-10-task-recurrence/specs/task-write/spec.md +61 -0
- package/openspec/changes/archive/2026-04-10-task-recurrence/tasks.md +39 -0
- package/openspec/config.yaml +20 -0
- package/openspec/specs/attachments/spec.md +15 -0
- package/openspec/specs/batch-operations/spec.md +15 -0
- package/openspec/specs/database-inspection/spec.md +15 -0
- package/openspec/specs/execution-runtime/spec.md +75 -0
- package/openspec/specs/folder-management/spec.md +39 -0
- package/openspec/specs/folder-write/spec.md +45 -0
- package/openspec/specs/forecast/spec.md +15 -0
- package/openspec/specs/identity-resolution/spec.md +51 -0
- package/openspec/specs/move-operations/spec.md +41 -0
- package/openspec/specs/perspective-management/spec.md +15 -0
- package/openspec/specs/project-filtering/spec.md +72 -0
- package/openspec/specs/project-management/spec.md +31 -0
- package/openspec/specs/project-write/spec.md +79 -0
- package/openspec/specs/recurrence/spec.md +51 -0
- package/openspec/specs/settings/spec.md +15 -0
- package/openspec/specs/tag-management/spec.md +39 -0
- package/openspec/specs/tag-write/spec.md +49 -0
- package/openspec/specs/task-filtering/spec.md +63 -0
- package/openspec/specs/task-management/spec.md +51 -0
- package/openspec/specs/task-write/spec.md +115 -0
- package/openspec/specs/url-automation/spec.md +15 -0
- package/openspec/specs/window-state/spec.md +15 -0
- package/package.json +32 -0
- package/scripts/cleanup-fixtures.ts +89 -0
- package/server.json +21 -0
- package/src/runtime/bridge.ts +97 -0
- package/src/runtime/index.ts +4 -0
- package/src/runtime/jxaShim.ts +55 -0
- package/src/runtime/resultProtocol.ts +62 -0
- package/src/runtime/snippetLoader.ts +79 -0
- package/src/schemas/enums.ts +32 -0
- package/src/schemas/index.ts +38 -0
- package/src/schemas/shapes.ts +267 -0
- package/src/server.ts +58 -0
- package/src/snippets/complete_project.js +73 -0
- package/src/snippets/complete_task.js +85 -0
- package/src/snippets/create_folder.js +52 -0
- package/src/snippets/create_project.js +107 -0
- package/src/snippets/create_tag.js +55 -0
- package/src/snippets/create_task.js +159 -0
- package/src/snippets/delete_folder.js +26 -0
- package/src/snippets/delete_project.js +20 -0
- package/src/snippets/delete_tag.js +20 -0
- package/src/snippets/delete_task.js +20 -0
- package/src/snippets/drop_project.js +73 -0
- package/src/snippets/drop_task.js +85 -0
- package/src/snippets/edit_folder.js +46 -0
- package/src/snippets/edit_project.js +106 -0
- package/src/snippets/edit_tag.js +56 -0
- package/src/snippets/edit_task.js +146 -0
- package/src/snippets/get_folder.js +48 -0
- package/src/snippets/get_project.js +77 -0
- package/src/snippets/get_tag.js +51 -0
- package/src/snippets/get_task.js +96 -0
- package/src/snippets/list_folders.js +50 -0
- package/src/snippets/list_projects.js +98 -0
- package/src/snippets/list_tags.js +54 -0
- package/src/snippets/list_tasks.js +127 -0
- package/src/snippets/move_project.js +79 -0
- package/src/snippets/move_task.js +113 -0
- package/src/snippets/resolve_name.js +83 -0
- package/src/tools/completeProject.ts +21 -0
- package/src/tools/completeTask.ts +23 -0
- package/src/tools/createFolder.ts +20 -0
- package/src/tools/createProject.ts +20 -0
- package/src/tools/createTag.ts +20 -0
- package/src/tools/createTask.ts +20 -0
- package/src/tools/deleteFolder.ts +24 -0
- package/src/tools/deleteProject.ts +24 -0
- package/src/tools/deleteTag.ts +24 -0
- package/src/tools/deleteTask.ts +26 -0
- package/src/tools/dropProject.ts +21 -0
- package/src/tools/dropTask.ts +23 -0
- package/src/tools/editFolder.ts +19 -0
- package/src/tools/editProject.ts +20 -0
- package/src/tools/editTag.ts +20 -0
- package/src/tools/editTask.ts +20 -0
- package/src/tools/getFolder.ts +24 -0
- package/src/tools/getProject.ts +24 -0
- package/src/tools/getTag.ts +24 -0
- package/src/tools/getTask.ts +24 -0
- package/src/tools/index.ts +85 -0
- package/src/tools/listFolders.ts +32 -0
- package/src/tools/listProjects.ts +32 -0
- package/src/tools/listTags.ts +32 -0
- package/src/tools/listTasks.ts +56 -0
- package/src/tools/moveProject.ts +20 -0
- package/src/tools/moveTask.ts +20 -0
- package/src/tools/resolveName.ts +37 -0
- package/test/integration/.gitkeep +0 -0
- package/test/integration/completeProject.int.test.ts +25 -0
- package/test/integration/completeTask.int.test.ts +30 -0
- package/test/integration/createFolder.int.test.ts +50 -0
- package/test/integration/createProject.int.test.ts +49 -0
- package/test/integration/createTag.int.test.ts +52 -0
- package/test/integration/createTask.int.test.ts +55 -0
- package/test/integration/deleteFolder.int.test.ts +64 -0
- package/test/integration/deleteProject.int.test.ts +31 -0
- package/test/integration/deleteTag.int.test.ts +61 -0
- package/test/integration/deleteTask.int.test.ts +36 -0
- package/test/integration/dropProject.int.test.ts +24 -0
- package/test/integration/dropTask.int.test.ts +29 -0
- package/test/integration/editFolder.int.test.ts +43 -0
- package/test/integration/editProject.int.test.ts +39 -0
- package/test/integration/editTag.int.test.ts +43 -0
- package/test/integration/editTask.int.test.ts +56 -0
- package/test/integration/fixtures.ts +219 -0
- package/test/integration/getTask.int.test.ts +64 -0
- package/test/integration/listFoldersFiltered.int.test.ts +98 -0
- package/test/integration/listProjects.int.test.ts +73 -0
- package/test/integration/listProjectsFiltered.int.test.ts +96 -0
- package/test/integration/listTagsFiltered.int.test.ts +54 -0
- package/test/integration/listTasksFiltered.int.test.ts +141 -0
- package/test/integration/moveProject.int.test.ts +57 -0
- package/test/integration/moveTask.int.test.ts +61 -0
- package/test/integration/plannedDate.int.test.ts +72 -0
- package/test/integration/preflight.ts +60 -0
- package/test/integration/resolveName.int.test.ts +86 -0
- package/test/integration/taskRecurrence.int.test.ts +106 -0
- package/test/unit/.gitkeep +0 -0
- package/test/unit/bridge.injection.test.ts +66 -0
- package/test/unit/resultProtocol.test.ts +71 -0
- package/test/unit/schemas.createFolder.test.ts +38 -0
- package/test/unit/schemas.createProject.test.ts +115 -0
- package/test/unit/schemas.createTask.test.ts +87 -0
- package/test/unit/schemas.editTag.test.ts +64 -0
- package/test/unit/schemas.folderTagFiltering.test.ts +42 -0
- package/test/unit/schemas.listProjects.test.ts +44 -0
- package/test/unit/schemas.moveOperations.test.ts +60 -0
- package/test/unit/schemas.recurrence.test.ts +120 -0
- package/test/unit/schemas.test.ts +126 -0
- package/test/unit/snippetLoader.test.ts +56 -0
- package/test/unit/tools.deleteTask.test.ts +19 -0
- package/test/unit/tools.listTasks.test.ts +126 -0
- package/tsconfig.json +19 -0
- package/vitest.config.ts +8 -0
- package/vitest.integration.config.ts +18 -0
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { MoveTaskInput, TaskSummary } from "../schemas/index.js";
|
|
3
|
+
export type MoveTaskInputType = z.infer<typeof MoveTaskInput>;
|
|
4
|
+
export declare function moveTaskHandler(input: MoveTaskInputType): Promise<z.infer<typeof TaskSummary>>;
|
|
5
|
+
export declare const moveTaskTool: {
|
|
6
|
+
readonly name: "move_task";
|
|
7
|
+
readonly description: "Move a task to a different project (making it a top-level task) or make it a subtask of another task. Exactly one of projectId or parentTaskId must be provided. Throws a not-found error if any ID does not exist.";
|
|
8
|
+
readonly inputSchema: z.ZodEffects<z.ZodObject<{
|
|
9
|
+
id: z.ZodString;
|
|
10
|
+
projectId: z.ZodOptional<z.ZodString>;
|
|
11
|
+
parentTaskId: z.ZodOptional<z.ZodString>;
|
|
12
|
+
}, "strip", z.ZodTypeAny, {
|
|
13
|
+
id: string;
|
|
14
|
+
parentTaskId?: string | undefined;
|
|
15
|
+
projectId?: string | undefined;
|
|
16
|
+
}, {
|
|
17
|
+
id: string;
|
|
18
|
+
parentTaskId?: string | undefined;
|
|
19
|
+
projectId?: string | undefined;
|
|
20
|
+
}>, {
|
|
21
|
+
id: string;
|
|
22
|
+
parentTaskId?: string | undefined;
|
|
23
|
+
projectId?: string | undefined;
|
|
24
|
+
}, {
|
|
25
|
+
id: string;
|
|
26
|
+
parentTaskId?: string | undefined;
|
|
27
|
+
projectId?: string | undefined;
|
|
28
|
+
}>;
|
|
29
|
+
readonly handler: typeof moveTaskHandler;
|
|
30
|
+
};
|
|
31
|
+
//# sourceMappingURL=moveTask.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"moveTask.d.ts","sourceRoot":"","sources":["../../src/tools/moveTask.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB,OAAO,EAAE,aAAa,EAAE,WAAW,EAAE,MAAM,qBAAqB,CAAC;AAEjE,MAAM,MAAM,iBAAiB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,aAAa,CAAC,CAAC;AAE9D,wBAAsB,eAAe,CACnC,KAAK,EAAE,iBAAiB,GACvB,OAAO,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,WAAW,CAAC,CAAC,CAGtC;AAED,eAAO,MAAM,YAAY;;;;;;;;;;;;;;;;;;;;;;;;;CAMf,CAAC"}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { runSnippet } from "../runtime/index.js";
|
|
2
|
+
import { MoveTaskInput, TaskSummary } from "../schemas/index.js";
|
|
3
|
+
export async function moveTaskHandler(input) {
|
|
4
|
+
const raw = await runSnippet("move_task", input);
|
|
5
|
+
return TaskSummary.parse(raw);
|
|
6
|
+
}
|
|
7
|
+
export const moveTaskTool = {
|
|
8
|
+
name: "move_task",
|
|
9
|
+
description: "Move a task to a different project (making it a top-level task) or make it a subtask of another task. Exactly one of projectId or parentTaskId must be provided. Throws a not-found error if any ID does not exist.",
|
|
10
|
+
inputSchema: MoveTaskInput,
|
|
11
|
+
handler: moveTaskHandler,
|
|
12
|
+
};
|
|
13
|
+
//# sourceMappingURL=moveTask.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"moveTask.js","sourceRoot":"","sources":["../../src/tools/moveTask.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,UAAU,EAAE,MAAM,qBAAqB,CAAC;AACjD,OAAO,EAAE,aAAa,EAAE,WAAW,EAAE,MAAM,qBAAqB,CAAC;AAIjE,MAAM,CAAC,KAAK,UAAU,eAAe,CACnC,KAAwB;IAExB,MAAM,GAAG,GAAG,MAAM,UAAU,CAAC,WAAW,EAAE,KAAK,CAAC,CAAC;IACjD,OAAO,WAAW,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;AAChC,CAAC;AAED,MAAM,CAAC,MAAM,YAAY,GAAG;IAC1B,IAAI,EAAE,WAAW;IACjB,WAAW,EACT,qNAAqN;IACvN,WAAW,EAAE,aAAa;IAC1B,OAAO,EAAE,eAAe;CAChB,CAAC"}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { ResolveCandidate } from "../schemas/index.js";
|
|
3
|
+
export declare const resolveNameSchema: z.ZodObject<{
|
|
4
|
+
type: z.ZodEnum<["task", "project", "folder", "tag", "perspective"]>;
|
|
5
|
+
query: z.ZodString;
|
|
6
|
+
scope: z.ZodOptional<z.ZodString>;
|
|
7
|
+
}, "strip", z.ZodTypeAny, {
|
|
8
|
+
type: "task" | "project" | "folder" | "tag" | "perspective";
|
|
9
|
+
query: string;
|
|
10
|
+
scope?: string | undefined;
|
|
11
|
+
}, {
|
|
12
|
+
type: "task" | "project" | "folder" | "tag" | "perspective";
|
|
13
|
+
query: string;
|
|
14
|
+
scope?: string | undefined;
|
|
15
|
+
}>;
|
|
16
|
+
export type ResolveNameInput = z.infer<typeof resolveNameSchema>;
|
|
17
|
+
export declare function resolveNameHandler(input: ResolveNameInput): Promise<z.infer<typeof ResolveCandidate>[]>;
|
|
18
|
+
export declare const resolveNameTool: {
|
|
19
|
+
readonly name: "resolve_name";
|
|
20
|
+
readonly description: "Resolve an entity name to its stable ID(s). Returns ALL matches — never silently picks one. If multiple candidates are returned, ask the user or caller to disambiguate using the path field before proceeding with a write operation.";
|
|
21
|
+
readonly inputSchema: z.ZodObject<{
|
|
22
|
+
type: z.ZodEnum<["task", "project", "folder", "tag", "perspective"]>;
|
|
23
|
+
query: z.ZodString;
|
|
24
|
+
scope: z.ZodOptional<z.ZodString>;
|
|
25
|
+
}, "strip", z.ZodTypeAny, {
|
|
26
|
+
type: "task" | "project" | "folder" | "tag" | "perspective";
|
|
27
|
+
query: string;
|
|
28
|
+
scope?: string | undefined;
|
|
29
|
+
}, {
|
|
30
|
+
type: "task" | "project" | "folder" | "tag" | "perspective";
|
|
31
|
+
query: string;
|
|
32
|
+
scope?: string | undefined;
|
|
33
|
+
}>;
|
|
34
|
+
readonly handler: typeof resolveNameHandler;
|
|
35
|
+
};
|
|
36
|
+
//# sourceMappingURL=resolveName.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"resolveName.d.ts","sourceRoot":"","sources":["../../src/tools/resolveName.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB,OAAO,EAAc,gBAAgB,EAAE,MAAM,qBAAqB,CAAC;AAEnE,eAAO,MAAM,iBAAiB;;;;;;;;;;;;EAW5B,CAAC;AAEH,MAAM,MAAM,gBAAgB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,iBAAiB,CAAC,CAAC;AAEjE,wBAAsB,kBAAkB,CACtC,KAAK,EAAE,gBAAgB,GACtB,OAAO,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,gBAAgB,CAAC,EAAE,CAAC,CAO7C;AAED,eAAO,MAAM,eAAe;;;;;;;;;;;;;;;;;CAMlB,CAAC"}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { runSnippet } from "../runtime/index.js";
|
|
3
|
+
import { EntityType, ResolveCandidate } from "../schemas/index.js";
|
|
4
|
+
export const resolveNameSchema = z.object({
|
|
5
|
+
type: EntityType.describe("The entity type to search: task, project, folder, tag, or perspective"),
|
|
6
|
+
query: z.string().min(1).describe("Exact name to search for"),
|
|
7
|
+
scope: z
|
|
8
|
+
.string()
|
|
9
|
+
.optional()
|
|
10
|
+
.describe('Optional path prefix to narrow results, e.g. "Work ▸ Clients"'),
|
|
11
|
+
});
|
|
12
|
+
export async function resolveNameHandler(input) {
|
|
13
|
+
const raw = await runSnippet("resolve_name", {
|
|
14
|
+
type: input.type,
|
|
15
|
+
query: input.query,
|
|
16
|
+
scope: input.scope ?? null,
|
|
17
|
+
});
|
|
18
|
+
return z.array(ResolveCandidate).parse(raw);
|
|
19
|
+
}
|
|
20
|
+
export const resolveNameTool = {
|
|
21
|
+
name: "resolve_name",
|
|
22
|
+
description: "Resolve an entity name to its stable ID(s). Returns ALL matches — never silently picks one. If multiple candidates are returned, ask the user or caller to disambiguate using the path field before proceeding with a write operation.",
|
|
23
|
+
inputSchema: resolveNameSchema,
|
|
24
|
+
handler: resolveNameHandler,
|
|
25
|
+
};
|
|
26
|
+
//# sourceMappingURL=resolveName.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"resolveName.js","sourceRoot":"","sources":["../../src/tools/resolveName.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AACxB,OAAO,EAAE,UAAU,EAAE,MAAM,qBAAqB,CAAC;AACjD,OAAO,EAAE,UAAU,EAAE,gBAAgB,EAAE,MAAM,qBAAqB,CAAC;AAEnE,MAAM,CAAC,MAAM,iBAAiB,GAAG,CAAC,CAAC,MAAM,CAAC;IACxC,IAAI,EAAE,UAAU,CAAC,QAAQ,CACvB,uEAAuE,CACxE;IACD,KAAK,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,0BAA0B,CAAC;IAC7D,KAAK,EAAE,CAAC;SACL,MAAM,EAAE;SACR,QAAQ,EAAE;SACV,QAAQ,CACP,+DAA+D,CAChE;CACJ,CAAC,CAAC;AAIH,MAAM,CAAC,KAAK,UAAU,kBAAkB,CACtC,KAAuB;IAEvB,MAAM,GAAG,GAAG,MAAM,UAAU,CAAC,cAAc,EAAE;QAC3C,IAAI,EAAE,KAAK,CAAC,IAAI;QAChB,KAAK,EAAE,KAAK,CAAC,KAAK;QAClB,KAAK,EAAE,KAAK,CAAC,KAAK,IAAI,IAAI;KAC3B,CAAC,CAAC;IACH,OAAO,CAAC,CAAC,KAAK,CAAC,gBAAgB,CAAC,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;AAC9C,CAAC;AAED,MAAM,CAAC,MAAM,eAAe,GAAG;IAC7B,IAAI,EAAE,cAAc;IACpB,WAAW,EACT,wOAAwO;IAC1O,WAAW,EAAE,iBAAiB;IAC9B,OAAO,EAAE,kBAAkB;CACnB,CAAC"}
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
## Context
|
|
2
|
+
|
|
3
|
+
OmniFocus exposes two distinct automation surfaces on macOS:
|
|
4
|
+
|
|
5
|
+
1. **The scripting dictionary**, reachable from AppleScript and JXA (`osascript -l JavaScript`). This is the legacy surface the prior MCP server targeted by generating AppleScript source strings. It lacks folder creation, Single Actions list type, task moves, stable IDs, and recurrence-rule construction. It also requires string-concatenation of arguments, which is the root cause of the apostrophe and name-ambiguity bug classes.
|
|
6
|
+
|
|
7
|
+
2. **OmniJS**, the modern Omni Automation JavaScript runtime that lives *inside* the OmniFocus process. This is the API documented at omni-automation.com. It exposes `Task`, `Project`, `Folder`, `Tag`, `Task.RepetitionRule`, `Perspective`, `id.primaryKey`, `moveTasks`, `new Folder`, and everything else the prior server couldn't reach. It is reachable from outside OmniFocus *only* via the `evaluateJavascript` method on the scripting-dictionary application object.
|
|
8
|
+
|
|
9
|
+
The two are not interchangeable. The scripting dictionary is a capability-limited facade over the same underlying database; OmniJS is the authoritative modern API. Every "cannot do X" limitation in the prior server is a limitation of the scripting dictionary, not of JXA or macOS automation generally.
|
|
10
|
+
|
|
11
|
+
The project starts from a clean slate: no legacy code, no prior specs, no backwards-compatibility constraints. The only inputs are the Omni Automation API surface, the MCP protocol, and the enumerated failure modes of the prior server.
|
|
12
|
+
|
|
13
|
+
## Goals / Non-Goals
|
|
14
|
+
|
|
15
|
+
**Goals:**
|
|
16
|
+
|
|
17
|
+
- Expose the full Omni Automation API surface through an MCP server, eventually reaching 1:1 coverage. This change establishes the foundation; subsequent changes fill in capabilities.
|
|
18
|
+
- Eliminate by construction the bug classes of the prior server: apostrophe-breaking, name-ambiguity routing, parse-error-on-success, type-coercion failures in batch parameters, missing capability coverage.
|
|
19
|
+
- Make every domain operation authored as a static, standalone `.js` snippet that can be pasted into OmniFocus's Automation Console for manual verification.
|
|
20
|
+
- Provide stable, id-based addressing for every OmniFocus entity, with name/path resolution as a separate, explicit, ambiguity-reporting step.
|
|
21
|
+
- Establish an integration-test harness that exercises a real OmniFocus instance without polluting the user's data or leaking fixtures across sync.
|
|
22
|
+
- Name every target capability up front so future changes have a landing place and the scope of "full API coverage" is visible from the first commit.
|
|
23
|
+
|
|
24
|
+
**Non-Goals:**
|
|
25
|
+
|
|
26
|
+
- Cross-platform support. Omni Automation is macOS-only; the server is macOS-only.
|
|
27
|
+
- Compatibility with the prior AppleScript-based MCP server. Tool names, parameter shapes, and result envelopes are all new.
|
|
28
|
+
- Interactive UI automation: `Form`, `Alert`, `FilePicker`, `FileSaver`. These APIs require a human to click in the OmniFocus app while the script blocks; they cannot cross an MCP boundary without changing semantics.
|
|
29
|
+
- Device-local side effects unrelated to OmniFocus data: `Speech`, `Device`, clipboard mutation.
|
|
30
|
+
- Window mutation (resize, close, focus). Window *state* is exposed read-only; mutation is low-value and fragile.
|
|
31
|
+
- User-visible notifications posted from scripts. Out by default; reconsiderable if a concrete use case emerges.
|
|
32
|
+
- Custom perspective *creation and editing*. Listing and activation are in scope; programmatic definition of perspective rules is likely not scriptable even in OmniFocus Pro and is marked non-goal pending verification when the perspective-management change is drafted.
|
|
33
|
+
- CI coverage for integration tests. CI runs unit tests only; integration tests are local-developer-only by the nature of requiring a running OmniFocus instance.
|
|
34
|
+
- Transactional semantics across batch operations. OmniFocus does not expose transactions; batch tools return per-item result arrays with partial-success semantics.
|
|
35
|
+
|
|
36
|
+
## Decisions
|
|
37
|
+
|
|
38
|
+
### Decision 1: JXA is a thin shim; all domain logic runs inside OmniJS via `evaluateJavascript`.
|
|
39
|
+
|
|
40
|
+
**Choice:** Every tool invocation spawns `osascript -l JavaScript` running a minimal JXA wrapper whose only job is to call `Application('OmniFocus').evaluateJavascript(snippet)`, catch exceptions, and print one line of JSON to stdout. All domain logic — creating folders, setting project types, constructing repetition rules, reading IDs, walking hierarchies — lives in OmniJS snippets and runs inside the OmniFocus process.
|
|
41
|
+
|
|
42
|
+
**Rationale:** The capability gap between the scripting dictionary and OmniJS is the *entire* reason the prior server failed to cover critical features. Targeting OmniJS directly is the only path to feature parity with the Omni Automation API. JXA's only value here is as the carrier that reaches `evaluateJavascript`; nothing else in the JXA scripting-dictionary surface is used.
|
|
43
|
+
|
|
44
|
+
**Alternatives considered:**
|
|
45
|
+
|
|
46
|
+
- *Pure AppleScript, better escaping.* Rejected: the capability gap is structural; no amount of escaping fixes the missing folder, type, move, ID, and recurrence capabilities.
|
|
47
|
+
- *`omnifocus://` URL automation.* Rejected: URL schemes support a narrow slice of operations (add to inbox, open document, complete task) and provide no return channel for IDs or query results.
|
|
48
|
+
- *OmniFocus plug-in bundles installed into the user's library.* Rejected: requires out-of-band user setup, complicates distribution, and still requires an IPC channel back to the MCP server.
|
|
49
|
+
- *Shortcuts.app automation.* Rejected: similar return-channel and capability limitations, and adds a dependency on Shortcuts being configured.
|
|
50
|
+
|
|
51
|
+
**Bugs this kills:** "cannot create folders," "cannot create Single Actions list," "cannot move tasks between projects," "no project IDs returned," "cannot set recurrence," "folder name ambiguity" (because paths can be resolved via `flattenedFolders` inside OmniJS rather than scripting-dictionary `whose` clauses).
|
|
52
|
+
|
|
53
|
+
### Decision 2: Arguments are embedded as JSON literals; no string interpolation anywhere.
|
|
54
|
+
|
|
55
|
+
**Choice:** Snippet files are static `.js` files containing exactly one placeholder token: `__ARGS__`. At runtime, the TypeScript side constructs the script to execute via `template.replace("__ARGS__", JSON.stringify(args))`. The snippet reads `const args = __ARGS__;` as its first line. No other interpolation of user-supplied data into script source is permitted anywhere in the codebase.
|
|
56
|
+
|
|
57
|
+
**Rationale:** JSON is a syntactic subset of JavaScript. `JSON.stringify` of any JavaScript value produces a string that is both valid JSON and a valid JavaScript expression literal. Embedding it directly into source is injection-safe for arbitrary strings, including apostrophes, quotes, backslashes, newlines, unicode, and emoji — precisely the inputs that broke the prior server. It also has the useful property that the generated script is a standalone, paste-ready snippet: a developer can hand-edit `__ARGS__` in the template and paste it into OmniFocus's Automation Console to reproduce any operation.
|
|
58
|
+
|
|
59
|
+
**Alternatives considered:**
|
|
60
|
+
|
|
61
|
+
- *Passing arguments via environment variables.* Rejected: `evaluateJavascript` runs inside OmniFocus's process, which does not inherit the `osascript` environment. The snippet could not read them.
|
|
62
|
+
- *Writing arguments to a temp file and having the snippet read it.* Rejected: OmniJS has no general filesystem read primitive; `FileWrapper` is attachment-scoped. Adds a cleanup burden and a failure mode.
|
|
63
|
+
- *String-escape templating (Handlebars, Mustache, manual backslash escaping).* Rejected: every escape scheme has edge cases. The JSON-literal approach has none because JSON is closed under JavaScript expression syntax.
|
|
64
|
+
|
|
65
|
+
**Bugs this kills:** "apostrophes in task/project names break AppleScript string generation," plus the entire class of unicode and escape-sequence bugs that haven't been hit yet but would eventually surface under string concatenation.
|
|
66
|
+
|
|
67
|
+
### Decision 3: Results flow as exactly one line of JSON on stdout.
|
|
68
|
+
|
|
69
|
+
**Choice:** Every snippet's last expression is `JSON.stringify({ok: true, data: <result>})` or throws. The JXA shim wraps the `evaluateJavascript` call in try/catch. On success, it prints the returned string verbatim followed by a single newline. On failure, it prints `JSON.stringify({ok: false, error: {name, message, stack}})` followed by a single newline. The TypeScript runtime reads stdout, takes the first JSON-parseable line, and asserts `ok`.
|
|
70
|
+
|
|
71
|
+
**Rationale:** A strict protocol eliminates ambiguity about whether an operation succeeded. The prior server's "parse error on success" bug was a direct consequence of treating incidental AppleScript output as part of the result payload. With a one-line JSON contract, success and failure are trivially distinguishable and the protocol is easy to test in isolation.
|
|
72
|
+
|
|
73
|
+
**Alternatives considered:**
|
|
74
|
+
|
|
75
|
+
- *Multi-line JSON with framing.* Rejected: unnecessary complexity for the single-operation-per-invocation model.
|
|
76
|
+
- *Exit code as the success signal.* Rejected: `osascript` exit codes are unreliable across JXA error modes, and an exit code doesn't carry error details.
|
|
77
|
+
- *Writing results to a temp file.* Rejected: adds a cleanup burden and a filesystem round-trip with no benefit.
|
|
78
|
+
|
|
79
|
+
**Bugs this kills:** "`remove_item` returns parse error on success even when operation succeeded."
|
|
80
|
+
|
|
81
|
+
### Decision 4: `id.primaryKey` is the canonical address for every entity; name resolution is a separate, explicit, ambiguity-reporting step.
|
|
82
|
+
|
|
83
|
+
**Choice:** Every read tool returns `{id, ...}` where `id` is `obj.id.primaryKey`. Every write tool (in this and future changes) accepts `id` as the primary addressing parameter. Name and path resolution is a dedicated `resolve_name` tool that takes `{type, query, scope?}` and returns a list of candidate `{id, name, path, ...}` entries. When more than one candidate matches, the tool returns all of them and the caller must decide; it never silently picks.
|
|
84
|
+
|
|
85
|
+
**Rationale:** The prior server's duplicate-name and folder-ambiguity bugs are consequences of name-based addressing with silent disambiguation. IDs are stable across sessions within OmniFocus. Making ID the canonical address and forcing callers (including LLMs) to resolve ambiguity explicitly is the only way to produce deterministic behavior in a database where names are user-chosen and frequently collide.
|
|
86
|
+
|
|
87
|
+
**Alternatives considered:**
|
|
88
|
+
|
|
89
|
+
- *Name-based addressing with a "pick the first match" default.* Rejected: this is exactly what the prior server did, and it routed to the wrong target.
|
|
90
|
+
- *Path-only addressing.* Rejected: paths are fragile to user renames and still ambiguous when the same path exists under different roots (e.g., the same tag name under different parents).
|
|
91
|
+
- *Auto-disambiguation via fuzzy scoring.* Rejected: silent selection is precisely the failure mode being fixed.
|
|
92
|
+
|
|
93
|
+
**Open question:** Whether `id.primaryKey` values survive OmniFocus sync across devices and across database rebuilds. This is believed to be true but will be verified during the execution-runtime implementation and documented in the runtime README. If IDs turn out not to survive sync, the contract still holds within a single session; callers re-resolving after a sync event is an acceptable fallback.
|
|
94
|
+
|
|
95
|
+
**Bugs this kills:** "duplicate project names cause wrong-target routing," "folder name ambiguity — same folder name under different parents routes incorrectly," "no project IDs returned — projects only addressable by name."
|
|
96
|
+
|
|
97
|
+
### Decision 5: Tool parameters are validated with zod at the TS boundary; enums are strict; no type coercion.
|
|
98
|
+
|
|
99
|
+
**Choice:** Every MCP tool handler defines its input schema as a zod object. Handlers receive already-parsed, already-validated arguments. Enums like project type are `z.enum(["parallel", "sequential", "singleActions"])`. Batch arrays are `z.array(z.object({...}))` — never strings that look like JSON. Invalid inputs are rejected at the TS boundary with structured error messages before any snippet runs.
|
|
100
|
+
|
|
101
|
+
**Rationale:** The prior server had `sequential` parameters that arrived as strings and were treated as booleans with broken coercion, and `batch_remove_items` parameters that arrived as serialized JSON strings instead of arrays. These are the bug pattern you get when tool schemas are loose and the handler does ad-hoc type massaging. zod at the boundary makes the schema the contract.
|
|
102
|
+
|
|
103
|
+
**Alternatives considered:**
|
|
104
|
+
|
|
105
|
+
- *Ad-hoc `typeof` checks in handlers.* Rejected: this is what produced the original bugs.
|
|
106
|
+
- *JSON Schema validation via ajv.* Considered and deferred: zod is TypeScript-native, produces better type inference, and integrates naturally with the MCP SDK's expected schema surface. ajv is fine but adds a second schema language.
|
|
107
|
+
|
|
108
|
+
**Bugs this kills:** "`sequential` parameter must be a true boolean, not string," "`batch_remove_items` items parameter must be a JSON array, not serialized string."
|
|
109
|
+
|
|
110
|
+
### Decision 6: Integration tests scope all fixtures under a disposable folder and refuse to run under sync by default.
|
|
111
|
+
|
|
112
|
+
**Choice:** Every integration test run generates a UUID and creates a top-level folder named `__MCP_TEST_<uuid>__` in the user's OmniFocus database. All fixture projects, folders, tasks, and tags are created inside it. Teardown deletes the folder and everything under it. A preflight check inspects the database's sync configuration and refuses to run if sync is enabled, unless `MCP_TEST_ALLOW_SYNC=1` is set in the environment. Integration tests run serially (OmniFocus is a singleton). CI runs unit tests only.
|
|
113
|
+
|
|
114
|
+
**Rationale:** Integration tests against a real OmniFocus instance are the only way to validate the OmniJS API contract — mocks would drift. But real-database testing has a failure mode where fixtures leak into the user's actual workflow or, worse, sync to other devices. Fixture-folder scoping contains the blast radius; the sync preflight prevents cross-device leakage by default with an explicit opt-out for users who understand the tradeoff.
|
|
115
|
+
|
|
116
|
+
**Alternatives considered:**
|
|
117
|
+
|
|
118
|
+
- *Mocking the OmniJS API.* Rejected: this is the path to building something that passes tests and fails in production.
|
|
119
|
+
- *Dedicated test OmniFocus document.* Rejected: OmniFocus does not support multiple documents in a way that's ergonomic for automated switching.
|
|
120
|
+
- *Snapshot-and-restore the database.* Rejected: there is no supported API for this; copying the sqlite file behind OmniFocus's back is unsafe.
|
|
121
|
+
|
|
122
|
+
## Risks / Trade-offs
|
|
123
|
+
|
|
124
|
+
- **Risk:** `id.primaryKey` may not survive OmniFocus sync or database rebuilds. → **Mitigation:** Verify during runtime implementation and document findings. If IDs don't survive sync, document the session-scope guarantee and update `resolve_name` guidance accordingly. This is an open question, not a blocker.
|
|
125
|
+
|
|
126
|
+
- **Risk:** `evaluateJavascript` may have undocumented size limits on the script argument. → **Mitigation:** Keep snippets small and focused (one operation per file). For batch operations (future change), the snippet iterates over an args array; it does not inline per-item code.
|
|
127
|
+
|
|
128
|
+
- **Risk:** OmniJS error messages inside `evaluateJavascript` may be lossy when surfaced through JXA. → **Mitigation:** The snippet catches its own errors and constructs the error envelope itself before returning, so error details never cross the OmniJS→JXA boundary as a raw exception.
|
|
129
|
+
|
|
130
|
+
- **Risk:** Integration tests running against a real OmniFocus instance can still pollute the user's data if teardown fails mid-run. → **Mitigation:** Fixture folders are prefixed with `__MCP_TEST_` and include a UUID; a `clean-test-fixtures` dev script scans for and removes any stragglers. README documents the manual cleanup command.
|
|
131
|
+
|
|
132
|
+
- **Risk:** OmniFocus sync conflicts from concurrent modifications during a test run. → **Mitigation:** Serial test execution plus the sync preflight. Under the opt-in sync-allowed mode, users accept the risk explicitly.
|
|
133
|
+
|
|
134
|
+
- **Trade-off:** The MCP server is macOS-only. This is an inherent property of Omni Automation and not negotiable without abandoning the capability goals.
|
|
135
|
+
|
|
136
|
+
- **Trade-off:** Every tool invocation spawns an `osascript` process, which has startup cost. For single operations this is fine; for high-frequency batch operations it would be noticeable. Batch tools (future change) will therefore execute as a single snippet that iterates internally, not as a loop of tool calls from the caller side.
|
|
137
|
+
|
|
138
|
+
- **Trade-off:** Snippet files live outside the TypeScript compilation unit. Refactoring a shared OmniJS helper means editing `.js` files that tsc doesn't type-check. Accepted because testability-by-paste is more valuable than type-checking across the boundary; the JS side is small and exercised by integration tests.
|
|
139
|
+
|
|
140
|
+
## Bug-to-decision traceability
|
|
141
|
+
|
|
142
|
+
Every documented failure mode of the prior server is mapped to the decision that eliminates it, so the rationale survives archival:
|
|
143
|
+
|
|
144
|
+
| Prior-server bug | Killed by |
|
|
145
|
+
|---|---|
|
|
146
|
+
| Cannot create folders | Decision 1 (OmniJS exposes `new Folder`) |
|
|
147
|
+
| Cannot create Single Actions list | Decision 1 (OmniJS exposes `containsSingletonActions`) |
|
|
148
|
+
| Cannot move tasks between projects | Decision 1 (OmniJS exposes `moveTasks`) |
|
|
149
|
+
| No project IDs returned | Decisions 1 + 4 (`id.primaryKey` exists in OmniJS and is the canonical address) |
|
|
150
|
+
| Cannot set recurrence/repeat | Decision 1 (OmniJS exposes `Task.RepetitionRule`) |
|
|
151
|
+
| Apostrophes break script generation | Decision 2 (JSON-literal argument embedding) |
|
|
152
|
+
| Duplicate project names cause wrong-target routing | Decision 4 (ID addressing + ambiguity-reporting resolution) |
|
|
153
|
+
| Folder name ambiguity under different parents | Decision 4 (ID addressing + path-based resolution with ambiguity reporting) |
|
|
154
|
+
| `sequential` parameter must be a true boolean, not string | Decision 5 (zod enum validation at TS boundary) |
|
|
155
|
+
| `batch_remove_items` items parameter must be a JSON array | Decision 5 (zod array validation at TS boundary) |
|
|
156
|
+
| `remove_item` returns parse error on success | Decision 3 (strict one-line JSON result protocol) |
|
|
157
|
+
|
|
158
|
+
## Open Questions
|
|
159
|
+
|
|
160
|
+
- Do `id.primaryKey` values survive OmniFocus sync across devices and database rebuilds? To be verified during runtime implementation.
|
|
161
|
+
- Does `evaluateJavascript` have a practical size limit on the script argument? To be measured during runtime implementation and documented as an upper bound for future batch snippet design.
|
|
162
|
+
- Does OmniJS expose a reliable way to detect whether sync is enabled without mutating state? Needed for the integration-test preflight check. If not, the preflight falls back to reading the user's OmniFocus preferences via JXA before entering the OmniJS bridge.
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
## Why
|
|
2
|
+
|
|
3
|
+
OmniFocus needs an MCP server that exposes the full Omni Automation JavaScript API to LLM callers. A prior AppleScript-string-generating server exists but has structural limitations: it cannot create folders, cannot create Single Actions lists, cannot move tasks between projects, cannot set recurrence rules, returns no stable IDs, breaks on apostrophes in names, silently routes to the wrong target on duplicate names, and mis-parses successful results as errors. These are not bugs to patch — they are consequences of generating AppleScript source as concatenated strings against a scripting dictionary that lacks the needed capabilities.
|
|
4
|
+
|
|
5
|
+
This change starts from a clean slate with an architecture that kills those bug classes by construction: all operations execute inside OmniFocus's OmniJS runtime (where the modern API lives) via a JXA bridge, arguments are embedded as JSON literals (never concatenated), identity is always `id.primaryKey`, and results flow as a strict one-line JSON protocol. This first change establishes the runtime foundation and names every capability that will eventually exist, so subsequent changes can fill in the API surface incrementally without re-litigating the architecture.
|
|
6
|
+
|
|
7
|
+
## What Changes
|
|
8
|
+
|
|
9
|
+
- Establish a TypeScript MCP server scaffold (`@modelcontextprotocol/sdk`, zod for schemas, vitest for tests) with `src/runtime/`, `src/snippets/`, `src/tools/`, `src/schemas/`, `src/server.ts`, `test/unit/`, `test/integration/`, `package.json`, `tsconfig.json`.
|
|
10
|
+
- Implement the execution runtime: a JXA shim that calls `Application('OmniFocus').evaluateJavascript(snippet)`, wraps results in a strict `{ok, data} | {ok: false, error}` envelope, and prints exactly one line of JSON to stdout.
|
|
11
|
+
- Implement the snippet-authoring convention: static `.js` files with a single `__ARGS__` placeholder, injected by `template.replace("__ARGS__", JSON.stringify(args))`. No other interpolation is permitted anywhere in the codebase.
|
|
12
|
+
- Implement the identity model: every read returns `id.primaryKey`; every write will accept `id` as the canonical address. Name/path resolution is a separate `resolve_name` tool that returns candidate lists on ambiguity and never silently picks a winner.
|
|
13
|
+
- Deliver the minimum-viable read surface needed to prove the bridge end-to-end: `list_projects`, `get_project`, `list_folders`, `get_folder`, `list_tasks`, `get_task`, `list_tags`, `get_tag`, `resolve_name`. No mutating tools in this change.
|
|
14
|
+
- Establish the integration-test harness against a real OmniFocus instance, with all fixtures scoped under a disposable `__MCP_TEST_<uuid>__` top-level folder and a preflight that refuses to run when OmniFocus sync is enabled (unless `MCP_TEST_ALLOW_SYNC=1`).
|
|
15
|
+
- Name every target capability as a stub spec so future changes have a landing place: `recurrence`, `perspective-management`, `window-state`, `forecast`, `database-inspection`, `batch-operations`, `attachments`, `url-automation`, `settings`. Stubs declare scope and non-goals without specifying requirements that later changes will own.
|
|
16
|
+
- Document explicit non-goals: interactive UI APIs (`Form`, `Alert`, `FilePicker`, `FileSaver`), `Speech`, `Device`, clipboard mutation, window mutation, notifications, and custom perspective creation/editing. These are recorded as non-goals in design.md so they don't look like oversights.
|
|
17
|
+
|
|
18
|
+
## Capabilities
|
|
19
|
+
|
|
20
|
+
### New Capabilities
|
|
21
|
+
|
|
22
|
+
- `execution-runtime`: The JXA→OmniJS bridge, snippet loader, argument-injection rule, one-line JSON result protocol, and error model. Fully specified in this change.
|
|
23
|
+
- `identity-resolution`: `id.primaryKey` addressing, name/path resolution with ambiguity reporting, and the `resolve_name` tool contract. Fully specified in this change.
|
|
24
|
+
- `task-management`: Task read operations (`list_tasks`, `get_task`) specified in this change; full CRUD, move, completion, and hierarchy operations land in the `core-crud` change.
|
|
25
|
+
- `project-management`: Project read operations (`list_projects`, `get_project`) specified in this change; CRUD, type (parallel/sequential/singleActions), status, and review metadata land in `core-crud`.
|
|
26
|
+
- `folder-management`: Folder read operations (`list_folders`, `get_folder`) specified in this change; CRUD, nesting, rename, and move land in `core-crud`.
|
|
27
|
+
- `tag-management`: Tag read operations (`list_tags`, `get_tag`) specified in this change; CRUD, nesting, assignment, and status land in `core-crud`.
|
|
28
|
+
- `recurrence`: Stub naming the capability for Task.RepetitionRule construction, RepetitionMethod, structured schema plus raw RRULE escape hatch, and clearing recurrence. Requirements land in the `recurrence` change.
|
|
29
|
+
- `perspective-management`: Stub naming the capability for listing built-in and custom perspectives, activating a perspective in a window, and reading perspective details. Custom perspective creation/editing is a non-goal pending API verification.
|
|
30
|
+
- `window-state`: Stub naming the capability for read-only window state (current window, active perspective, sidebar selection, content selection). Window mutation is a non-goal.
|
|
31
|
+
- `forecast`: Stub naming the capability for forecast days, tasks due/deferred on a day, and forecast metadata.
|
|
32
|
+
- `database-inspection`: Stub naming the capability for scoped, paged, filtered database traversal including `dump_database`, inbox contents, and database metadata.
|
|
33
|
+
- `batch-operations`: Stub naming the capability for batch variants of mutating tools with per-item result arrays and partial-success semantics.
|
|
34
|
+
- `attachments`: Stub naming the capability for `FileWrapper`-based attachment listing and round-trip on task notes.
|
|
35
|
+
- `url-automation`: Stub naming the capability for `omnifocus://` URL construction and parsing.
|
|
36
|
+
- `settings`: Stub naming the capability for reading app settings and preferences where exposed.
|
|
37
|
+
|
|
38
|
+
### Modified Capabilities
|
|
39
|
+
|
|
40
|
+
None. This is a clean-slate project with no prior specs.
|
|
41
|
+
|
|
42
|
+
## Impact
|
|
43
|
+
|
|
44
|
+
- **New repository structure** with TypeScript toolchain, MCP SDK, zod, vitest.
|
|
45
|
+
- **Runtime dependency on macOS** (`osascript`) and a running OmniFocus instance. The server is macOS-only by nature of the Omni Automation platform.
|
|
46
|
+
- **Integration tests mutate a real OmniFocus database.** Mitigated by fixture-folder scoping and a sync-enabled preflight check. Documented loudly in the README.
|
|
47
|
+
- **No CI coverage for integration tests.** CI runs unit tests only (snippet injection, zod schema validation, result-protocol parsing). Integration is local-developer-only.
|
|
48
|
+
- **Replaces a prior AppleScript-based MCP server.** Callers migrating from the old server will encounter breaking changes in tool names, parameter types, and result shapes. This is accepted because the prior server's contract was structurally broken; compatibility is not a goal.
|
|
49
|
+
- **No external network dependencies.** The server runs entirely locally against the user's OmniFocus database.
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
## ADDED Requirements
|
|
2
|
+
|
|
3
|
+
### Requirement: Capability declared
|
|
4
|
+
|
|
5
|
+
The `attachments` capability SHALL cover listing, reading, adding, and removing file attachments on task notes via the OmniJS `FileWrapper` API. The on-wire representation of attachment contents across the MCP boundary (inline base64, filesystem path reference, or a hybrid with a size threshold) SHALL be decided when the `attachments` change is drafted. Requirements for individual tools SHALL be added by that change.
|
|
6
|
+
|
|
7
|
+
#### Scenario: Capability is named and scoped
|
|
8
|
+
- **WHEN** a future change proposes adding attachment tools
|
|
9
|
+
- **THEN** it lands requirements under this capability and makes the on-wire encoding decision explicit
|
package/openspec/changes/archive/2026-04-09-bootstrap-omnifocus-mcp/specs/batch-operations/spec.md
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
## ADDED Requirements
|
|
2
|
+
|
|
3
|
+
### Requirement: Capability declared
|
|
4
|
+
|
|
5
|
+
The `batch-operations` capability SHALL cover batch variants of every mutating tool (create, update, delete, move, complete, assign). Batch tools SHALL accept strictly typed arrays validated at the TypeScript boundary (never serialized JSON strings) and SHALL return a per-item result array with partial-success semantics: each element is an `{ok, data?, error?}` envelope carrying that item's outcome independent of other items. Batch tools SHALL execute as a single OmniJS snippet that iterates internally, not as a loop of per-item bridge invocations. Requirements for individual batch tools SHALL be added by the `batch-operations` change.
|
|
6
|
+
|
|
7
|
+
#### Scenario: Capability is named and semantics are fixed
|
|
8
|
+
- **WHEN** a future change proposes adding batch tools
|
|
9
|
+
- **THEN** it lands requirements under this capability with typed-array validation and partial-success result semantics as invariants
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
## ADDED Requirements
|
|
2
|
+
|
|
3
|
+
### Requirement: Capability declared
|
|
4
|
+
|
|
5
|
+
The `database-inspection` capability SHALL cover scoped, paged, and filtered traversal of the full OmniFocus database, including a `dump_database` tool that accepts `{scope, id?, include, format}` parameters, inbox inspection, and database metadata (name, sync status, object counts). The default behavior of `dump_database` SHALL exclude completed tasks, dropped entities, and task notes to keep payloads manageable; those are opt-in via the `include` parameter. Requirements for individual tools SHALL be added by the `forecast-and-inspection` change.
|
|
6
|
+
|
|
7
|
+
#### Scenario: Capability is named and scoped
|
|
8
|
+
- **WHEN** a future change proposes adding database-inspection tools
|
|
9
|
+
- **THEN** it lands requirements under this capability rather than inventing a new capability name
|
package/openspec/changes/archive/2026-04-09-bootstrap-omnifocus-mcp/specs/execution-runtime/spec.md
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
## ADDED Requirements
|
|
2
|
+
|
|
3
|
+
### Requirement: OmniJS execution via JXA bridge
|
|
4
|
+
|
|
5
|
+
The system SHALL execute every OmniFocus domain operation inside OmniFocus's OmniJS runtime by invoking `Application('OmniFocus').evaluateJavascript(snippet)` from a JXA host process spawned via `osascript -l JavaScript`. The JXA host SHALL NOT invoke any OmniFocus scripting-dictionary method other than `evaluateJavascript` for domain operations.
|
|
6
|
+
|
|
7
|
+
#### Scenario: Tool invocation routes through the bridge
|
|
8
|
+
- **WHEN** a tool handler invokes the runtime with a snippet and args
|
|
9
|
+
- **THEN** the runtime spawns `osascript -l JavaScript`, passes a JXA wrapper that calls `Application('OmniFocus').evaluateJavascript` with the prepared snippet, and awaits stdout
|
|
10
|
+
|
|
11
|
+
#### Scenario: Scripting-dictionary domain methods are forbidden
|
|
12
|
+
- **WHEN** a contributor adds a tool handler that calls e.g. `Application('OmniFocus').defaultDocument.projects` directly
|
|
13
|
+
- **THEN** a unit test (or review) flags the call as a runtime-contract violation and the change is rejected
|
|
14
|
+
|
|
15
|
+
### Requirement: Snippet argument injection via JSON literal
|
|
16
|
+
|
|
17
|
+
The system SHALL construct executable scripts by replacing exactly one `__ARGS__` placeholder in a static `.js` snippet template with the result of `JSON.stringify(args)`. No other interpolation of user-supplied data into script source is permitted anywhere in the codebase.
|
|
18
|
+
|
|
19
|
+
#### Scenario: Apostrophes and quotes survive injection
|
|
20
|
+
- **WHEN** a tool is invoked with `args = {name: "Finn's \"birthday\""}`
|
|
21
|
+
- **THEN** the executed snippet receives `args.name === "Finn's \"birthday\""` and no syntax error occurs
|
|
22
|
+
|
|
23
|
+
#### Scenario: Unicode and newlines survive injection
|
|
24
|
+
- **WHEN** a tool is invoked with `args = {note: "line1\nline2 — emoji 🎯"}`
|
|
25
|
+
- **THEN** the executed snippet receives the note verbatim including the newline and unicode code points
|
|
26
|
+
|
|
27
|
+
#### Scenario: Snippets are standalone and paste-ready
|
|
28
|
+
- **WHEN** a developer opens any file under `src/snippets/`
|
|
29
|
+
- **THEN** the file is a valid JavaScript program that can be pasted into OmniFocus's Automation Console after replacing `__ARGS__` with a hand-written object literal, with no additional preprocessing required
|
|
30
|
+
|
|
31
|
+
### Requirement: One-line JSON result protocol
|
|
32
|
+
|
|
33
|
+
The system SHALL represent every bridge invocation's result as exactly one line of JSON on the JXA host's stdout, matching the envelope `{ok: true, data: <value>} | {ok: false, error: {name: string, message: string, stack?: string}}`. The TypeScript runtime SHALL read stdout, extract the first JSON-parseable line, and return the parsed envelope to the caller. A result envelope with `ok: false` SHALL be thrown as a structured error with the error details preserved.
|
|
34
|
+
|
|
35
|
+
#### Scenario: Successful operation returns data envelope
|
|
36
|
+
- **WHEN** a snippet completes without throwing and its final expression is `JSON.stringify({ok: true, data: {id: "abc"}})`
|
|
37
|
+
- **THEN** the TS runtime receives `{ok: true, data: {id: "abc"}}` and returns `{id: "abc"}` to the tool handler
|
|
38
|
+
|
|
39
|
+
#### Scenario: Snippet exception produces error envelope
|
|
40
|
+
- **WHEN** a snippet throws an `Error` with message "not found"
|
|
41
|
+
- **THEN** the JXA shim catches the exception, prints `{ok: false, error: {name: "Error", message: "not found", ...}}` as one line on stdout, and the TS runtime throws a structured error carrying the name, message, and stack
|
|
42
|
+
|
|
43
|
+
#### Scenario: Extraneous stdout chatter does not corrupt results
|
|
44
|
+
- **WHEN** any process in the pipeline writes incidental non-JSON output to stdout before the result line
|
|
45
|
+
- **THEN** the TS runtime still parses the result envelope correctly by selecting the first complete JSON-parseable line
|
|
46
|
+
|
|
47
|
+
### Requirement: Snippet loader
|
|
48
|
+
|
|
49
|
+
The system SHALL load snippet templates from `src/snippets/<name>.js` relative to the compiled server's known snippet root, caching the file contents in memory after the first read. The loader SHALL reject snippets that do not contain exactly one `__ARGS__` token.
|
|
50
|
+
|
|
51
|
+
#### Scenario: Snippet is loaded and cached
|
|
52
|
+
- **WHEN** a tool handler invokes the runtime with snippet name `"list_projects"` twice in the same process
|
|
53
|
+
- **THEN** the file `list_projects.js` is read from disk exactly once and the cached content is used for the second invocation
|
|
54
|
+
|
|
55
|
+
#### Scenario: Snippet with zero placeholders is rejected
|
|
56
|
+
- **WHEN** a snippet file contains no `__ARGS__` token
|
|
57
|
+
- **THEN** the loader throws a contract violation at load time, not at runtime
|
|
58
|
+
|
|
59
|
+
#### Scenario: Snippet with multiple placeholders is rejected
|
|
60
|
+
- **WHEN** a snippet file contains two or more `__ARGS__` tokens
|
|
61
|
+
- **THEN** the loader throws a contract violation at load time
|
|
62
|
+
|
|
63
|
+
### Requirement: Script timeout
|
|
64
|
+
|
|
65
|
+
The system SHALL enforce a configurable per-invocation timeout on `osascript` execution, defaulting to 30 seconds, and SHALL terminate the child process and throw a timeout error when exceeded.
|
|
66
|
+
|
|
67
|
+
#### Scenario: Long-running snippet is terminated
|
|
68
|
+
- **WHEN** a snippet runs longer than the configured timeout
|
|
69
|
+
- **THEN** the runtime sends SIGTERM to the `osascript` process and throws a timeout error identifying the snippet name and elapsed time
|
package/openspec/changes/archive/2026-04-09-bootstrap-omnifocus-mcp/specs/folder-management/spec.md
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
## ADDED Requirements
|
|
2
|
+
|
|
3
|
+
### Requirement: List folders
|
|
4
|
+
|
|
5
|
+
The system SHALL provide a `list_folders` tool that returns every folder in the OmniFocus database as an array of summaries, each containing at minimum `{id, name, path, parentId, status}`. The `path` field SHALL use the canonical ` ▸ ` separator and SHALL equal the folder's name for top-level folders. The `parentId` field SHALL be `null` for top-level folders. The `status` field SHALL be one of `"active" | "dropped"`.
|
|
6
|
+
|
|
7
|
+
#### Scenario: All folders are returned with full paths
|
|
8
|
+
- **WHEN** `list_folders` is called with no arguments
|
|
9
|
+
- **THEN** the tool returns every folder in the database including nested folders, each with its full ancestor path
|
|
10
|
+
|
|
11
|
+
#### Scenario: Top-level folders have null parent
|
|
12
|
+
- **WHEN** a folder exists at the root of the folder hierarchy
|
|
13
|
+
- **THEN** its summary carries `parentId: null` and `path` equal to its name
|
|
14
|
+
|
|
15
|
+
### Requirement: Get folder by ID
|
|
16
|
+
|
|
17
|
+
The system SHALL provide a `get_folder` tool that accepts `{id: string}` and returns the full detail record of the named folder, including `{id, name, path, parentId, status, childFolderIds, projectIds}`. If no folder exists with that ID, the tool SHALL return a structured not-found error.
|
|
18
|
+
|
|
19
|
+
#### Scenario: Existing folder returns full detail with children
|
|
20
|
+
- **WHEN** `get_folder` is called with the ID of an existing folder
|
|
21
|
+
- **THEN** the tool returns the folder's full detail including the IDs of its immediate child folders and immediate child projects
|
|
22
|
+
|
|
23
|
+
#### Scenario: Missing folder returns not-found error
|
|
24
|
+
- **WHEN** `get_folder` is called with an ID that does not correspond to any folder
|
|
25
|
+
- **THEN** the tool returns a structured error with a not-found code
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
## ADDED Requirements
|
|
2
|
+
|
|
3
|
+
### Requirement: Capability declared
|
|
4
|
+
|
|
5
|
+
The `forecast` capability SHALL cover OmniFocus Forecast data, including forecast days, tasks due or deferred on a given day, the configured forecast tag, and forecast badge counts. Requirements for individual tools SHALL be added by the `forecast-and-inspection` change.
|
|
6
|
+
|
|
7
|
+
#### Scenario: Capability is named and scoped
|
|
8
|
+
- **WHEN** a future change proposes adding forecast tools
|
|
9
|
+
- **THEN** it lands requirements under this capability rather than inventing a new capability name
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
## ADDED Requirements
|
|
2
|
+
|
|
3
|
+
### Requirement: Canonical ID addressing
|
|
4
|
+
|
|
5
|
+
The system SHALL use `id.primaryKey` as the canonical identifier for every addressable OmniFocus entity (Task, Project, Folder, Tag, Perspective). Every read tool SHALL return an `id` field carrying this value. Every write tool (in this and future changes) SHALL accept an `id` parameter as its primary addressing mode.
|
|
6
|
+
|
|
7
|
+
#### Scenario: Read tools return primary key
|
|
8
|
+
- **WHEN** a caller invokes `list_projects`
|
|
9
|
+
- **THEN** every returned element has an `id` field equal to the OmniJS expression `project.id.primaryKey` for that project
|
|
10
|
+
|
|
11
|
+
#### Scenario: IDs are stable within a session
|
|
12
|
+
- **WHEN** a caller reads a project's ID via `list_projects`, then reads the same project again via `get_project` using that ID
|
|
13
|
+
- **THEN** the second read succeeds and returns the same project, without re-resolving by name
|
|
14
|
+
|
|
15
|
+
### Requirement: Name and path resolution with ambiguity reporting
|
|
16
|
+
|
|
17
|
+
The system SHALL provide a `resolve_name` tool that accepts `{type, query, scope?}` and returns a list of candidate entities `[{id, name, path, type, ...}]`. When more than one entity matches the query, the tool SHALL return all matches and SHALL NOT silently pick a winner. The caller is responsible for disambiguating.
|
|
18
|
+
|
|
19
|
+
#### Scenario: Unique match returns single candidate
|
|
20
|
+
- **WHEN** `resolve_name` is called with `{type: "project", query: "Q4 Planning"}` and exactly one project has that name
|
|
21
|
+
- **THEN** the tool returns a list with one element containing the project's id, name, and full folder path
|
|
22
|
+
|
|
23
|
+
#### Scenario: Ambiguous match returns all candidates
|
|
24
|
+
- **WHEN** `resolve_name` is called with `{type: "project", query: "Inbox Cleanup"}` and two projects have that name under different folders
|
|
25
|
+
- **THEN** the tool returns a list with both candidates, each carrying a distinct id and distinct folder path
|
|
26
|
+
|
|
27
|
+
#### Scenario: No match returns empty list
|
|
28
|
+
- **WHEN** `resolve_name` is called with a query that matches no entity of the requested type
|
|
29
|
+
- **THEN** the tool returns an empty list and does not throw an error
|
|
30
|
+
|
|
31
|
+
#### Scenario: Path-qualified query narrows scope
|
|
32
|
+
- **WHEN** `resolve_name` is called with `{type: "folder", query: "Acme", scope: "Work ▸ Clients"}`
|
|
33
|
+
- **THEN** the tool returns only folders named "Acme" that are direct or transitive children of the folder at path "Work ▸ Clients"
|
|
34
|
+
|
|
35
|
+
### Requirement: Folder path addressing
|
|
36
|
+
|
|
37
|
+
The system SHALL represent folder paths as strings using the separator ` ▸ ` (U+25B8 with surrounding spaces) and SHALL support resolving a folder by its full path as an alternative to its ID. When a path is ambiguous (the same path exists under multiple roots, which cannot occur for folders but can for tags), the resolution SHALL report the ambiguity through `resolve_name`.
|
|
38
|
+
|
|
39
|
+
#### Scenario: Folder resolves by full path
|
|
40
|
+
- **WHEN** `resolve_name` is called with `{type: "folder", query: "Work ▸ Clients ▸ Acme"}`
|
|
41
|
+
- **THEN** the tool returns the folder whose full ancestor chain matches that path exactly
|
|
42
|
+
|
|
43
|
+
#### Scenario: Returned paths use the canonical separator
|
|
44
|
+
- **WHEN** any read tool returns a folder path or a project's containing folder path
|
|
45
|
+
- **THEN** the path uses ` ▸ ` as the separator between ancestor names
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
## ADDED Requirements
|
|
2
|
+
|
|
3
|
+
### Requirement: Capability declared
|
|
4
|
+
|
|
5
|
+
The `perspective-management` capability SHALL cover listing built-in and custom perspectives, reading perspective metadata, and activating a perspective in a document window. Custom perspective *creation and editing* are a non-goal pending verification of OmniJS scriptability of `Perspective.Custom` definitions; if a future investigation proves the API is available, the non-goal may be revisited by a later change. Requirements for individual tools SHALL be added by the `perspectives-and-windows` change.
|
|
6
|
+
|
|
7
|
+
#### Scenario: Capability is named and scoped
|
|
8
|
+
- **WHEN** a future change proposes adding perspective tools
|
|
9
|
+
- **THEN** it lands requirements under this capability rather than inventing a new capability name
|
package/openspec/changes/archive/2026-04-09-bootstrap-omnifocus-mcp/specs/project-management/spec.md
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
## ADDED Requirements
|
|
2
|
+
|
|
3
|
+
### Requirement: List projects
|
|
4
|
+
|
|
5
|
+
The system SHALL provide a `list_projects` tool that returns every project in the OmniFocus database as an array of summaries, each containing at minimum `{id, name, folderPath, status, type}`. The `status` field SHALL be one of `"active" | "onHold" | "done" | "dropped"`. The `type` field SHALL be one of `"parallel" | "sequential" | "singleActions"`. The `folderPath` field SHALL use the canonical ` ▸ ` separator and SHALL be an empty string for projects at the top level with no containing folder.
|
|
6
|
+
|
|
7
|
+
#### Scenario: All projects are returned with canonical fields
|
|
8
|
+
- **WHEN** `list_projects` is called with no arguments
|
|
9
|
+
- **THEN** the tool returns every project in the database, each with id, name, folderPath, status, and type populated using the canonical enum values
|
|
10
|
+
|
|
11
|
+
#### Scenario: Single Actions list is reported with correct type
|
|
12
|
+
- **WHEN** the database contains a Single Actions list project
|
|
13
|
+
- **THEN** that project appears in the result with `type: "singleActions"`, never as `"parallel"` or `"sequential"`
|
|
14
|
+
|
|
15
|
+
### Requirement: Get project by ID
|
|
16
|
+
|
|
17
|
+
The system SHALL provide a `get_project` tool that accepts `{id: string}` and returns the full detail record of the named project, including `{id, name, note, folderPath, status, type, flagged, deferDate, dueDate, completionDate, reviewInterval, nextReviewDate, lastReviewDate, tagIds}`. If no project exists with that ID, the tool SHALL return a structured not-found error.
|
|
18
|
+
|
|
19
|
+
#### Scenario: Existing project returns full detail
|
|
20
|
+
- **WHEN** `get_project` is called with the ID of an existing project
|
|
21
|
+
- **THEN** the tool returns the project's full detail record
|
|
22
|
+
|
|
23
|
+
#### Scenario: Missing project returns not-found error
|
|
24
|
+
- **WHEN** `get_project` is called with an ID that does not correspond to any project
|
|
25
|
+
- **THEN** the tool returns a structured error with a not-found code
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
## ADDED Requirements
|
|
2
|
+
|
|
3
|
+
### Requirement: Capability declared
|
|
4
|
+
|
|
5
|
+
The `recurrence` capability SHALL cover the construction, assignment, reading, and clearing of `Task.RepetitionRule` values on OmniFocus tasks, including the `RepetitionMethod` (`Fixed`, `DueDate`, `Start`, `None`) and the underlying ICS RRULE string. Tools under this capability SHALL expose both a structured schema for common recurrence patterns (frequency, interval, weekday set) and a raw `rrule` escape hatch for patterns the structured schema does not express. Requirements for individual tools SHALL be added by the `recurrence` change. This change only declares the capability and its scope.
|
|
6
|
+
|
|
7
|
+
#### Scenario: Capability is named and scoped
|
|
8
|
+
- **WHEN** a future change proposes adding recurrence-related tools
|
|
9
|
+
- **THEN** it lands requirements under this capability rather than inventing a new capability name
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
## ADDED Requirements
|
|
2
|
+
|
|
3
|
+
### Requirement: Capability declared
|
|
4
|
+
|
|
5
|
+
The `settings` capability SHALL cover reading OmniFocus application settings and preferences where the OmniJS API exposes them. Write access SHALL be limited to the subset of settings that OmniJS documents as writable; the specific writable keys SHALL be enumerated when the `settings` change is drafted. Requirements for individual tools SHALL be added by that change.
|
|
6
|
+
|
|
7
|
+
#### Scenario: Capability is named and scoped
|
|
8
|
+
- **WHEN** a future change proposes adding settings tools
|
|
9
|
+
- **THEN** it lands requirements under this capability, with an explicit enumeration of writable keys vs read-only keys
|