@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,115 @@
|
|
|
1
|
+
# task-write
|
|
2
|
+
|
|
3
|
+
## Purpose
|
|
4
|
+
|
|
5
|
+
TBD — Defines tools for creating and mutating OmniFocus tasks, including create, edit, complete, drop, and delete operations.
|
|
6
|
+
|
|
7
|
+
## Requirements
|
|
8
|
+
|
|
9
|
+
### Requirement: Create task
|
|
10
|
+
|
|
11
|
+
The system SHALL provide a `create_task` tool that creates a new OmniFocus task and returns its full detail record. The tool SHALL accept `{name: string, note?: string, flagged?: boolean, deferDate?: string, plannedDate?: string, dueDate?: string, estimatedMinutes?: number, projectId?: string, parentTaskId?: string, tagIds?: string[], repetitionRule?: RepetitionRuleInput}`. Placement SHALL be determined as follows: if both `projectId` and `parentTaskId` are provided the tool SHALL return an error; if only `projectId` is provided the task is placed at the project root; if only `parentTaskId` is provided the task is created as a subtask inheriting its parent's project; if neither is provided the task is placed in the inbox. When `repetitionRule` is provided, the task SHALL have the specified recurrence set at creation.
|
|
12
|
+
|
|
13
|
+
#### Scenario: Create inbox task
|
|
14
|
+
- **WHEN** `create_task` is called with `{name: "Buy milk"}` and no `projectId` or `parentTaskId`
|
|
15
|
+
- **THEN** the tool creates the task in the OmniFocus inbox and returns its full detail record including a stable `id`
|
|
16
|
+
|
|
17
|
+
#### Scenario: Create task in a project
|
|
18
|
+
- **WHEN** `create_task` is called with `{name: "Write tests", projectId: "abc123"}`
|
|
19
|
+
- **THEN** the tool creates the task at the root of the specified project and returns its full detail record
|
|
20
|
+
|
|
21
|
+
#### Scenario: Create subtask
|
|
22
|
+
- **WHEN** `create_task` is called with `{name: "Review PR", parentTaskId: "xyz789"}`
|
|
23
|
+
- **THEN** the tool creates the task as a child of the specified parent task and returns its full detail record
|
|
24
|
+
|
|
25
|
+
#### Scenario: Ambiguous placement is rejected
|
|
26
|
+
- **WHEN** `create_task` is called with both `projectId` and `parentTaskId`
|
|
27
|
+
- **THEN** the tool returns a validation error before any snippet executes
|
|
28
|
+
|
|
29
|
+
#### Scenario: Non-existent project returns not-found error
|
|
30
|
+
- **WHEN** `create_task` is called with a `projectId` that does not correspond to any project
|
|
31
|
+
- **THEN** the tool returns a structured not-found error
|
|
32
|
+
|
|
33
|
+
#### Scenario: Create task with repetition rule
|
|
34
|
+
- **WHEN** `create_task` is called with `{ name: "Weekly review", repetitionRule: { frequency: "weekly", interval: 1, method: "start" } }`
|
|
35
|
+
- **THEN** the task is created with the specified recurrence and the returned TaskDetail includes the parsed repetitionRule
|
|
36
|
+
|
|
37
|
+
#### Scenario: Create task with planned date
|
|
38
|
+
- **WHEN** `create_task` is called with `{ name: "Clean kitchen", plannedDate: "2026-04-15T09:00:00Z" }`
|
|
39
|
+
- **THEN** the task is created with the specified planned date and the returned TaskDetail includes `plannedDate`
|
|
40
|
+
|
|
41
|
+
### Requirement: Edit task
|
|
42
|
+
|
|
43
|
+
The system SHALL provide an `edit_task` tool that modifies an existing task and returns its updated full detail record. The tool SHALL accept `{id: string}` plus any subset of `{name?: string, note?: string, flagged?: boolean, deferDate?: string | null, plannedDate?: string | null, dueDate?: string | null, estimatedMinutes?: number | null, tagIds?: string[], repetitionRule?: RepetitionRuleInput | null}`. Fields omitted from the call SHALL be left unchanged. When `tagIds` is provided it SHALL replace the task's entire tag set; when omitted, tags SHALL be unchanged. Passing `null` for a date or `estimatedMinutes` SHALL clear the field. Passing `repetitionRule: null` SHALL clear the task's recurrence; passing a `RepetitionRuleInput` object SHALL set or replace the recurrence; omitting `repetitionRule` SHALL leave the existing recurrence unchanged.
|
|
44
|
+
|
|
45
|
+
#### Scenario: Edit a single field
|
|
46
|
+
- **WHEN** `edit_task` is called with `{id: "abc123", flagged: true}`
|
|
47
|
+
- **THEN** only the `flagged` field is changed; all other fields retain their previous values
|
|
48
|
+
|
|
49
|
+
#### Scenario: Replace tag set
|
|
50
|
+
- **WHEN** `edit_task` is called with `{id: "abc123", tagIds: ["t1", "t2"]}`
|
|
51
|
+
- **THEN** the task's tags are set to exactly `["t1", "t2"]`, replacing any previously assigned tags
|
|
52
|
+
|
|
53
|
+
#### Scenario: Clear a date field
|
|
54
|
+
- **WHEN** `edit_task` is called with `{id: "abc123", dueDate: null}`
|
|
55
|
+
- **THEN** the task's due date is cleared
|
|
56
|
+
|
|
57
|
+
#### Scenario: Clear planned date
|
|
58
|
+
- **WHEN** `edit_task` is called with `{id: "abc123", plannedDate: null}`
|
|
59
|
+
- **THEN** the task's planned date is cleared
|
|
60
|
+
|
|
61
|
+
#### Scenario: Non-existent task returns not-found error
|
|
62
|
+
- **WHEN** `edit_task` is called with an ID that does not correspond to any task
|
|
63
|
+
- **THEN** the tool returns a structured not-found error
|
|
64
|
+
|
|
65
|
+
#### Scenario: Non-existent tag ID returns not-found error
|
|
66
|
+
- **WHEN** `edit_task` is called with a `tagIds` array containing an ID that does not correspond to any tag
|
|
67
|
+
- **THEN** the tool returns a structured not-found error and the task is not modified
|
|
68
|
+
|
|
69
|
+
#### Scenario: Set repetition via edit
|
|
70
|
+
- **WHEN** `edit_task` is called with `{ id: "t1", repetitionRule: { frequency: "monthly", interval: 1, method: "dueDate" } }`
|
|
71
|
+
- **THEN** the task's recurrence is set and all other fields are unchanged
|
|
72
|
+
|
|
73
|
+
#### Scenario: Clear repetition via edit
|
|
74
|
+
- **WHEN** `edit_task` is called with `{ id: "t1", repetitionRule: null }`
|
|
75
|
+
- **THEN** the task's recurrence is cleared and all other fields are unchanged
|
|
76
|
+
|
|
77
|
+
### Requirement: Complete task
|
|
78
|
+
|
|
79
|
+
The system SHALL provide a `complete_task` tool that marks an existing task complete using OmniJS `markComplete()` and returns the task's updated full detail record.
|
|
80
|
+
|
|
81
|
+
#### Scenario: Complete an existing task
|
|
82
|
+
- **WHEN** `complete_task` is called with the ID of an available task
|
|
83
|
+
- **THEN** the task's status becomes `"complete"` and the tool returns the updated detail record
|
|
84
|
+
|
|
85
|
+
#### Scenario: Non-existent task returns not-found error
|
|
86
|
+
- **WHEN** `complete_task` is called with an ID that does not correspond to any task
|
|
87
|
+
- **THEN** the tool returns a structured not-found error
|
|
88
|
+
|
|
89
|
+
### Requirement: Drop task
|
|
90
|
+
|
|
91
|
+
The system SHALL provide a `drop_task` tool that marks an existing task dropped using OmniJS `drop()` and returns the task's updated full detail record.
|
|
92
|
+
|
|
93
|
+
#### Scenario: Drop an existing task
|
|
94
|
+
- **WHEN** `drop_task` is called with the ID of an available task
|
|
95
|
+
- **THEN** the task's status becomes `"dropped"` and the tool returns the updated detail record
|
|
96
|
+
|
|
97
|
+
#### Scenario: Non-existent task returns not-found error
|
|
98
|
+
- **WHEN** `drop_task` is called with an ID that does not correspond to any task
|
|
99
|
+
- **THEN** the tool returns a structured not-found error
|
|
100
|
+
|
|
101
|
+
### Requirement: Delete task
|
|
102
|
+
|
|
103
|
+
The system SHALL provide a `delete_task` tool that permanently deletes a task and all its subtasks using OmniJS `deleteObject()`. The tool description SHALL instruct the AI to confirm with the user before invoking this tool, noting that deletion is permanent and includes all subtasks.
|
|
104
|
+
|
|
105
|
+
#### Scenario: Delete an existing task
|
|
106
|
+
- **WHEN** `delete_task` is called with the ID of an existing task
|
|
107
|
+
- **THEN** the task and all its subtasks are permanently removed from OmniFocus and the tool returns a confirmation envelope
|
|
108
|
+
|
|
109
|
+
#### Scenario: Delete task with subtasks removes all children
|
|
110
|
+
- **WHEN** `delete_task` is called with the ID of a task that has subtasks
|
|
111
|
+
- **THEN** the task and all descendant subtasks are deleted
|
|
112
|
+
|
|
113
|
+
#### Scenario: Non-existent task returns not-found error
|
|
114
|
+
- **WHEN** `delete_task` is called with an ID that does not correspond to any task
|
|
115
|
+
- **THEN** the tool returns a structured not-found error
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# url-automation
|
|
2
|
+
|
|
3
|
+
## Purpose
|
|
4
|
+
|
|
5
|
+
Covers `omnifocus://` URL construction and parsing. Individual tools will be defined in the `url-automation` change.
|
|
6
|
+
|
|
7
|
+
## Requirements
|
|
8
|
+
|
|
9
|
+
### Requirement: Capability declared
|
|
10
|
+
|
|
11
|
+
The `url-automation` capability SHALL cover `omnifocus://` URL construction (including task-paste format for bulk creation, add-to-inbox URLs, and deep links to entities by ID) and parsing of incoming `omnifocus://` URLs into structured form. Requirements for individual tools SHALL be added by the `url-automation` change.
|
|
12
|
+
|
|
13
|
+
#### Scenario: Capability is named and scoped
|
|
14
|
+
- **WHEN** a future change proposes adding URL-automation tools
|
|
15
|
+
- **THEN** it lands requirements under this capability rather than inventing a new capability name
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# window-state
|
|
2
|
+
|
|
3
|
+
## Purpose
|
|
4
|
+
|
|
5
|
+
Covers read-only inspection of OmniFocus document window state. Window mutation is an explicit non-goal. Individual tools will be defined in the `perspectives-and-windows` change.
|
|
6
|
+
|
|
7
|
+
## Requirements
|
|
8
|
+
|
|
9
|
+
### Requirement: Capability declared
|
|
10
|
+
|
|
11
|
+
The `window-state` capability SHALL cover read-only inspection of OmniFocus document window state, including the currently active window, active perspective, sidebar selection, and content selection. Window *mutation* (resize, close, focus, open) is an explicit non-goal. Requirements for individual tools SHALL be added by the `perspectives-and-windows` change.
|
|
12
|
+
|
|
13
|
+
#### Scenario: Capability is named and read-only scope is fixed
|
|
14
|
+
- **WHEN** a future change proposes adding window-state tools
|
|
15
|
+
- **THEN** it lands read-only requirements under this capability; any mutation proposal requires first revisiting the non-goal in design
|
package/package.json
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@scardis/omnifocus-mcp",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "MCP server for OmniFocus via Omni Automation JavaScript API",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"mcpName": "io.github.steveardis/omnifocus",
|
|
7
|
+
"type": "module",
|
|
8
|
+
"engines": {
|
|
9
|
+
"node": ">=20"
|
|
10
|
+
},
|
|
11
|
+
"bin": {
|
|
12
|
+
"omnifocus-mcp": "dist/server.js"
|
|
13
|
+
},
|
|
14
|
+
"scripts": {
|
|
15
|
+
"build": "tsc",
|
|
16
|
+
"typecheck": "tsc --noEmit",
|
|
17
|
+
"start": "tsx src/server.ts",
|
|
18
|
+
"test": "vitest run --config vitest.config.ts",
|
|
19
|
+
"test:integration": "node -e \"if(process.platform!=='darwin'){console.error('Integration tests require macOS');process.exit(1)}\" && vitest run --config vitest.integration.config.ts",
|
|
20
|
+
"test:cleanup-fixtures": "tsx scripts/cleanup-fixtures.ts"
|
|
21
|
+
},
|
|
22
|
+
"dependencies": {
|
|
23
|
+
"@modelcontextprotocol/sdk": "^1.0.0",
|
|
24
|
+
"zod": "^3.23.8"
|
|
25
|
+
},
|
|
26
|
+
"devDependencies": {
|
|
27
|
+
"@types/node": "^22.0.0",
|
|
28
|
+
"tsx": "^4.19.0",
|
|
29
|
+
"typescript": "^5.6.0",
|
|
30
|
+
"vitest": "^2.1.0"
|
|
31
|
+
}
|
|
32
|
+
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cleanup script: removes any stale __MCP_TEST_*__ folders, __mcp_*__ projects,
|
|
3
|
+
* __MCP_*__ tags, and __mcp_*__ inbox tasks from OmniFocus.
|
|
4
|
+
* Run via: npm run test:cleanup-fixtures
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { spawnSync } from "child_process";
|
|
8
|
+
|
|
9
|
+
const snippet = `(() => {
|
|
10
|
+
function deleteFolder(f) {
|
|
11
|
+
f.flattenedProjects.forEach(function(p) { deleteObject(p); });
|
|
12
|
+
f.folders.forEach(function(child) { deleteFolder(child); });
|
|
13
|
+
deleteObject(f);
|
|
14
|
+
}
|
|
15
|
+
var removed = [];
|
|
16
|
+
|
|
17
|
+
// Stale test folders
|
|
18
|
+
flattenedFolders.filter(function(f) {
|
|
19
|
+
return f.name.startsWith('__MCP_TEST_') && f.name.endsWith('__');
|
|
20
|
+
}).forEach(function(f) { removed.push("folder: " + f.name); deleteFolder(f); });
|
|
21
|
+
|
|
22
|
+
// Stale test projects (orphaned at top level by move_project tests)
|
|
23
|
+
flattenedProjects.filter(function(p) {
|
|
24
|
+
return p.name.startsWith('__mcp_') && p.name.endsWith('__');
|
|
25
|
+
}).forEach(function(p) { removed.push("project: " + p.name); deleteObject(p); });
|
|
26
|
+
|
|
27
|
+
// Stale test tags
|
|
28
|
+
flattenedTags.filter(function(t) {
|
|
29
|
+
return (t.name.startsWith('__mcp_') || t.name.startsWith('__MCP_')) && t.name.endsWith('__');
|
|
30
|
+
}).forEach(function(t) { removed.push("tag: " + t.name); deleteObject(t); });
|
|
31
|
+
|
|
32
|
+
// Stale test inbox tasks
|
|
33
|
+
inbox.filter(function(t) {
|
|
34
|
+
return t.name.startsWith('__mcp_') && t.name.endsWith('__');
|
|
35
|
+
}).forEach(function(t) { removed.push("task: " + t.name); deleteObject(t); });
|
|
36
|
+
|
|
37
|
+
return JSON.stringify({ok:true,data:{removed:removed,count:removed.length}});
|
|
38
|
+
})()`;
|
|
39
|
+
|
|
40
|
+
const script = `
|
|
41
|
+
(function() {
|
|
42
|
+
var app = Application('OmniFocus');
|
|
43
|
+
app.includeStandardAdditions = true;
|
|
44
|
+
try {
|
|
45
|
+
var r = app.evaluateJavascript(${JSON.stringify(snippet)});
|
|
46
|
+
$.NSFileHandle.fileHandleWithStandardOutput.writeData(
|
|
47
|
+
$.NSString.alloc.initWithString(r+'\\n').dataUsingEncoding($.NSUTF8StringEncoding)
|
|
48
|
+
);
|
|
49
|
+
} catch(e) {
|
|
50
|
+
$.NSFileHandle.fileHandleWithStandardOutput.writeData(
|
|
51
|
+
$.NSString.alloc.initWithString(JSON.stringify({ok:false,error:{message:e.message}})+'\\n').dataUsingEncoding($.NSUTF8StringEncoding)
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
})();
|
|
55
|
+
`;
|
|
56
|
+
|
|
57
|
+
const result = spawnSync("osascript", ["-l", "JavaScript"], {
|
|
58
|
+
input: script,
|
|
59
|
+
encoding: "utf-8",
|
|
60
|
+
timeout: 15_000,
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
const stdout = result.stdout || "";
|
|
64
|
+
const line = stdout.split("\n").find((l) => l.trim().startsWith("{"));
|
|
65
|
+
if (!line) {
|
|
66
|
+
console.error("No output from OmniFocus:", stdout);
|
|
67
|
+
process.exit(1);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const parsed = JSON.parse(line) as {
|
|
71
|
+
ok: boolean;
|
|
72
|
+
data?: { removed: string[]; count: number };
|
|
73
|
+
error?: { message: string };
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
if (!parsed.ok) {
|
|
77
|
+
console.error("Cleanup failed:", parsed.error?.message);
|
|
78
|
+
process.exit(1);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const { removed, count } = parsed.data!;
|
|
82
|
+
if (count === 0) {
|
|
83
|
+
console.log("No stale test fixtures found.");
|
|
84
|
+
} else {
|
|
85
|
+
console.log(`Removed ${count} stale test fixture(s):`);
|
|
86
|
+
for (const item of removed) {
|
|
87
|
+
console.log(` - ${item}`);
|
|
88
|
+
}
|
|
89
|
+
}
|
package/server.json
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://static.modelcontextprotocol.io/schemas/2025-12-11/server.schema.json",
|
|
3
|
+
"name": "io.github.steveardis/omnifocus",
|
|
4
|
+
"title": "OmniFocus",
|
|
5
|
+
"description": "MCP server for OmniFocus (macOS task manager) via Omni Automation JavaScript API. Full CRUD on tasks, projects, folders, and tags, plus repetition rules, planned dates, filtering, and move operations.",
|
|
6
|
+
"repository": {
|
|
7
|
+
"url": "https://github.com/steveardis/omnifocus-mcp",
|
|
8
|
+
"source": "github"
|
|
9
|
+
},
|
|
10
|
+
"version": "0.1.0",
|
|
11
|
+
"packages": [
|
|
12
|
+
{
|
|
13
|
+
"registryType": "npm",
|
|
14
|
+
"identifier": "@scardis/omnifocus-mcp",
|
|
15
|
+
"version": "0.1.0",
|
|
16
|
+
"transport": {
|
|
17
|
+
"type": "stdio"
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
]
|
|
21
|
+
}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { spawn } from "child_process";
|
|
2
|
+
import { loadSnippet } from "./snippetLoader.js";
|
|
3
|
+
import { buildJxaScript } from "./jxaShim.js";
|
|
4
|
+
import { parseResultLine, ExecutionError } from "./resultProtocol.js";
|
|
5
|
+
|
|
6
|
+
const DEFAULT_TIMEOUT_MS = 30_000;
|
|
7
|
+
|
|
8
|
+
export interface RunOptions {
|
|
9
|
+
timeoutMs?: number;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Execute an OmniJS snippet inside OmniFocus via the JXA bridge.
|
|
14
|
+
*
|
|
15
|
+
* @param name - Snippet filename (without .js extension) under src/snippets/
|
|
16
|
+
* @param args - Arguments to inject. Embedded as a JSON literal; safe for
|
|
17
|
+
* all unicode, apostrophes, quotes, etc. (Design Decision 2).
|
|
18
|
+
* @param opts - Optional timeout override (default 30s)
|
|
19
|
+
* @returns The `data` field from the success envelope
|
|
20
|
+
* @throws ExecutionError if the snippet throws inside OmniJS
|
|
21
|
+
* @throws Error on timeout, process errors, or protocol violations
|
|
22
|
+
*/
|
|
23
|
+
export async function runSnippet(
|
|
24
|
+
name: string,
|
|
25
|
+
args: unknown,
|
|
26
|
+
opts: RunOptions = {}
|
|
27
|
+
): Promise<unknown> {
|
|
28
|
+
const timeoutMs = opts.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
29
|
+
|
|
30
|
+
// Load and inject args — the only interpolation allowed (Decision 2)
|
|
31
|
+
const template = loadSnippet(name);
|
|
32
|
+
const snippet = template.replace("__ARGS__", JSON.stringify(args));
|
|
33
|
+
|
|
34
|
+
// Wrap snippet in JXA shim
|
|
35
|
+
const jxaScript = buildJxaScript(snippet);
|
|
36
|
+
|
|
37
|
+
return new Promise((resolve, reject) => {
|
|
38
|
+
const MAX_OUTPUT_BYTES = 10 * 1024 * 1024; // 10 MB
|
|
39
|
+
|
|
40
|
+
const ac = new AbortController();
|
|
41
|
+
const timer = setTimeout(() => {
|
|
42
|
+
ac.abort();
|
|
43
|
+
child.kill("SIGKILL");
|
|
44
|
+
reject(new Error(`Snippet "${name}" timed out after ${timeoutMs}ms`));
|
|
45
|
+
}, timeoutMs);
|
|
46
|
+
|
|
47
|
+
const child = spawn("osascript", ["-l", "JavaScript"], {
|
|
48
|
+
signal: ac.signal,
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
let stdout = "";
|
|
52
|
+
let stderr = "";
|
|
53
|
+
|
|
54
|
+
child.stdout.on("data", (chunk: Buffer) => {
|
|
55
|
+
stdout += chunk.toString();
|
|
56
|
+
if (stdout.length > MAX_OUTPUT_BYTES) {
|
|
57
|
+
child.kill("SIGKILL");
|
|
58
|
+
clearTimeout(timer);
|
|
59
|
+
reject(new Error(`Snippet "${name}" exceeded maximum output size`));
|
|
60
|
+
}
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
child.stderr.on("data", (chunk: Buffer) => {
|
|
64
|
+
stderr += chunk.toString();
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
child.on("error", (err) => {
|
|
68
|
+
clearTimeout(timer);
|
|
69
|
+
if ((err as NodeJS.ErrnoException).code === "ABORT_ERR") return; // handled by timer
|
|
70
|
+
reject(err);
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
child.on("close", (code) => {
|
|
74
|
+
clearTimeout(timer);
|
|
75
|
+
try {
|
|
76
|
+
const envelope = parseResultLine(stdout);
|
|
77
|
+
if (envelope.ok) {
|
|
78
|
+
resolve(envelope.data);
|
|
79
|
+
} else {
|
|
80
|
+
reject(new ExecutionError(envelope.error));
|
|
81
|
+
}
|
|
82
|
+
} catch (parseErr) {
|
|
83
|
+
const hint = stdout.slice(0, 200) || stderr.slice(0, 200);
|
|
84
|
+
reject(
|
|
85
|
+
new Error(
|
|
86
|
+
`Failed to parse bridge output for snippet "${name}" (exit ${code})` +
|
|
87
|
+
(hint ? `: ${hint}` : "")
|
|
88
|
+
)
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
// Write JXA script to stdin
|
|
94
|
+
child.stdin.write(jxaScript);
|
|
95
|
+
child.stdin.end();
|
|
96
|
+
});
|
|
97
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* JXA wrapper template. The `__SNIPPET__` placeholder is replaced with the
|
|
3
|
+
* OmniJS snippet to execute. The resulting script is passed to
|
|
4
|
+
* `osascript -l JavaScript` via stdin.
|
|
5
|
+
*
|
|
6
|
+
* This shim:
|
|
7
|
+
* - Calls Application('OmniFocus').evaluateJavascript() with the snippet
|
|
8
|
+
* - Wraps in try/catch
|
|
9
|
+
* - Prints exactly one line of JSON: {ok, data} | {ok: false, error: {...}}
|
|
10
|
+
*
|
|
11
|
+
* The JXA scripting dictionary is used ONLY for evaluateJavascript.
|
|
12
|
+
* No other scripting-dictionary domain methods are invoked here.
|
|
13
|
+
*/
|
|
14
|
+
export const JXA_SHIM_TEMPLATE = `
|
|
15
|
+
(function() {
|
|
16
|
+
var snippet = __SNIPPET__;
|
|
17
|
+
var app = Application('OmniFocus');
|
|
18
|
+
app.includeStandardAdditions = true;
|
|
19
|
+
try {
|
|
20
|
+
var result = app.evaluateJavascript(snippet);
|
|
21
|
+
// result is already a JSON string produced by the snippet
|
|
22
|
+
$.NSFileHandle.fileHandleWithStandardOutput.writeData(
|
|
23
|
+
$.NSString.alloc.initWithString(result + '\\n')
|
|
24
|
+
.dataUsingEncoding($.NSUTF8StringEncoding)
|
|
25
|
+
);
|
|
26
|
+
} catch(e) {
|
|
27
|
+
var errName = e.name || 'Error';
|
|
28
|
+
var errMsg = e.message || String(e);
|
|
29
|
+
// OmniJS wraps thrown errors; the original name is embedded in the message
|
|
30
|
+
// as "SomeName: original message". Recover it when the name is generic.
|
|
31
|
+
var nameMatch = errMsg.match(/^([A-Z]\\w*Error): ([\\s\\S]*)$/);
|
|
32
|
+
if (nameMatch) { errName = nameMatch[1]; errMsg = nameMatch[2]; }
|
|
33
|
+
var err = JSON.stringify({
|
|
34
|
+
ok: false,
|
|
35
|
+
error: {
|
|
36
|
+
name: errName,
|
|
37
|
+
message: errMsg
|
|
38
|
+
}
|
|
39
|
+
});
|
|
40
|
+
$.NSFileHandle.fileHandleWithStandardOutput.writeData(
|
|
41
|
+
$.NSString.alloc.initWithString(err + '\\n')
|
|
42
|
+
.dataUsingEncoding($.NSUTF8StringEncoding)
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
})();
|
|
46
|
+
`;
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Build the complete JXA script to pass to osascript.
|
|
50
|
+
* The snippet is embedded as a JS string literal via JSON.stringify,
|
|
51
|
+
* so it is safe to contain any characters including quotes, backslashes, etc.
|
|
52
|
+
*/
|
|
53
|
+
export function buildJxaScript(snippet: string): string {
|
|
54
|
+
return JXA_SHIM_TEMPLATE.replace("__SNIPPET__", JSON.stringify(snippet));
|
|
55
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
|
|
3
|
+
const ErrorDetail = z.object({
|
|
4
|
+
name: z.string(),
|
|
5
|
+
message: z.string(),
|
|
6
|
+
stack: z.string().optional(),
|
|
7
|
+
});
|
|
8
|
+
|
|
9
|
+
const SuccessEnvelope = z.object({
|
|
10
|
+
ok: z.literal(true),
|
|
11
|
+
data: z.unknown(),
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
const ErrorEnvelope = z.object({
|
|
15
|
+
ok: z.literal(false),
|
|
16
|
+
error: ErrorDetail,
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
const Envelope = z.discriminatedUnion("ok", [SuccessEnvelope, ErrorEnvelope]);
|
|
20
|
+
|
|
21
|
+
export type ResultEnvelope = z.infer<typeof Envelope>;
|
|
22
|
+
|
|
23
|
+
export class ExecutionError extends Error {
|
|
24
|
+
readonly errorName: string;
|
|
25
|
+
readonly remoteStack?: string;
|
|
26
|
+
|
|
27
|
+
constructor(detail: z.infer<typeof ErrorDetail>) {
|
|
28
|
+
super(detail.message);
|
|
29
|
+
this.name = "ExecutionError";
|
|
30
|
+
this.errorName = detail.name;
|
|
31
|
+
this.remoteStack = detail.stack;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Scans stdout for the first JSON-parseable line and validates it as a result
|
|
37
|
+
* envelope. Ignores any preceding non-JSON chatter (e.g. osascript warnings).
|
|
38
|
+
*/
|
|
39
|
+
export function parseResultLine(stdout: string): ResultEnvelope {
|
|
40
|
+
const lines = stdout.split("\n");
|
|
41
|
+
for (const line of lines) {
|
|
42
|
+
const trimmed = line.trim();
|
|
43
|
+
if (!trimmed) continue;
|
|
44
|
+
let parsed: unknown;
|
|
45
|
+
try {
|
|
46
|
+
parsed = JSON.parse(trimmed);
|
|
47
|
+
} catch {
|
|
48
|
+
continue; // not JSON, skip
|
|
49
|
+
}
|
|
50
|
+
const result = Envelope.safeParse(parsed);
|
|
51
|
+
if (result.success) {
|
|
52
|
+
return result.data;
|
|
53
|
+
}
|
|
54
|
+
// Parsed as JSON but not a valid envelope — this is a protocol error
|
|
55
|
+
throw new Error(
|
|
56
|
+
`Bridge returned JSON that is not a valid result envelope: ${trimmed}`
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
throw new Error(
|
|
60
|
+
`No valid JSON result envelope found in osascript output:\n${stdout}`
|
|
61
|
+
);
|
|
62
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { readFileSync } from "fs";
|
|
2
|
+
import { join, dirname } from "path";
|
|
3
|
+
import { fileURLToPath } from "url";
|
|
4
|
+
|
|
5
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
6
|
+
const SNIPPETS_DIR = join(__dirname, "..", "snippets");
|
|
7
|
+
|
|
8
|
+
// Allowlist of valid snippet names. Adding a new snippet requires an explicit
|
|
9
|
+
// entry here, which prevents path-traversal attacks if a snippet name ever
|
|
10
|
+
// reaches this loader from a dynamic source.
|
|
11
|
+
const ALLOWED_SNIPPETS = new Set([
|
|
12
|
+
"get_folder",
|
|
13
|
+
"get_project",
|
|
14
|
+
"get_tag",
|
|
15
|
+
"get_task",
|
|
16
|
+
"list_folders",
|
|
17
|
+
"list_projects",
|
|
18
|
+
"list_tags",
|
|
19
|
+
"list_tasks",
|
|
20
|
+
"resolve_name",
|
|
21
|
+
"create_task",
|
|
22
|
+
"edit_task",
|
|
23
|
+
"complete_task",
|
|
24
|
+
"drop_task",
|
|
25
|
+
"delete_task",
|
|
26
|
+
"create_project",
|
|
27
|
+
"edit_project",
|
|
28
|
+
"complete_project",
|
|
29
|
+
"drop_project",
|
|
30
|
+
"delete_project",
|
|
31
|
+
"create_folder",
|
|
32
|
+
"edit_folder",
|
|
33
|
+
"delete_folder",
|
|
34
|
+
"create_tag",
|
|
35
|
+
"edit_tag",
|
|
36
|
+
"delete_tag",
|
|
37
|
+
"move_task",
|
|
38
|
+
"move_project",
|
|
39
|
+
]);
|
|
40
|
+
|
|
41
|
+
const cache = new Map<string, string>();
|
|
42
|
+
|
|
43
|
+
export function loadSnippet(name: string): string {
|
|
44
|
+
if (!ALLOWED_SNIPPETS.has(name)) {
|
|
45
|
+
throw new Error(`Unknown snippet: "${name}"`);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
if (cache.has(name)) {
|
|
49
|
+
return cache.get(name)!;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const filePath = join(SNIPPETS_DIR, `${name}.js`);
|
|
53
|
+
let content: string;
|
|
54
|
+
try {
|
|
55
|
+
content = readFileSync(filePath, "utf-8");
|
|
56
|
+
} catch {
|
|
57
|
+
throw new Error(`Snippet "${name}" could not be loaded`);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const matches = content.split("__ARGS__").length - 1;
|
|
61
|
+
if (matches === 0) {
|
|
62
|
+
throw new Error(
|
|
63
|
+
`Snippet "${name}" contains no __ARGS__ placeholder. Every snippet must have exactly one.`
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
if (matches > 1) {
|
|
67
|
+
throw new Error(
|
|
68
|
+
`Snippet "${name}" contains ${matches} __ARGS__ placeholders. Exactly one is required.`
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
cache.set(name, content);
|
|
73
|
+
return content;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** For testing: clear the in-memory cache. */
|
|
77
|
+
export function clearSnippetCache(): void {
|
|
78
|
+
cache.clear();
|
|
79
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
|
|
3
|
+
export const IdSchema = z.string().min(1, "ID must be a non-empty string");
|
|
4
|
+
|
|
5
|
+
export const EntityType = z.enum([
|
|
6
|
+
"task",
|
|
7
|
+
"project",
|
|
8
|
+
"folder",
|
|
9
|
+
"tag",
|
|
10
|
+
"perspective",
|
|
11
|
+
]);
|
|
12
|
+
|
|
13
|
+
export const ProjectType = z.enum(["parallel", "sequential", "singleActions"]);
|
|
14
|
+
|
|
15
|
+
export const ProjectStatus = z.enum(["active", "onHold", "done", "dropped"]);
|
|
16
|
+
|
|
17
|
+
export const TaskStatus = z.enum([
|
|
18
|
+
"available",
|
|
19
|
+
"incomplete",
|
|
20
|
+
"completedByChildren",
|
|
21
|
+
"complete",
|
|
22
|
+
"dropped",
|
|
23
|
+
"dueSoon",
|
|
24
|
+
"overdue",
|
|
25
|
+
"flagged",
|
|
26
|
+
"blocked",
|
|
27
|
+
"next",
|
|
28
|
+
]);
|
|
29
|
+
|
|
30
|
+
export const TagStatus = z.enum(["active", "onHold", "dropped"]);
|
|
31
|
+
|
|
32
|
+
export const FolderStatus = z.enum(["active", "dropped"]);
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
export {
|
|
2
|
+
IdSchema,
|
|
3
|
+
EntityType,
|
|
4
|
+
ProjectType,
|
|
5
|
+
ProjectStatus,
|
|
6
|
+
TaskStatus,
|
|
7
|
+
TagStatus,
|
|
8
|
+
FolderStatus,
|
|
9
|
+
} from "./enums.js";
|
|
10
|
+
|
|
11
|
+
export {
|
|
12
|
+
RepetitionRuleInput,
|
|
13
|
+
RepetitionRuleDetail,
|
|
14
|
+
TaskSummary,
|
|
15
|
+
ListTasksFilter,
|
|
16
|
+
TaskDetail,
|
|
17
|
+
CreateTaskInput,
|
|
18
|
+
EditTaskInput,
|
|
19
|
+
MoveTaskInput,
|
|
20
|
+
ReviewIntervalInput,
|
|
21
|
+
CreateProjectInput,
|
|
22
|
+
EditProjectInput,
|
|
23
|
+
MoveProjectInput,
|
|
24
|
+
ListProjectsFilter,
|
|
25
|
+
ProjectSummary,
|
|
26
|
+
ProjectDetail,
|
|
27
|
+
ListFoldersFilter,
|
|
28
|
+
CreateFolderInput,
|
|
29
|
+
EditFolderInput,
|
|
30
|
+
FolderSummary,
|
|
31
|
+
FolderDetail,
|
|
32
|
+
ListTagsFilter,
|
|
33
|
+
CreateTagInput,
|
|
34
|
+
EditTagInput,
|
|
35
|
+
TagSummary,
|
|
36
|
+
TagDetail,
|
|
37
|
+
ResolveCandidate,
|
|
38
|
+
} from "./shapes.js";
|