@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,141 @@
|
|
|
1
|
+
import { describe, it, expect, beforeAll, afterAll } from "vitest";
|
|
2
|
+
import { runSnippet } from "../../src/runtime/bridge.js";
|
|
3
|
+
import {
|
|
4
|
+
createTestFolder,
|
|
5
|
+
cleanupTestFolder,
|
|
6
|
+
createTestProject,
|
|
7
|
+
createTestTag,
|
|
8
|
+
deleteTestTag,
|
|
9
|
+
type TestFixture,
|
|
10
|
+
} from "./fixtures.js";
|
|
11
|
+
import { TaskSummary } from "../../src/schemas/index.js";
|
|
12
|
+
import { z } from "zod";
|
|
13
|
+
|
|
14
|
+
const TaskSummaryArray = z.array(TaskSummary);
|
|
15
|
+
|
|
16
|
+
describe("list_tasks filtering (integration)", () => {
|
|
17
|
+
let fixture: TestFixture;
|
|
18
|
+
let projectId: string;
|
|
19
|
+
let tagId: string;
|
|
20
|
+
|
|
21
|
+
beforeAll(async () => {
|
|
22
|
+
fixture = await createTestFolder();
|
|
23
|
+
projectId = await createTestProject(fixture.folderId, "FilterTest Project");
|
|
24
|
+
tagId = await createTestTag(`__mcp_filter_tag_${Date.now()}__`);
|
|
25
|
+
|
|
26
|
+
// Create tasks with various properties inside the project
|
|
27
|
+
// flagged task
|
|
28
|
+
await runSnippet("create_task", {
|
|
29
|
+
name: "Flagged Task",
|
|
30
|
+
projectId,
|
|
31
|
+
flagged: true,
|
|
32
|
+
});
|
|
33
|
+
// task with tag and due date in the past
|
|
34
|
+
await runSnippet("create_task", {
|
|
35
|
+
name: "Tagged Past Due Task",
|
|
36
|
+
projectId,
|
|
37
|
+
tagIds: [tagId],
|
|
38
|
+
dueDate: "2020-01-01T00:00:00.000Z",
|
|
39
|
+
});
|
|
40
|
+
// task with due date far in future
|
|
41
|
+
await runSnippet("create_task", {
|
|
42
|
+
name: "Future Due Task",
|
|
43
|
+
projectId,
|
|
44
|
+
dueDate: "2099-12-31T23:59:59.000Z",
|
|
45
|
+
});
|
|
46
|
+
// plain task (no flags, no tags, no due date)
|
|
47
|
+
await runSnippet("create_task", { name: "Plain Task", projectId });
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
afterAll(async () => {
|
|
51
|
+
await deleteTestTag(tagId);
|
|
52
|
+
await cleanupTestFolder(fixture.folderId);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it("returns dueDate and tagIds on each task summary", async () => {
|
|
56
|
+
const raw = await runSnippet("list_tasks", { scope: { projectId } });
|
|
57
|
+
const tasks = TaskSummaryArray.parse(raw);
|
|
58
|
+
expect(tasks.length).toBeGreaterThan(0);
|
|
59
|
+
// Every task must have these fields (added to TaskSummary)
|
|
60
|
+
for (const t of tasks) {
|
|
61
|
+
expect(t).toHaveProperty("dueDate");
|
|
62
|
+
expect(t).toHaveProperty("tagIds");
|
|
63
|
+
expect(Array.isArray(t.tagIds)).toBe(true);
|
|
64
|
+
}
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it("enriched summary includes correct dueDate and tagIds", async () => {
|
|
68
|
+
const raw = await runSnippet("list_tasks", { scope: { projectId } });
|
|
69
|
+
const tasks = TaskSummaryArray.parse(raw);
|
|
70
|
+
const tagged = tasks.find((t) => t.name === "Tagged Past Due Task");
|
|
71
|
+
expect(tagged).toBeDefined();
|
|
72
|
+
expect(tagged!.dueDate).toBe("2020-01-01T00:00:00.000Z");
|
|
73
|
+
expect(tagged!.tagIds).toContain(tagId);
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it("default excludes complete and dropped tasks", async () => {
|
|
77
|
+
// Complete one task then check it's absent by default
|
|
78
|
+
const raw1 = await runSnippet("list_tasks", { scope: { projectId } });
|
|
79
|
+
const tasks1 = TaskSummaryArray.parse(raw1);
|
|
80
|
+
const plain = tasks1.find((t) => t.name === "Plain Task");
|
|
81
|
+
expect(plain).toBeDefined();
|
|
82
|
+
|
|
83
|
+
await runSnippet("complete_task", { id: plain!.id });
|
|
84
|
+
|
|
85
|
+
const raw2 = await runSnippet("list_tasks", { scope: { projectId } });
|
|
86
|
+
const tasks2 = TaskSummaryArray.parse(raw2);
|
|
87
|
+
expect(tasks2.find((t) => t.id === plain!.id)).toBeUndefined();
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it("explicit status filter retrieves complete tasks", async () => {
|
|
91
|
+
const raw = await runSnippet("list_tasks", {
|
|
92
|
+
scope: { projectId },
|
|
93
|
+
filter: { status: ["complete"] },
|
|
94
|
+
});
|
|
95
|
+
const tasks = TaskSummaryArray.parse(raw);
|
|
96
|
+
expect(tasks.some((t) => t.name === "Plain Task")).toBe(true);
|
|
97
|
+
expect(tasks.every((t) => t.status === "complete")).toBe(true);
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it("flagged filter returns only flagged tasks", async () => {
|
|
101
|
+
const raw = await runSnippet("list_tasks", {
|
|
102
|
+
scope: { projectId },
|
|
103
|
+
filter: { flagged: true },
|
|
104
|
+
});
|
|
105
|
+
const tasks = TaskSummaryArray.parse(raw);
|
|
106
|
+
expect(tasks.length).toBeGreaterThan(0);
|
|
107
|
+
expect(tasks.every((t) => t.flagged)).toBe(true);
|
|
108
|
+
expect(tasks.some((t) => t.name === "Flagged Task")).toBe(true);
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
it("tagId filter returns only tasks with that tag", async () => {
|
|
112
|
+
const raw = await runSnippet("list_tasks", {
|
|
113
|
+
scope: { projectId },
|
|
114
|
+
filter: { tagId },
|
|
115
|
+
});
|
|
116
|
+
const tasks = TaskSummaryArray.parse(raw);
|
|
117
|
+
expect(tasks.length).toBeGreaterThan(0);
|
|
118
|
+
expect(tasks.every((t) => t.tagIds.includes(tagId))).toBe(true);
|
|
119
|
+
expect(tasks.some((t) => t.name === "Tagged Past Due Task")).toBe(true);
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
it("dueBeforeDate filter returns tasks due on or before the date", async () => {
|
|
123
|
+
// Cutoff in 2021 — should match the 2020 task, not the 2099 task
|
|
124
|
+
const raw = await runSnippet("list_tasks", {
|
|
125
|
+
scope: { projectId },
|
|
126
|
+
filter: { dueBeforeDate: "2021-01-01T00:00:00.000Z" },
|
|
127
|
+
});
|
|
128
|
+
const tasks = TaskSummaryArray.parse(raw);
|
|
129
|
+
expect(tasks.some((t) => t.name === "Tagged Past Due Task")).toBe(true);
|
|
130
|
+
expect(tasks.some((t) => t.name === "Future Due Task")).toBe(false);
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
it("limit caps the number of returned tasks", async () => {
|
|
134
|
+
const raw = await runSnippet("list_tasks", {
|
|
135
|
+
scope: { projectId },
|
|
136
|
+
limit: 1,
|
|
137
|
+
});
|
|
138
|
+
const tasks = TaskSummaryArray.parse(raw);
|
|
139
|
+
expect(tasks.length).toBeLessThanOrEqual(1);
|
|
140
|
+
});
|
|
141
|
+
});
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { describe, it, expect, beforeAll, afterAll } from "vitest";
|
|
2
|
+
import { runSnippet } from "../../src/runtime/bridge.js";
|
|
3
|
+
import { createTestFolder, cleanupTestFolder, createTestProject, type TestFixture } from "./fixtures.js";
|
|
4
|
+
import { ProjectSummary } from "../../src/schemas/index.js";
|
|
5
|
+
|
|
6
|
+
describe("move_project (integration)", () => {
|
|
7
|
+
let fixtureA: TestFixture;
|
|
8
|
+
let fixtureB: TestFixture;
|
|
9
|
+
let projectId: string;
|
|
10
|
+
|
|
11
|
+
beforeAll(async () => {
|
|
12
|
+
fixtureA = await createTestFolder();
|
|
13
|
+
fixtureB = await createTestFolder();
|
|
14
|
+
projectId = await createTestProject(fixtureA.folderId, "__mcp_move_project__");
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
afterAll(async () => {
|
|
18
|
+
// Delete the project explicitly — it may have been moved to top level (outside any fixture folder)
|
|
19
|
+
try {
|
|
20
|
+
await runSnippet("delete_project", { id: projectId });
|
|
21
|
+
} catch (_) { /* already gone */ }
|
|
22
|
+
await cleanupTestFolder(fixtureA.folderId);
|
|
23
|
+
await cleanupTestFolder(fixtureB.folderId);
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it("moves project to a different folder", async () => {
|
|
27
|
+
const raw = await runSnippet("move_project", { id: projectId, folderId: fixtureB.folderId });
|
|
28
|
+
const result = ProjectSummary.parse(raw);
|
|
29
|
+
expect(result.id).toBe(projectId);
|
|
30
|
+
expect(result.folderId).toBe(fixtureB.folderId);
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it("moves project to top level (folderId null)", async () => {
|
|
34
|
+
const raw = await runSnippet("move_project", { id: projectId, folderId: null });
|
|
35
|
+
const result = ProjectSummary.parse(raw);
|
|
36
|
+
expect(result.id).toBe(projectId);
|
|
37
|
+
expect(result.folderId).toBeNull();
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it("non-existent project ID returns NotFoundError", async () => {
|
|
41
|
+
await expect(
|
|
42
|
+
runSnippet("move_project", { id: "nonexistent-project-xyz", folderId: fixtureA.folderId })
|
|
43
|
+
).rejects.toSatisfy((e: unknown) => {
|
|
44
|
+
const err = e as Record<string, unknown>;
|
|
45
|
+
return err.name === "ExecutionError" && err.errorName === "NotFoundError";
|
|
46
|
+
});
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it("non-existent folder ID returns NotFoundError", async () => {
|
|
50
|
+
await expect(
|
|
51
|
+
runSnippet("move_project", { id: projectId, folderId: "nonexistent-folder-xyz" })
|
|
52
|
+
).rejects.toSatisfy((e: unknown) => {
|
|
53
|
+
const err = e as Record<string, unknown>;
|
|
54
|
+
return err.name === "ExecutionError" && err.errorName === "NotFoundError";
|
|
55
|
+
});
|
|
56
|
+
});
|
|
57
|
+
});
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { describe, it, expect, beforeAll, afterAll } from "vitest";
|
|
2
|
+
import { runSnippet } from "../../src/runtime/bridge.js";
|
|
3
|
+
import { createTestFolder, cleanupTestFolder, createTestProject, type TestFixture } from "./fixtures.js";
|
|
4
|
+
import { TaskSummary } from "../../src/schemas/index.js";
|
|
5
|
+
|
|
6
|
+
describe("move_task (integration)", () => {
|
|
7
|
+
let fixture: TestFixture;
|
|
8
|
+
let projectAId: string;
|
|
9
|
+
let projectBId: string;
|
|
10
|
+
let taskId: string;
|
|
11
|
+
let parentTaskId: string;
|
|
12
|
+
|
|
13
|
+
beforeAll(async () => {
|
|
14
|
+
fixture = await createTestFolder();
|
|
15
|
+
projectAId = await createTestProject(fixture.folderId, "__mcp_move_proj_a__");
|
|
16
|
+
projectBId = await createTestProject(fixture.folderId, "__mcp_move_proj_b__");
|
|
17
|
+
|
|
18
|
+
// Create a task in project A
|
|
19
|
+
const taskRaw = await runSnippet("create_task", {
|
|
20
|
+
name: "__mcp_move_task__",
|
|
21
|
+
projectId: projectAId,
|
|
22
|
+
});
|
|
23
|
+
taskId = (taskRaw as { id: string }).id;
|
|
24
|
+
|
|
25
|
+
// Create a parent task in project B
|
|
26
|
+
const parentRaw = await runSnippet("create_task", {
|
|
27
|
+
name: "__mcp_parent_task__",
|
|
28
|
+
projectId: projectBId,
|
|
29
|
+
});
|
|
30
|
+
parentTaskId = (parentRaw as { id: string }).id;
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
afterAll(async () => {
|
|
34
|
+
await cleanupTestFolder(fixture.folderId);
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it("moves task to a different project", async () => {
|
|
38
|
+
const raw = await runSnippet("move_task", { id: taskId, projectId: projectBId });
|
|
39
|
+
const result = TaskSummary.parse(raw);
|
|
40
|
+
expect(result.id).toBe(taskId);
|
|
41
|
+
expect(result.containerId).toBe(projectBId);
|
|
42
|
+
expect(result.containerType).toBe("project");
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it("makes task a subtask of another task", async () => {
|
|
46
|
+
const raw = await runSnippet("move_task", { id: taskId, parentTaskId });
|
|
47
|
+
const result = TaskSummary.parse(raw);
|
|
48
|
+
expect(result.id).toBe(taskId);
|
|
49
|
+
expect(result.containerId).toBe(parentTaskId);
|
|
50
|
+
expect(result.containerType).toBe("task");
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it("non-existent task ID returns NotFoundError", async () => {
|
|
54
|
+
await expect(
|
|
55
|
+
runSnippet("move_task", { id: "nonexistent-task-xyz", projectId: projectAId })
|
|
56
|
+
).rejects.toSatisfy((e: unknown) => {
|
|
57
|
+
const err = e as Record<string, unknown>;
|
|
58
|
+
return err.name === "ExecutionError" && err.errorName === "NotFoundError";
|
|
59
|
+
});
|
|
60
|
+
});
|
|
61
|
+
});
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { describe, it, expect, beforeAll, afterAll } from "vitest";
|
|
2
|
+
import { runSnippet } from "../../src/runtime/bridge.js";
|
|
3
|
+
import { createTestFolder, cleanupTestFolder, createTestProject, type TestFixture } from "./fixtures.js";
|
|
4
|
+
import { TaskDetail } from "../../src/schemas/index.js";
|
|
5
|
+
|
|
6
|
+
describe("plannedDate (integration)", () => {
|
|
7
|
+
let fixture: TestFixture;
|
|
8
|
+
let projectId: string;
|
|
9
|
+
|
|
10
|
+
beforeAll(async () => {
|
|
11
|
+
fixture = await createTestFolder();
|
|
12
|
+
projectId = await createTestProject(fixture.folderId, "__mcp_planned_date_test__");
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
afterAll(async () => {
|
|
16
|
+
await cleanupTestFolder(fixture.folderId);
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
it("creates task with plannedDate and get_task returns it", async () => {
|
|
20
|
+
const raw = await runSnippet("create_task", {
|
|
21
|
+
name: "__mcp_planned_create__",
|
|
22
|
+
projectId,
|
|
23
|
+
plannedDate: "2026-04-15T09:00:00.000Z",
|
|
24
|
+
});
|
|
25
|
+
const task = TaskDetail.parse(raw);
|
|
26
|
+
expect(task.plannedDate).not.toBeNull();
|
|
27
|
+
expect(task.plannedDate).toContain("2026-04-15");
|
|
28
|
+
|
|
29
|
+
const fetched = TaskDetail.parse(await runSnippet("get_task", { id: task.id }));
|
|
30
|
+
expect(fetched.plannedDate).toContain("2026-04-15");
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it("edits task to set plannedDate", async () => {
|
|
34
|
+
const created = TaskDetail.parse(
|
|
35
|
+
await runSnippet("create_task", { name: "__mcp_planned_edit__", projectId })
|
|
36
|
+
);
|
|
37
|
+
expect(created.plannedDate).toBeNull();
|
|
38
|
+
|
|
39
|
+
const edited = TaskDetail.parse(
|
|
40
|
+
await runSnippet("edit_task", { id: created.id, plannedDate: "2026-05-01T10:00:00.000Z" })
|
|
41
|
+
);
|
|
42
|
+
expect(edited.plannedDate).toContain("2026-05-01");
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it("clears plannedDate via edit with null", async () => {
|
|
46
|
+
const created = TaskDetail.parse(
|
|
47
|
+
await runSnippet("create_task", {
|
|
48
|
+
name: "__mcp_planned_clear__",
|
|
49
|
+
projectId,
|
|
50
|
+
plannedDate: "2026-04-20T08:00:00.000Z",
|
|
51
|
+
})
|
|
52
|
+
);
|
|
53
|
+
expect(created.plannedDate).not.toBeNull();
|
|
54
|
+
|
|
55
|
+
const cleared = TaskDetail.parse(
|
|
56
|
+
await runSnippet("edit_task", { id: created.id, plannedDate: null })
|
|
57
|
+
);
|
|
58
|
+
expect(cleared.plannedDate).toBeNull();
|
|
59
|
+
|
|
60
|
+
const fetched = TaskDetail.parse(await runSnippet("get_task", { id: created.id }));
|
|
61
|
+
expect(fetched.plannedDate).toBeNull();
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it("task without plannedDate returns null (backward compat)", async () => {
|
|
65
|
+
const raw = await runSnippet("create_task", {
|
|
66
|
+
name: "__mcp_planned_none__",
|
|
67
|
+
projectId,
|
|
68
|
+
});
|
|
69
|
+
const task = TaskDetail.parse(raw);
|
|
70
|
+
expect(task.plannedDate).toBeNull();
|
|
71
|
+
});
|
|
72
|
+
});
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { runSnippet } from "../../src/runtime/index.js";
|
|
2
|
+
|
|
3
|
+
const SYNC_CHECK_SNIPPET_NAME = "check_sync_enabled";
|
|
4
|
+
|
|
5
|
+
// Inline snippet: no src/snippets file needed for this one-off preflight check.
|
|
6
|
+
// We run it via the bridge directly using a temporary approach.
|
|
7
|
+
async function isSyncEnabled(): Promise<boolean> {
|
|
8
|
+
try {
|
|
9
|
+
// OmniJS: check if the database has sync settings configured
|
|
10
|
+
const { spawnSync } = await import("child_process");
|
|
11
|
+
const script = `
|
|
12
|
+
(function() {
|
|
13
|
+
try {
|
|
14
|
+
var app = Application('OmniFocus');
|
|
15
|
+
app.includeStandardAdditions = true;
|
|
16
|
+
var snippet = "(function() { try { var db = document; var sync = db.willSynchronize !== undefined ? db.willSynchronize : false; return JSON.stringify({ok:true,data:{syncEnabled:sync}}); } catch(e) { return JSON.stringify({ok:true,data:{syncEnabled:false}}); } })()";
|
|
17
|
+
var result = app.evaluateJavascript(snippet);
|
|
18
|
+
$.NSFileHandle.fileHandleWithStandardOutput.writeData(
|
|
19
|
+
$.NSString.alloc.initWithString(result + '\\n').dataUsingEncoding($.NSUTF8StringEncoding)
|
|
20
|
+
);
|
|
21
|
+
} catch(e) {
|
|
22
|
+
$.NSFileHandle.fileHandleWithStandardOutput.writeData(
|
|
23
|
+
$.NSString.alloc.initWithString(JSON.stringify({ok:true,data:{syncEnabled:false}}) + '\\n').dataUsingEncoding($.NSUTF8StringEncoding)
|
|
24
|
+
);
|
|
25
|
+
}
|
|
26
|
+
})();
|
|
27
|
+
`;
|
|
28
|
+
const result = spawnSync("osascript", ["-l", "JavaScript"], {
|
|
29
|
+
input: script,
|
|
30
|
+
encoding: "utf-8",
|
|
31
|
+
timeout: 10_000,
|
|
32
|
+
});
|
|
33
|
+
if (result.stdout) {
|
|
34
|
+
const parsed = JSON.parse(result.stdout.trim()) as {
|
|
35
|
+
ok: boolean;
|
|
36
|
+
data: { syncEnabled: boolean };
|
|
37
|
+
};
|
|
38
|
+
if (parsed.ok) return parsed.data.syncEnabled;
|
|
39
|
+
}
|
|
40
|
+
return false;
|
|
41
|
+
} catch {
|
|
42
|
+
return false;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export async function setup(): Promise<void> {
|
|
47
|
+
if (process.platform !== "darwin") {
|
|
48
|
+
throw new Error(
|
|
49
|
+
"Integration tests require macOS (OmniFocus + osascript unavailable)"
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const syncEnabled = await isSyncEnabled();
|
|
54
|
+
if (syncEnabled && !process.env["MCP_TEST_ALLOW_SYNC"]) {
|
|
55
|
+
throw new Error(
|
|
56
|
+
"OmniFocus sync is enabled. Integration tests would propagate fixture folders to other devices.\n" +
|
|
57
|
+
"Set MCP_TEST_ALLOW_SYNC=1 to run anyway (fixtures will sync), or disable sync first."
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { describe, it, expect, beforeAll, afterAll } from "vitest";
|
|
2
|
+
import { runSnippet } from "../../src/runtime/bridge.js";
|
|
3
|
+
import { createTestFolder, cleanupTestFolder, type TestFixture } from "./fixtures.js";
|
|
4
|
+
import { z } from "zod";
|
|
5
|
+
import { ResolveCandidate } from "../../src/schemas/index.js";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Integration test: resolve_name
|
|
9
|
+
*
|
|
10
|
+
* Creates two projects with identical names under different folders.
|
|
11
|
+
* Asserts resolve_name returns both candidates with distinct paths,
|
|
12
|
+
* never silently picking one.
|
|
13
|
+
*/
|
|
14
|
+
describe("resolve_name (integration)", () => {
|
|
15
|
+
let fixture1: TestFixture;
|
|
16
|
+
let fixture2: TestFixture;
|
|
17
|
+
const DUPE_NAME = `DuplicateProject_${Date.now()}`;
|
|
18
|
+
|
|
19
|
+
beforeAll(async () => {
|
|
20
|
+
fixture1 = await createTestFolder();
|
|
21
|
+
fixture2 = await createTestFolder();
|
|
22
|
+
|
|
23
|
+
const { spawnSync } = await import("child_process");
|
|
24
|
+
|
|
25
|
+
for (const folderId of [fixture1.folderId, fixture2.folderId]) {
|
|
26
|
+
const createSnippet = `(() => {
|
|
27
|
+
var folder = flattenedFolders.find(function(f){ return f.id.primaryKey === ${JSON.stringify(folderId)}; });
|
|
28
|
+
if (!folder) throw new Error("Fixture folder not found: " + ${JSON.stringify(folderId)});
|
|
29
|
+
var p = new Project(${JSON.stringify(DUPE_NAME)}, folder);
|
|
30
|
+
return JSON.stringify({ok:true,data:{id:p.id.primaryKey}});
|
|
31
|
+
})()`;
|
|
32
|
+
const script = `
|
|
33
|
+
(function() {
|
|
34
|
+
var app = Application('OmniFocus');
|
|
35
|
+
app.includeStandardAdditions = true;
|
|
36
|
+
try {
|
|
37
|
+
var r = app.evaluateJavascript(${JSON.stringify(createSnippet)});
|
|
38
|
+
$.NSFileHandle.fileHandleWithStandardOutput.writeData($.NSString.alloc.initWithString(r+'\\n').dataUsingEncoding($.NSUTF8StringEncoding));
|
|
39
|
+
} catch(e) {
|
|
40
|
+
$.NSFileHandle.fileHandleWithStandardOutput.writeData($.NSString.alloc.initWithString(JSON.stringify({ok:false,error:{name:e.name,message:e.message}})+'\\n').dataUsingEncoding($.NSUTF8StringEncoding));
|
|
41
|
+
}
|
|
42
|
+
})();
|
|
43
|
+
`;
|
|
44
|
+
const result = spawnSync("osascript", ["-l", "JavaScript"], {
|
|
45
|
+
input: script, encoding: "utf-8", timeout: 15_000,
|
|
46
|
+
});
|
|
47
|
+
const line = (result.stdout || "").split("\n").find((l) => l.trim().startsWith("{"));
|
|
48
|
+
if (!line) throw new Error(`Could not create fixture project in ${folderId}`);
|
|
49
|
+
const parsed = JSON.parse(line) as { ok: boolean; error?: { message: string } };
|
|
50
|
+
if (!parsed.ok) throw new Error(`Create project failed: ${parsed.error?.message}`);
|
|
51
|
+
}
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
afterAll(async () => {
|
|
55
|
+
await Promise.all([
|
|
56
|
+
cleanupTestFolder(fixture1.folderId),
|
|
57
|
+
cleanupTestFolder(fixture2.folderId),
|
|
58
|
+
]);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it("returns both candidates when duplicate project names exist under different folders", async () => {
|
|
62
|
+
const raw = await runSnippet("resolve_name", {
|
|
63
|
+
type: "project",
|
|
64
|
+
query: DUPE_NAME,
|
|
65
|
+
scope: null,
|
|
66
|
+
});
|
|
67
|
+
const candidates = z.array(ResolveCandidate).parse(raw);
|
|
68
|
+
const mine = candidates.filter((c) => c.name === DUPE_NAME);
|
|
69
|
+
expect(mine.length).toBeGreaterThanOrEqual(2);
|
|
70
|
+
|
|
71
|
+
// Each candidate must have a distinct path
|
|
72
|
+
const paths = mine.map((c) => c.path);
|
|
73
|
+
const unique = new Set(paths);
|
|
74
|
+
expect(unique.size).toBe(mine.length);
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it("returns empty array for a name that matches nothing", async () => {
|
|
78
|
+
const raw = await runSnippet("resolve_name", {
|
|
79
|
+
type: "project",
|
|
80
|
+
query: `__nonexistent_${Date.now()}__`,
|
|
81
|
+
scope: null,
|
|
82
|
+
});
|
|
83
|
+
const candidates = z.array(ResolveCandidate).parse(raw);
|
|
84
|
+
expect(candidates).toHaveLength(0);
|
|
85
|
+
});
|
|
86
|
+
});
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import { describe, it, expect, beforeAll, afterAll } from "vitest";
|
|
2
|
+
import { runSnippet } from "../../src/runtime/bridge.js";
|
|
3
|
+
import { createTestFolder, cleanupTestFolder, createTestProject, type TestFixture } from "./fixtures.js";
|
|
4
|
+
import { TaskDetail } from "../../src/schemas/index.js";
|
|
5
|
+
|
|
6
|
+
describe("task recurrence (integration)", () => {
|
|
7
|
+
let fixture: TestFixture;
|
|
8
|
+
let projectId: string;
|
|
9
|
+
|
|
10
|
+
beforeAll(async () => {
|
|
11
|
+
fixture = await createTestFolder();
|
|
12
|
+
projectId = await createTestProject(fixture.folderId, "__mcp_recurrence_test__");
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
afterAll(async () => {
|
|
16
|
+
await cleanupTestFolder(fixture.folderId);
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
it("creates task with daily repetition and get_task returns it", async () => {
|
|
20
|
+
const raw = await runSnippet("create_task", {
|
|
21
|
+
name: "__mcp_daily_task__",
|
|
22
|
+
projectId,
|
|
23
|
+
repetitionRule: { frequency: "daily", interval: 1, method: "fixed" },
|
|
24
|
+
});
|
|
25
|
+
const task = TaskDetail.parse(raw);
|
|
26
|
+
expect(task.repetitionRule).not.toBeNull();
|
|
27
|
+
expect(task.repetitionRule?.frequency).toBe("daily");
|
|
28
|
+
expect(task.repetitionRule?.interval).toBe(1);
|
|
29
|
+
expect(task.repetitionRule?.method).toBe("fixed");
|
|
30
|
+
|
|
31
|
+
const getraw = await runSnippet("get_task", { id: task.id });
|
|
32
|
+
const fetched = TaskDetail.parse(getraw);
|
|
33
|
+
expect(fetched.repetitionRule?.frequency).toBe("daily");
|
|
34
|
+
expect(fetched.repetitionRule?.method).toBe("fixed");
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it("creates task with weekly repetition on Mon/Wed/Fri", async () => {
|
|
38
|
+
const raw = await runSnippet("create_task", {
|
|
39
|
+
name: "__mcp_weekly_task__",
|
|
40
|
+
projectId,
|
|
41
|
+
repetitionRule: {
|
|
42
|
+
frequency: "weekly",
|
|
43
|
+
interval: 1,
|
|
44
|
+
daysOfWeek: ["monday", "wednesday", "friday"],
|
|
45
|
+
method: "start",
|
|
46
|
+
},
|
|
47
|
+
});
|
|
48
|
+
const task = TaskDetail.parse(raw);
|
|
49
|
+
expect(task.repetitionRule?.frequency).toBe("weekly");
|
|
50
|
+
expect(task.repetitionRule?.daysOfWeek).toEqual(
|
|
51
|
+
expect.arrayContaining(["monday", "wednesday", "friday"])
|
|
52
|
+
);
|
|
53
|
+
expect(task.repetitionRule?.daysOfWeek).toHaveLength(3);
|
|
54
|
+
expect(task.repetitionRule?.method).toBe("start");
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it("edits existing task to add monthly repetition", async () => {
|
|
58
|
+
const created = await runSnippet("create_task", {
|
|
59
|
+
name: "__mcp_edit_recur_task__",
|
|
60
|
+
projectId,
|
|
61
|
+
});
|
|
62
|
+
const task = TaskDetail.parse(created);
|
|
63
|
+
expect(task.repetitionRule).toBeNull();
|
|
64
|
+
|
|
65
|
+
const edited = await runSnippet("edit_task", {
|
|
66
|
+
id: task.id,
|
|
67
|
+
repetitionRule: { frequency: "monthly", interval: 1, method: "dueDate" },
|
|
68
|
+
});
|
|
69
|
+
const updated = TaskDetail.parse(edited);
|
|
70
|
+
expect(updated.repetitionRule?.frequency).toBe("monthly");
|
|
71
|
+
expect(updated.repetitionRule?.interval).toBe(1);
|
|
72
|
+
expect(updated.repetitionRule?.method).toBe("dueDate");
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it("clears repetition via edit with null", async () => {
|
|
76
|
+
const created = await runSnippet("create_task", {
|
|
77
|
+
name: "__mcp_clear_recur_task__",
|
|
78
|
+
projectId,
|
|
79
|
+
repetitionRule: { frequency: "daily", interval: 1, method: "fixed" },
|
|
80
|
+
});
|
|
81
|
+
const task = TaskDetail.parse(created);
|
|
82
|
+
expect(task.repetitionRule).not.toBeNull();
|
|
83
|
+
|
|
84
|
+
const cleared = await runSnippet("edit_task", {
|
|
85
|
+
id: task.id,
|
|
86
|
+
repetitionRule: null,
|
|
87
|
+
});
|
|
88
|
+
const updated = TaskDetail.parse(cleared);
|
|
89
|
+
expect(updated.repetitionRule).toBeNull();
|
|
90
|
+
|
|
91
|
+
const fetched = TaskDetail.parse(await runSnippet("get_task", { id: task.id }));
|
|
92
|
+
expect(fetched.repetitionRule).toBeNull();
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it("task created without repetitionRule returns repetitionRule: null", async () => {
|
|
96
|
+
const raw = await runSnippet("create_task", {
|
|
97
|
+
name: "__mcp_no_recur_task__",
|
|
98
|
+
projectId,
|
|
99
|
+
});
|
|
100
|
+
const task = TaskDetail.parse(raw);
|
|
101
|
+
expect(task.repetitionRule).toBeNull();
|
|
102
|
+
|
|
103
|
+
const fetched = TaskDetail.parse(await runSnippet("get_task", { id: task.id }));
|
|
104
|
+
expect(fetched.repetitionRule).toBeNull();
|
|
105
|
+
});
|
|
106
|
+
});
|
|
File without changes
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Proves Design Decision 2:
|
|
5
|
+
* JSON.stringify(args) produces a valid JS expression for any args value,
|
|
6
|
+
* including apostrophes, quotes, backslashes, newlines, and unicode.
|
|
7
|
+
*
|
|
8
|
+
* We test the injection logic directly without hitting the bridge or OmniFocus.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
const TRICKY_ARGS = {
|
|
12
|
+
name: "Finn's \"birthday\" 🎯",
|
|
13
|
+
note: "line1\nline2\ttabbed",
|
|
14
|
+
path: "Work â–¸ Clients â–¸ Acme",
|
|
15
|
+
backslash: "C:\\Users\\test",
|
|
16
|
+
unicode: "\u2603 snowman \u0000 null",
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
function injectArgs(template: string, args: unknown): string {
|
|
20
|
+
return template.replace("__ARGS__", JSON.stringify(args));
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
describe("argument injection safety", () => {
|
|
24
|
+
it("produces valid JavaScript for tricky string values", () => {
|
|
25
|
+
const template = "(() => { const args = __ARGS__; return args; })()";
|
|
26
|
+
const injected = injectArgs(template, TRICKY_ARGS);
|
|
27
|
+
// new Function wraps the body in a function; we eval the IIFE directly
|
|
28
|
+
let result: unknown;
|
|
29
|
+
expect(() => {
|
|
30
|
+
result = new Function(`return ${injected}`)();
|
|
31
|
+
}).not.toThrow();
|
|
32
|
+
expect(result).toEqual(TRICKY_ARGS);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it("recovers apostrophes exactly", () => {
|
|
36
|
+
const template = "(() => { const args = __ARGS__; return args; })()";
|
|
37
|
+
const args = { name: "it's a test's value" };
|
|
38
|
+
const injected = injectArgs(template, args);
|
|
39
|
+
const result = new Function(`return ${injected}`)() as typeof args;
|
|
40
|
+
expect(result.name).toBe("it's a test's value");
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it("recovers double quotes exactly", () => {
|
|
44
|
+
const template = "(() => { const args = __ARGS__; return args; })()";
|
|
45
|
+
const args = { name: 'She said "hello"' };
|
|
46
|
+
const injected = injectArgs(template, args);
|
|
47
|
+
const result = new Function(`return ${injected}`)() as typeof args;
|
|
48
|
+
expect(result.name).toBe('She said "hello"');
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it("recovers newlines exactly", () => {
|
|
52
|
+
const template = "(() => { const args = __ARGS__; return args; })()";
|
|
53
|
+
const args = { note: "line1\nline2\nline3" };
|
|
54
|
+
const injected = injectArgs(template, args);
|
|
55
|
+
const result = new Function(`return ${injected}`)() as typeof args;
|
|
56
|
+
expect(result.note).toBe("line1\nline2\nline3");
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it("recovers unicode exactly", () => {
|
|
60
|
+
const template = "(() => { const args = __ARGS__; return args; })()";
|
|
61
|
+
const args = { symbol: "▸ ★ 🎉" };
|
|
62
|
+
const injected = injectArgs(template, args);
|
|
63
|
+
const result = new Function(`return ${injected}`)() as typeof args;
|
|
64
|
+
expect(result.symbol).toBe("▸ ★ 🎉");
|
|
65
|
+
});
|
|
66
|
+
});
|