@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,40 @@
|
|
|
1
|
+
## 1. Schema changes (src/schemas/)
|
|
2
|
+
|
|
3
|
+
- [x] 1.1 Define `MoveTaskInput` zod schema: `{ id: IdSchema, projectId: IdSchema.optional(), parentTaskId: IdSchema.optional() }` with `.refine()` that exactly one of `projectId` or `parentTaskId` is provided
|
|
4
|
+
- [x] 1.2 Define `MoveProjectInput` zod schema: `{ id: IdSchema, folderId: IdSchema.nullable() }`
|
|
5
|
+
- [x] 1.3 Export `MoveTaskInput` and `MoveProjectInput` from `src/schemas/index.ts`
|
|
6
|
+
|
|
7
|
+
## 2. Snippets (src/snippets/)
|
|
8
|
+
|
|
9
|
+
- [x] 2.1 Create `move_task.js`: resolve task by ID (NotFoundError if missing); if `projectId` provided, resolve project (NotFoundError if missing), assign `task.containingProject = project`; if `parentTaskId` provided, resolve parent task (NotFoundError if missing), assign `task.parentTask = parentTask`; return updated `TaskSummary` fields
|
|
10
|
+
- [x] 2.2 Create `move_project.js`: resolve project by ID (NotFoundError if missing); if `folderId` is non-null, resolve folder (NotFoundError if missing), assign `project.parentFolder = folder`; if `folderId` is null, assign `project.parentFolder = null`; return updated `ProjectSummary` fields
|
|
11
|
+
|
|
12
|
+
## 3. Tool handlers (src/tools/)
|
|
13
|
+
|
|
14
|
+
- [x] 3.1 Create `moveTask.ts`: schema uses `MoveTaskInput`, handler calls `runSnippet("move_task", input)`, parses result as `TaskSummary`, exports `moveTaskTool`
|
|
15
|
+
- [x] 3.2 Create `moveProject.ts`: schema uses `MoveProjectInput`, handler calls `runSnippet("move_project", input)`, parses result as `ProjectSummary`, exports `moveProjectTool`
|
|
16
|
+
|
|
17
|
+
## 4. Registration
|
|
18
|
+
|
|
19
|
+
- [x] 4.1 Add `move_task` and `move_project` to `ALLOWED_SNIPPETS` in `src/runtime/snippetLoader.ts`
|
|
20
|
+
- [x] 4.2 Import and add `moveTaskTool` and `moveProjectTool` to `allTools` in `src/tools/index.ts`
|
|
21
|
+
|
|
22
|
+
## 5. Unit tests (test/unit/)
|
|
23
|
+
|
|
24
|
+
- [x] 5.1 Add schema tests for `MoveTaskInput`: valid with projectId only; valid with parentTaskId only; invalid with both; invalid with neither
|
|
25
|
+
- [x] 5.2 Add schema tests for `MoveProjectInput`: valid with folderId string; valid with folderId null; invalid with missing id
|
|
26
|
+
|
|
27
|
+
## 6. Integration tests (test/integration/)
|
|
28
|
+
|
|
29
|
+
- [x] 6.1 `moveTask.int.test.ts`: create task in project A; move to project B; verify task appears in project B
|
|
30
|
+
- [x] 6.2 `moveTask.int.test.ts`: create task; make it a subtask of another task via parentTaskId
|
|
31
|
+
- [x] 6.3 `moveTask.int.test.ts`: non-existent task ID returns NotFoundError
|
|
32
|
+
- [x] 6.4 `moveProject.int.test.ts`: create project in folder A; move to folder B; verify project is in folder B
|
|
33
|
+
- [x] 6.5 `moveProject.int.test.ts`: move project to top level (folderId: null)
|
|
34
|
+
- [x] 6.6 `moveProject.int.test.ts`: non-existent project ID returns NotFoundError
|
|
35
|
+
|
|
36
|
+
## 7. Verification
|
|
37
|
+
|
|
38
|
+
- [x] 7.1 `npm run typecheck` clean
|
|
39
|
+
- [x] 7.2 `npm test` (unit suite) clean
|
|
40
|
+
- [x] 7.3 Manually run integration suite
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
## Context
|
|
2
|
+
|
|
3
|
+
Projects in OmniJS have more writable fields than tasks: type (`sequential`, `containsSingletonActions`), review interval, and folder placement. Status transitions use dedicated methods. This design follows the same patterns established in `task-crud`.
|
|
4
|
+
|
|
5
|
+
OmniJS project write operations:
|
|
6
|
+
- `new Project(name, position)` — creates a project; `position` is a `Folder` or omitted for top-level
|
|
7
|
+
- Property assignment for scalars: `name`, `note`, `flagged`, `deferDate`, `dueDate`, `sequential`, `containsSingletonActions`
|
|
8
|
+
- `project.status = Project.Status.Active / OnHold` — for hold/active transitions
|
|
9
|
+
- `project.markComplete()` — dedicated method for done
|
|
10
|
+
- `project.status = Project.Status.Dropped` — drops the project (`project.drop()` is not available via `evaluateJavascript`)
|
|
11
|
+
- `project.reviewInterval.steps = n` — only `steps` can be mutated in-place; `Project.ReviewInterval` is a CallbackObject in the `evaluateJavascript` context and cannot be constructed with `new`, so the unit cannot be changed and the interval cannot be cleared (OmniJS rejects `null`)
|
|
12
|
+
- Tags: `project.addTag(tag)` / `project.removeTag(tag)` — same pattern as tasks
|
|
13
|
+
- `deleteObject(project)` — permanent deletion including all tasks
|
|
14
|
+
|
|
15
|
+
## Goals / Non-Goals
|
|
16
|
+
|
|
17
|
+
**Goals:**
|
|
18
|
+
- Expose create, edit, complete, drop, and delete for projects
|
|
19
|
+
- Support folder placement on create (top-level or inside a folder)
|
|
20
|
+
- Edit any subset of fields in one call including review interval and type
|
|
21
|
+
- Replace-not-merge tag semantics (consistent with task-crud)
|
|
22
|
+
|
|
23
|
+
**Non-Goals:**
|
|
24
|
+
- Moving a project to a different folder (separate `move-operations` change)
|
|
25
|
+
- Creating tasks inside a new project in the same call (use `create_task` after)
|
|
26
|
+
- Recurrence rules (separate `recurrence` change)
|
|
27
|
+
|
|
28
|
+
## Decisions
|
|
29
|
+
|
|
30
|
+
### Decision 1: `type` mapped to OmniJS properties
|
|
31
|
+
|
|
32
|
+
Project type is not a single OmniJS property — it is encoded across two booleans:
|
|
33
|
+
- `sequential: true` → `"sequential"`
|
|
34
|
+
- `containsSingletonActions: true` → `"singleActions"`
|
|
35
|
+
- both false → `"parallel"`
|
|
36
|
+
|
|
37
|
+
`create_project` and `edit_project` accept the enum string `"parallel" | "sequential" | "singleActions"` and the snippet maps it to the correct property assignments.
|
|
38
|
+
|
|
39
|
+
### Decision 2: `reviewInterval` — steps only, in-place mutation
|
|
40
|
+
|
|
41
|
+
`edit_project` and `create_project` accept `reviewInterval` as `{steps: number, unit: "days" | "weeks" | "months" | "years"}`. Only the `steps` field can actually be applied: `project.reviewInterval.steps = n` mutates the existing interval in place.
|
|
42
|
+
|
|
43
|
+
**Limitations discovered during implementation:** `Project.ReviewInterval` is a CallbackObject in the `evaluateJavascript` context — `new Project.ReviewInterval(steps, unit)` throws. `Project.ReviewInterval.Unit` is also undefined in this context, so the unit cannot be changed. Setting `project.reviewInterval = null` is rejected by OmniJS ("must be set to a non-null value"). As a result, the `unit` field in `reviewInterval` input is accepted by the schema but silently ignored at runtime.
|
|
44
|
+
|
|
45
|
+
**Note:** Read output (`get_project`) returns the string form `"1 weeks"` — that is a read-side concern unchanged by this limitation.
|
|
46
|
+
|
|
47
|
+
### Decision 3: Status transitions via dedicated tools and property assignment
|
|
48
|
+
|
|
49
|
+
`complete_project` calls `markComplete()`. `drop_project` sets `project.status = Project.Status.Dropped` — `project.drop()` does not exist in the `evaluateJavascript` context. Active/on-hold are set via `project.status = Project.Status.Active / OnHold` inside `edit_project` (these are property assignments, not method calls). This keeps the common "put on hold" operation accessible through `edit_project` without requiring a dedicated tool.
|
|
50
|
+
|
|
51
|
+
### Decision 4: `delete_project` description warns about task cascade
|
|
52
|
+
|
|
53
|
+
`deleteObject(project)` removes the project and all its tasks. The tool description explicitly states this. Consistent with `delete_task` and `delete_folder` confirmation pattern.
|
|
54
|
+
|
|
55
|
+
## Risks / Trade-offs
|
|
56
|
+
|
|
57
|
+
- **`new Project(name)` without a position** creates a top-level project. Verified in OmniJS.
|
|
58
|
+
- **`new Project(name, folder)` with a Folder object** places the project inside that folder.
|
|
59
|
+
- **Review interval unit enum values** — OmniJS `Project.ReviewInterval.Unit` has `.days`, `.weeks`, `.months`, `.years`. Snippet should map string to the enum member, not pass the string directly.
|
|
60
|
+
- **Changing type from `singleActions`** may silently fail if the project has tasks structured for that mode — low risk for typical usage, not worth guarding against in v1.
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
## Why
|
|
2
|
+
|
|
3
|
+
The bootstrap change delivered read-only project access. Callers can observe projects but cannot create, modify, complete, drop, or delete them — making the server unsuitable for any workflow that involves acting on projects.
|
|
4
|
+
|
|
5
|
+
## What Changes
|
|
6
|
+
|
|
7
|
+
- Add `create_project` tool: create a project in a folder or at the top level
|
|
8
|
+
- Add `edit_project` tool: modify any subset of a project's scalar fields, type, tags, and review interval in a single call
|
|
9
|
+
- Add `complete_project` tool: mark a project done
|
|
10
|
+
- Add `drop_project` tool: mark a project dropped
|
|
11
|
+
- Add `delete_project` tool: permanently delete a project and all its tasks (tool description instructs the AI to confirm with the user before invoking)
|
|
12
|
+
|
|
13
|
+
## Capabilities
|
|
14
|
+
|
|
15
|
+
### New Capabilities
|
|
16
|
+
|
|
17
|
+
- `project-write`: Create, edit, complete, drop, and permanently delete OmniFocus projects
|
|
18
|
+
|
|
19
|
+
### Modified Capabilities
|
|
20
|
+
|
|
21
|
+
_(none — existing `project-management` read requirements are unchanged)_
|
|
22
|
+
|
|
23
|
+
## Impact
|
|
24
|
+
|
|
25
|
+
- 5 new MCP tools registered in `src/server.ts`
|
|
26
|
+
- 5 new tool handler files in `src/tools/`
|
|
27
|
+
- 5 new OmniJS snippets in `src/snippets/`
|
|
28
|
+
- `ALLOWED_SNIPPETS` allowlist in `src/runtime/snippetLoader.ts` must be extended
|
|
29
|
+
- Integration tests will mutate real OmniFocus data (scoped to fixture folder per existing pattern)
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
## ADDED Requirements
|
|
2
|
+
|
|
3
|
+
### Requirement: Create project
|
|
4
|
+
|
|
5
|
+
The system SHALL provide a `create_project` tool that creates a new OmniFocus project and returns its full detail record. The tool SHALL accept `{name: string, folderId?: string, note?: string, type?: "parallel" | "sequential" | "singleActions", status?: "active" | "onHold", flagged?: boolean, deferDate?: string, dueDate?: string, reviewInterval?: {steps: number, unit: "days" | "weeks" | "months" | "years"}, tagIds?: string[]}`. If `folderId` is provided the project SHALL be created inside that folder; otherwise it SHALL be created at the top level.
|
|
6
|
+
|
|
7
|
+
#### Scenario: Create top-level project
|
|
8
|
+
- **WHEN** `create_project` is called with `{name: "My Project"}` and no `folderId`
|
|
9
|
+
- **THEN** the tool creates the project at the top level and returns its full detail record including a stable `id`
|
|
10
|
+
|
|
11
|
+
#### Scenario: Create project inside a folder
|
|
12
|
+
- **WHEN** `create_project` is called with `{name: "My Project", folderId: "abc123"}`
|
|
13
|
+
- **THEN** the tool creates the project inside the specified folder and returns its full detail record with `folderPath` reflecting the folder
|
|
14
|
+
|
|
15
|
+
#### Scenario: Create sequential project
|
|
16
|
+
- **WHEN** `create_project` is called with `{name: "My Project", type: "sequential"}`
|
|
17
|
+
- **THEN** the returned project detail includes `type: "sequential"`
|
|
18
|
+
|
|
19
|
+
#### Scenario: Non-existent folder returns not-found error
|
|
20
|
+
- **WHEN** `create_project` is called with a `folderId` that does not correspond to any folder
|
|
21
|
+
- **THEN** the tool returns a structured not-found error
|
|
22
|
+
|
|
23
|
+
### Requirement: Edit project
|
|
24
|
+
|
|
25
|
+
The system SHALL provide an `edit_project` tool that modifies an existing project and returns its updated full detail record. The tool SHALL accept `{id: string}` plus any subset of `{name?: string, note?: string, type?: "parallel" | "sequential" | "singleActions", status?: "active" | "onHold", flagged?: boolean, deferDate?: string | null, dueDate?: string | null, reviewInterval?: {steps: number, unit: "days" | "weeks" | "months" | "years"}, tagIds?: string[]}`. Fields omitted from the call SHALL be left unchanged. When `tagIds` is provided it SHALL replace the project's entire tag set. Passing `null` for a date SHALL clear the field. When `reviewInterval` is provided, only the `steps` value is updated; the `unit` field is accepted by the schema but cannot be changed at runtime due to OmniJS API constraints in the `evaluateJavascript` context.
|
|
26
|
+
|
|
27
|
+
#### Scenario: Put project on hold
|
|
28
|
+
- **WHEN** `edit_project` is called with `{id: "abc123", status: "onHold"}`
|
|
29
|
+
- **THEN** the project's status becomes `"onHold"` and all other fields are unchanged
|
|
30
|
+
|
|
31
|
+
#### Scenario: Set review interval
|
|
32
|
+
- **WHEN** `edit_project` is called with `{id: "abc123", reviewInterval: {steps: 2, unit: "weeks"}}`
|
|
33
|
+
- **THEN** the project's review interval is updated and the returned detail reflects the change
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
#### Scenario: Non-existent project returns not-found error
|
|
37
|
+
- **WHEN** `edit_project` is called with an ID that does not correspond to any project
|
|
38
|
+
- **THEN** the tool returns a structured not-found error
|
|
39
|
+
|
|
40
|
+
### Requirement: Complete project
|
|
41
|
+
|
|
42
|
+
The system SHALL provide a `complete_project` tool that marks an existing project done using OmniJS `markComplete()` and returns the project's updated full detail record.
|
|
43
|
+
|
|
44
|
+
#### Scenario: Complete an existing project
|
|
45
|
+
- **WHEN** `complete_project` is called with the ID of an active project
|
|
46
|
+
- **THEN** the project's status becomes `"done"` and the tool returns the updated detail record
|
|
47
|
+
|
|
48
|
+
#### Scenario: Non-existent project returns not-found error
|
|
49
|
+
- **WHEN** `complete_project` is called with an ID that does not correspond to any project
|
|
50
|
+
- **THEN** the tool returns a structured not-found error
|
|
51
|
+
|
|
52
|
+
### Requirement: Drop project
|
|
53
|
+
|
|
54
|
+
The system SHALL provide a `drop_project` tool that marks an existing project dropped using OmniJS `drop()` and returns the project's updated full detail record.
|
|
55
|
+
|
|
56
|
+
#### Scenario: Drop an existing project
|
|
57
|
+
- **WHEN** `drop_project` is called with the ID of an active project
|
|
58
|
+
- **THEN** the project's status becomes `"dropped"` and the tool returns the updated detail record
|
|
59
|
+
|
|
60
|
+
#### Scenario: Non-existent project returns not-found error
|
|
61
|
+
- **WHEN** `drop_project` is called with an ID that does not correspond to any project
|
|
62
|
+
- **THEN** the tool returns a structured not-found error
|
|
63
|
+
|
|
64
|
+
### Requirement: Delete project
|
|
65
|
+
|
|
66
|
+
The system SHALL provide a `delete_project` tool that permanently deletes a project and all its tasks 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 removes all tasks within the project.
|
|
67
|
+
|
|
68
|
+
#### Scenario: Delete an existing project
|
|
69
|
+
- **WHEN** `delete_project` is called with the ID of an existing project
|
|
70
|
+
- **THEN** the project and all its tasks are permanently removed from OmniFocus and the tool returns a confirmation envelope
|
|
71
|
+
|
|
72
|
+
#### Scenario: Non-existent project returns not-found error
|
|
73
|
+
- **WHEN** `delete_project` is called with an ID that does not correspond to any project
|
|
74
|
+
- **THEN** the tool returns a structured not-found error
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
## 1. Snippets (src/snippets/)
|
|
2
|
+
|
|
3
|
+
- [x] 1.1 `create_project.js`: accept `{name, folderId?, note?, type?, status?, flagged?, deferDate?, dueDate?, reviewInterval?, tagIds?}`; resolve folder by ID if provided; map `type` string to `sequential`/`containsSingletonActions` booleans; set `status` to `Project.Status.OnHold` if `"onHold"`; construct `new Project.ReviewInterval(steps, unit)` if `reviewInterval` provided; resolve and assign tags by ID; return full project detail envelope
|
|
4
|
+
- [x] 1.2 `edit_project.js`: accept `{id, name?, note?, type?, status?, flagged?, deferDate?, dueDate?, reviewInterval?, tagIds?}`; resolve project by ID; apply only provided fields; map `type` and `status` to OmniJS equivalents; handle `null` for date/reviewInterval to clear; replace tag set when `tagIds` provided; return updated full detail envelope
|
|
5
|
+
- [x] 1.3 `complete_project.js`: accept `{id}`; resolve project by ID; call `project.markComplete()`; return updated full detail envelope
|
|
6
|
+
- [x] 1.4 `drop_project.js`: accept `{id}`; resolve project by ID; use `project.status = Project.Status.Dropped` (project.drop() is not available via evaluateJavascript); return updated full detail envelope
|
|
7
|
+
- [x] 1.5 `delete_project.js`: accept `{id}`; resolve project by ID; call `deleteObject(project)`; return `{ok: true, data: {id}}`
|
|
8
|
+
|
|
9
|
+
## 2. Snippet allowlist
|
|
10
|
+
|
|
11
|
+
- [x] 2.1 Add `create_project`, `edit_project`, `complete_project`, `drop_project`, `delete_project` to `ALLOWED_SNIPPETS` in `src/runtime/snippetLoader.ts`
|
|
12
|
+
|
|
13
|
+
## 3. Schemas (src/schemas/shapes.ts)
|
|
14
|
+
|
|
15
|
+
- [x] 3.1 Define `ReviewIntervalInput` zod schema: `{steps: z.number().int().positive(), unit: z.enum(["days","weeks","months","years"])}`
|
|
16
|
+
- [x] 3.2 Define `CreateProjectInput` zod schema with all optional fields including `reviewInterval: ReviewIntervalInput.optional()`
|
|
17
|
+
- [x] 3.3 Define `EditProjectInput` zod schema — `id` required; date fields accept `string | null`; `reviewInterval` accepts `ReviewIntervalInput` (null not supported — OmniJS rejects null for this property)
|
|
18
|
+
|
|
19
|
+
## 4. Tool handlers (src/tools/)
|
|
20
|
+
|
|
21
|
+
- [x] 4.1 `createProject.ts`: validate input with `CreateProjectInput`; invoke `runSnippet("create_project", args)`; validate result against `ProjectDetail`; return
|
|
22
|
+
- [x] 4.2 `editProject.ts`: validate input with `EditProjectInput`; invoke `runSnippet("edit_project", args)`; validate result against `ProjectDetail`; return
|
|
23
|
+
- [x] 4.3 `completeProject.ts`: input `{id: IdSchema}`; invoke `runSnippet("complete_project", {id})`; validate result against `ProjectDetail`; return
|
|
24
|
+
- [x] 4.4 `dropProject.ts`: input `{id: IdSchema}`; invoke `runSnippet("drop_project", {id})`; validate result against `ProjectDetail`; return
|
|
25
|
+
- [x] 4.5 `deleteProject.ts`: input `{id: IdSchema}`; tool description MUST state the AI should confirm with the user before invoking and note that all tasks in the project are permanently deleted; invoke `runSnippet("delete_project", {id})`; return confirmation
|
|
26
|
+
|
|
27
|
+
## 5. Server registration
|
|
28
|
+
|
|
29
|
+
- [x] 5.1 Export all five new tool definitions from `src/tools/index.ts`
|
|
30
|
+
|
|
31
|
+
## 6. Unit tests (test/unit/)
|
|
32
|
+
|
|
33
|
+
- [x] 6.1 `schemas.createProject.test.ts`: valid inputs pass; invalid `type` enum rejected; invalid `reviewInterval.unit` rejected; null dates accepted in edit schema
|
|
34
|
+
|
|
35
|
+
## 7. Integration tests (test/integration/)
|
|
36
|
+
|
|
37
|
+
- [x] 7.1 `createProject.int.test.ts`: create top-level project; create project in folder; verify `folderPath` and `type`
|
|
38
|
+
- [x] 7.2 `editProject.int.test.ts`: edit name; set on-hold; set review interval steps; (clear review interval not supported — OmniJS rejects null)
|
|
39
|
+
- [x] 7.3 `completeProject.int.test.ts`: complete a project, verify status = `"done"`
|
|
40
|
+
- [x] 7.4 `dropProject.int.test.ts`: drop a project, verify status = `"dropped"`
|
|
41
|
+
- [x] 7.5 `deleteProject.int.test.ts`: delete a project, verify subsequent `get_project` returns not-found
|
|
42
|
+
|
|
43
|
+
## 8. Verification
|
|
44
|
+
|
|
45
|
+
- [x] 8.1 `npm run typecheck` clean
|
|
46
|
+
- [x] 8.2 `npm test` (unit suite) clean
|
|
47
|
+
- [x] 8.3 Manually run integration suite; verify fixture cleanup
|
|
48
|
+
- [ ] 8.4 Connect to Claude Desktop; exercise all five tools against a real database
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
## Context
|
|
2
|
+
|
|
3
|
+
`list_projects` currently fetches all projects and returns a flat `ProjectSummary` array with no filtering. This mirrors the pre-filtering state of `list_tasks`. The task-filtering change established the pattern: filter inside the OmniJS snippet before data crosses the JXA bridge, enrich the summary shape with fields needed for filtering and display, change the default to exclude terminal statuses.
|
|
4
|
+
|
|
5
|
+
`ProjectSummary` currently omits `flagged` and the parent folder ID (it has `folderPath` string but not `folderId`). Without `folderId`, the LLM cannot cross-reference a listed project with a known folder ID, and cannot filter by folder ID from returned results.
|
|
6
|
+
|
|
7
|
+
## Goals / Non-Goals
|
|
8
|
+
|
|
9
|
+
**Goals:**
|
|
10
|
+
- Filter projects by `status[]`, `folderId` (recursive subtree), and `flagged` inside the snippet
|
|
11
|
+
- Enrich `ProjectSummary` with `flagged` and `folderId`
|
|
12
|
+
- Change default behavior: exclude `done` and `dropped` projects when no `status` filter is provided
|
|
13
|
+
- Cap results with a configurable `limit` (default 100)
|
|
14
|
+
|
|
15
|
+
**Non-Goals:**
|
|
16
|
+
- Filtering by tag, due date, or review date (can be added later)
|
|
17
|
+
- Filtering `list_folders` or `list_tags` (separate changes if needed)
|
|
18
|
+
- Pagination / cursor-based continuation
|
|
19
|
+
|
|
20
|
+
## Decisions
|
|
21
|
+
|
|
22
|
+
### Decision 1: Same pattern as task-filtering — filter in snippet
|
|
23
|
+
|
|
24
|
+
All filter logic runs inside `evaluateJavascript`. The TypeScript layer validates the schema and passes filter args through. Consistent with task-filtering; avoids any future performance concerns if project counts grow.
|
|
25
|
+
|
|
26
|
+
### Decision 2: Enrich ProjectSummary with flagged and folderId
|
|
27
|
+
|
|
28
|
+
`ProjectSummary` gains:
|
|
29
|
+
- `flagged: boolean` — needed to support the `flagged` filter and display flagged state in results
|
|
30
|
+
- `folderId: string | null` — the direct parent folder's `id.primaryKey`, or `null` for top-level projects
|
|
31
|
+
|
|
32
|
+
`folderPath` is kept as-is (useful for display). `folderId` is added alongside it as the machine-readable reference. No other detail fields are promoted to the summary.
|
|
33
|
+
|
|
34
|
+
### Decision 3: Default excludes done and dropped
|
|
35
|
+
|
|
36
|
+
When `filter.status` is omitted, the snippet excludes `Project.Status.Done` and `Project.Status.Dropped`. This mirrors the task-filtering default and is the correct behavior for LLM planning queries. Done/dropped projects can be retrieved explicitly with `status: ["done"]` or `status: ["dropped"]`.
|
|
37
|
+
|
|
38
|
+
### Decision 4: folderId filter is recursive
|
|
39
|
+
|
|
40
|
+
When `filter.folderId` is provided, the snippet returns projects whose parent folder chain includes the specified folder ID — i.e., direct children and all nested descendants. This is consistent with how `list_tasks` treats `scope.folderId`.
|
|
41
|
+
|
|
42
|
+
Implementation: check `p.flattenedProjects` on the resolved folder rather than walking `flattenedProjects` globally and checking ancestry. More efficient and correct.
|
|
43
|
+
|
|
44
|
+
### Decision 5: limit default 100
|
|
45
|
+
|
|
46
|
+
Projects are fewer than tasks; 100 is a sensible default. Callers may increase it.
|
|
47
|
+
|
|
48
|
+
## Risks / Trade-offs
|
|
49
|
+
|
|
50
|
+
- **Breaking default behavior** — same risk as task-filtering. Acceptable: single consumer, new default is strictly more useful.
|
|
51
|
+
- **folderId recursive filter uses folder.flattenedProjects** — this is the cleanest OmniJS approach. If the folder ID is invalid, return an empty array (not an error) since this is a filter, not a scope requirement. Actually: throw NotFoundError to be consistent with list_tasks scope behavior.
|
|
52
|
+
- **flagged on ProjectSummary is a new required field** — any existing unit test fixtures for ProjectSummary will need updating (same situation as TaskSummary in task-filtering).
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
## Why
|
|
2
|
+
|
|
3
|
+
`list_projects` currently returns every project in the database with no filtering. Users with years of OmniFocus history may have hundreds of completed (`done`) and dropped projects, making it impractical for an LLM to identify what's actually active. Applying the same filter-in-snippet pattern established by task-filtering gives the LLM a useful, scoped view of the project list.
|
|
4
|
+
|
|
5
|
+
## What Changes
|
|
6
|
+
|
|
7
|
+
- `list_projects` accepts an optional `filter` object: `status` (array), `folderId` (recursive subtree), `flagged`
|
|
8
|
+
- `list_projects` accepts an optional `limit` (default 100)
|
|
9
|
+
- **BREAKING**: Default behavior changes — when no `status` filter is given, `done` and `dropped` projects are excluded. Pass `status: ["done"]` explicitly to retrieve completed projects.
|
|
10
|
+
- `ProjectSummary` is enriched with `flagged` (boolean) and `folderId` (string | null — the direct parent folder's ID) so the LLM can reason about results without fetching full project detail
|
|
11
|
+
|
|
12
|
+
## Capabilities
|
|
13
|
+
|
|
14
|
+
### New Capabilities
|
|
15
|
+
- `project-filtering`: Filter parameters and enriched summary for `list_projects`
|
|
16
|
+
|
|
17
|
+
### Modified Capabilities
|
|
18
|
+
- `project-management`: `list_projects` tool signature changes (new filter/limit params, enriched return shape, changed default behavior)
|
|
19
|
+
|
|
20
|
+
## Impact
|
|
21
|
+
|
|
22
|
+
- `src/snippets/list_projects.js` — rewritten to apply filters and return enriched fields
|
|
23
|
+
- `src/schemas/shapes.ts` — `ProjectSummary` gains `flagged` and `folderId`; new `ListProjectsFilter` schema
|
|
24
|
+
- `src/tools/listProjects.ts` — input schema updated with filter and limit
|
|
25
|
+
- `test/unit/` — schema tests for new filter type
|
|
26
|
+
- `test/integration/listProjectsFiltered.int.test.ts` — new integration tests for filter combinations
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
## ADDED Requirements
|
|
2
|
+
|
|
3
|
+
### Requirement: Filter list_projects results
|
|
4
|
+
|
|
5
|
+
The system SHALL allow `list_projects` to accept an optional `filter` object that restricts which projects are returned. All filter fields are optional and combine as AND conditions. When `filter.status` is omitted, the tool SHALL exclude projects with status `done` or `dropped` by default.
|
|
6
|
+
|
|
7
|
+
Filter fields:
|
|
8
|
+
- `status` (array of ProjectStatus): return only projects whose status is in the array; overrides the default exclusion of done/dropped
|
|
9
|
+
- `folderId` (string): return only projects within the specified folder or any of its descendant folders; throws not-found if the folder ID does not exist
|
|
10
|
+
- `flagged` (boolean): when `true`, return only flagged projects
|
|
11
|
+
|
|
12
|
+
#### Scenario: Default excludes done and dropped projects
|
|
13
|
+
- **WHEN** `list_projects` is called with no filter
|
|
14
|
+
- **THEN** the tool returns only projects with status `active` or `onHold`
|
|
15
|
+
|
|
16
|
+
#### Scenario: Filter by status array
|
|
17
|
+
- **WHEN** `list_projects` is called with `{ filter: { status: ["active"] } }`
|
|
18
|
+
- **THEN** the tool returns only active projects
|
|
19
|
+
|
|
20
|
+
#### Scenario: Explicit status filter retrieves done projects
|
|
21
|
+
- **WHEN** `list_projects` is called with `{ filter: { status: ["done"] } }`
|
|
22
|
+
- **THEN** the tool returns completed projects (the default exclusion does not apply)
|
|
23
|
+
|
|
24
|
+
#### Scenario: Filter by folderId returns projects in subtree
|
|
25
|
+
- **WHEN** `list_projects` is called with `{ filter: { folderId: "abc123" } }`
|
|
26
|
+
- **THEN** the tool returns only projects whose parent folder chain includes the specified folder
|
|
27
|
+
|
|
28
|
+
#### Scenario: Non-existent folderId returns not-found error
|
|
29
|
+
- **WHEN** `list_projects` is called with a `folderId` that does not correspond to any folder
|
|
30
|
+
- **THEN** the tool returns a structured not-found error
|
|
31
|
+
|
|
32
|
+
#### Scenario: Filter by flagged
|
|
33
|
+
- **WHEN** `list_projects` is called with `{ filter: { flagged: true } }`
|
|
34
|
+
- **THEN** the tool returns only flagged projects, excluding done and dropped projects
|
|
35
|
+
|
|
36
|
+
#### Scenario: Combined filters act as AND
|
|
37
|
+
- **WHEN** `list_projects` is called with `{ filter: { status: ["active"], flagged: true } }`
|
|
38
|
+
- **THEN** the tool returns only projects that are both active AND flagged
|
|
39
|
+
|
|
40
|
+
### Requirement: Limit list_projects results
|
|
41
|
+
|
|
42
|
+
The system SHALL allow `list_projects` to accept an optional `limit` integer that caps the number of projects returned. When `limit` is omitted, a default of 100 SHALL apply.
|
|
43
|
+
|
|
44
|
+
#### Scenario: Default limit of 100
|
|
45
|
+
- **WHEN** `list_projects` is called without a `limit` and more than 100 projects match
|
|
46
|
+
- **THEN** the tool returns at most 100 projects
|
|
47
|
+
|
|
48
|
+
#### Scenario: Custom limit
|
|
49
|
+
- **WHEN** `list_projects` is called with `{ limit: 20 }`
|
|
50
|
+
- **THEN** the tool returns at most 20 projects
|
|
51
|
+
|
|
52
|
+
### Requirement: Enriched project summary includes flagged and folderId
|
|
53
|
+
|
|
54
|
+
The `ProjectSummary` returned by `list_projects` SHALL include `flagged` (boolean) and `folderId` (string or null) in addition to existing fields. `folderId` SHALL be the direct parent folder's `id.primaryKey`, or `null` for top-level projects.
|
|
55
|
+
|
|
56
|
+
#### Scenario: Summary includes flagged
|
|
57
|
+
- **WHEN** `list_projects` returns a flagged project
|
|
58
|
+
- **THEN** the project summary includes `flagged: true`
|
|
59
|
+
|
|
60
|
+
#### Scenario: Summary includes folderId for nested project
|
|
61
|
+
- **WHEN** `list_projects` returns a project inside a folder
|
|
62
|
+
- **THEN** the project summary includes `folderId` set to the parent folder's ID
|
|
63
|
+
|
|
64
|
+
#### Scenario: Summary includes folderId null for top-level project
|
|
65
|
+
- **WHEN** `list_projects` returns a top-level project with no containing folder
|
|
66
|
+
- **THEN** the project summary includes `folderId: null`
|
package/openspec/changes/archive/2026-04-09-project-filtering/specs/project-management/spec.md
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
## MODIFIED Requirements
|
|
2
|
+
|
|
3
|
+
### Requirement: List projects
|
|
4
|
+
|
|
5
|
+
The system SHALL provide a `list_projects` tool that returns projects in the OmniFocus database as an array of summaries, each containing `{id, name, folderPath, folderId, status, type, flagged}`. The `status` field SHALL be one of `"active" | "onHold" | "done" | "dropped"`. The `type` field SHALL be one of `"parallel" | "sequential" | "singleActions"`. The `folderPath` field SHALL use the canonical ` ▸ ` separator and SHALL be an empty string for top-level projects. The `folderId` field SHALL be the direct parent folder's ID or `null` for top-level projects. The tool SHALL accept an optional `filter` object (see project-filtering spec) and an optional `limit` integer (default 100). When no `filter.status` is provided, projects with status `done` or `dropped` SHALL be excluded by default.
|
|
6
|
+
|
|
7
|
+
#### Scenario: Active and on-hold projects returned by default
|
|
8
|
+
- **WHEN** `list_projects` is called with no arguments
|
|
9
|
+
- **THEN** the tool returns only projects with status `active` or `onHold`, each with id, name, folderPath, folderId, status, type, and flagged populated
|
|
10
|
+
|
|
11
|
+
#### Scenario: Single Actions list is reported with correct type
|
|
12
|
+
- **WHEN** the database contains a Single Actions list project
|
|
13
|
+
- **THEN** that project appears in the result with `type: "singleActions"`, never as `"parallel"` or `"sequential"`
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
## 1. Schema changes (src/schemas/)
|
|
2
|
+
|
|
3
|
+
- [x] 1.1 Add `flagged: z.boolean()` and `folderId: IdSchema.nullable()` to `ProjectSummary` in `shapes.ts`
|
|
4
|
+
- [x] 1.2 Define `ListProjectsFilter` zod schema: `{ status: z.array(ProjectStatus).optional(), folderId: IdSchema.optional(), flagged: z.literal(true).optional() }`
|
|
5
|
+
- [x] 1.3 Export `ListProjectsFilter` from `src/schemas/index.ts`
|
|
6
|
+
|
|
7
|
+
## 2. Snippet rewrite (src/snippets/list_projects.js)
|
|
8
|
+
|
|
9
|
+
- [x] 2.1 Add `flagged` and `folderId` (parent folder's `id.primaryKey` or `null`) to the project mapping helper
|
|
10
|
+
- [x] 2.2 Implement default status exclusion: when `args.filter?.status` is absent, exclude `Project.Status.Done` and `Project.Status.Dropped`
|
|
11
|
+
- [x] 2.3 Implement `status` array filter: when `args.filter?.status` is provided, keep only projects whose status maps to one of the given strings
|
|
12
|
+
- [x] 2.4 Implement `folderId` filter: resolve the folder by ID (throw `NotFoundError` if not found), then use `folder.flattenedProjects` to get the matching set before applying other filters
|
|
13
|
+
- [x] 2.5 Implement `flagged` filter: when `args.filter?.flagged` is `true`, keep only flagged projects
|
|
14
|
+
- [x] 2.6 Apply `args.limit` (default 100) as a slice after all filters
|
|
15
|
+
|
|
16
|
+
## 3. Tool handler update (src/tools/listProjects.ts)
|
|
17
|
+
|
|
18
|
+
- [x] 3.1 Add `filter: ListProjectsFilter.optional()` and `limit: z.number().int().positive().optional()` to `listProjectsSchema`
|
|
19
|
+
- [x] 3.2 Pass `filter` and `limit` through to `runSnippet`
|
|
20
|
+
- [x] 3.3 Update tool description to document filter params and new default-exclusion behavior
|
|
21
|
+
|
|
22
|
+
## 4. Unit tests (test/unit/)
|
|
23
|
+
|
|
24
|
+
- [x] 4.1 Search for any `ProjectSummary` fixtures in unit tests and add `flagged` and `folderId` fields
|
|
25
|
+
- [x] 4.2 Add schema tests for `ListProjectsFilter`: valid filter passes; invalid status enum rejected; `flagged: false` rejected (must be literal `true` or absent)
|
|
26
|
+
|
|
27
|
+
## 5. Integration tests (test/integration/)
|
|
28
|
+
|
|
29
|
+
- [x] 5.1 `listProjectsFiltered.int.test.ts`: create projects with known properties; verify default excludes done/dropped projects
|
|
30
|
+
- [x] 5.2 Verify `status` filter: create a completed project; confirm absent by default and present when `status: ["done"]` is passed
|
|
31
|
+
- [x] 5.3 Verify `folderId` filter: confirm only projects in that folder's subtree are returned
|
|
32
|
+
- [x] 5.4 Verify `flagged` filter: create flagged and unflagged projects; confirm only flagged returned
|
|
33
|
+
- [x] 5.5 Verify `limit`: confirm result is capped
|
|
34
|
+
- [x] 5.6 Verify enriched summary: returned projects include correct `flagged` and `folderId` fields
|
|
35
|
+
|
|
36
|
+
## 6. Verification
|
|
37
|
+
|
|
38
|
+
- [x] 6.1 `npm run typecheck` clean
|
|
39
|
+
- [x] 6.2 `npm test` (unit suite) clean
|
|
40
|
+
- [x] 6.3 Manually run integration suite; verify fixture cleanup
|
|
41
|
+
- [ ] 6.4 Connect to Claude Desktop; exercise filter queries against a real database
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
## Context
|
|
2
|
+
|
|
3
|
+
Tags in OmniJS form a tree (parent/child nesting) and have a `status` property (`active`, `onHold`, `dropped`). Write operations are straightforward. Deleting a tag with child tags removes the entire subtree; tasks that held the tag have it automatically removed by OmniFocus.
|
|
4
|
+
|
|
5
|
+
OmniJS tag write operations:
|
|
6
|
+
- `new Tag(name)` — creates a top-level tag
|
|
7
|
+
- `new Tag(name, parentTag)` — creates a child tag nested under `parentTag`
|
|
8
|
+
- `tag.name = "..."` — rename
|
|
9
|
+
- `tag.status = Tag.Status.Active / OnHold / Dropped` — status transitions
|
|
10
|
+
- `deleteObject(tag)` — removes the tag; child tags and task associations are cleaned up by OmniFocus automatically (unlike folders/projects, no manual cascade needed)
|
|
11
|
+
|
|
12
|
+
## Goals / Non-Goals
|
|
13
|
+
|
|
14
|
+
**Goals:**
|
|
15
|
+
- Create top-level and child tags
|
|
16
|
+
- Rename tags and change their status
|
|
17
|
+
- Delete tags (OmniFocus handles the cascade automatically)
|
|
18
|
+
|
|
19
|
+
**Non-Goals:**
|
|
20
|
+
- Moving a tag to a different parent (separate `move-operations` change)
|
|
21
|
+
|
|
22
|
+
## Decisions
|
|
23
|
+
|
|
24
|
+
### Decision 1: `deleteObject(tag)` is safe without manual cascade
|
|
25
|
+
|
|
26
|
+
Unlike folders, `deleteObject(tag)` in OmniJS removes the tag, its child tags, and all task/project tag associations automatically. No recursive snippet logic is needed.
|
|
27
|
+
|
|
28
|
+
**Verification needed during implementation:** confirm child tags are removed and task associations are cleaned. If this proves incorrect, apply the folder recursive pattern.
|
|
29
|
+
|
|
30
|
+
### Decision 2: `edit_tag` exposes both `name` and `status`
|
|
31
|
+
|
|
32
|
+
Tags have a meaningful `status` (`active`, `onHold`, `dropped`) that users set intentionally. Unlike folder status (which is derived), tag status is directly writable and useful — e.g., putting a context tag on hold while travelling. Both `name` and `status` are optional fields in `edit_tag`.
|
|
33
|
+
|
|
34
|
+
### Decision 3: `create_tag` placement via optional `parentTagId`
|
|
35
|
+
|
|
36
|
+
- `parentTagId` omitted → top-level tag
|
|
37
|
+
- `parentTagId` provided → child tag nested under that tag
|
|
38
|
+
|
|
39
|
+
Same pattern as `create_folder` with `parentFolderId`.
|
|
40
|
+
|
|
41
|
+
## Risks / Trade-offs
|
|
42
|
+
|
|
43
|
+
- **Deleting a tag with many children** — automatic cascade by OmniFocus; no performance concern beyond normal `deleteObject` overhead.
|
|
44
|
+
- **`tag.status` enum values** — OmniJS uses `Tag.Status.Active`, `Tag.Status.OnHold`, `Tag.Status.Dropped`. Snippet must map input strings to the correct enum members.
|
|
45
|
+
- **Renaming a tag used in many tasks** — OmniFocus updates all associations automatically (tags are objects with stable IDs, not strings).
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
## Why
|
|
2
|
+
|
|
3
|
+
The bootstrap change delivered read-only tag access. Callers can read the tag hierarchy but cannot create new tags, rename existing ones, or delete them — blocking workflows that involve tagging tasks with tags that don't yet exist or maintaining the tag taxonomy.
|
|
4
|
+
|
|
5
|
+
## What Changes
|
|
6
|
+
|
|
7
|
+
- Add `create_tag` tool: create a top-level tag or a child tag nested under an existing tag
|
|
8
|
+
- Add `edit_tag` tool: rename a tag
|
|
9
|
+
- Add `delete_tag` tool: permanently delete a tag; tasks that held the tag have it removed (tool description instructs the AI to confirm with the user before invoking)
|
|
10
|
+
|
|
11
|
+
## Capabilities
|
|
12
|
+
|
|
13
|
+
### New Capabilities
|
|
14
|
+
|
|
15
|
+
- `tag-write`: Create, rename, and permanently delete OmniFocus tags
|
|
16
|
+
|
|
17
|
+
### Modified Capabilities
|
|
18
|
+
|
|
19
|
+
_(none — existing `tag-management` read requirements are unchanged)_
|
|
20
|
+
|
|
21
|
+
## Impact
|
|
22
|
+
|
|
23
|
+
- 3 new MCP tools registered in `src/server.ts`
|
|
24
|
+
- 3 new tool handler files in `src/tools/`
|
|
25
|
+
- 3 new OmniJS snippets in `src/snippets/`
|
|
26
|
+
- `ALLOWED_SNIPPETS` allowlist in `src/runtime/snippetLoader.ts` must be extended
|
|
27
|
+
- Deleting a tag with child tags also removes all descendants; tool description should note this
|
|
28
|
+
- Integration tests will mutate real OmniFocus data (scoped to fixture folder per existing pattern)
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
## ADDED Requirements
|
|
2
|
+
|
|
3
|
+
### Requirement: Create tag
|
|
4
|
+
|
|
5
|
+
The system SHALL provide a `create_tag` tool that creates a new OmniFocus tag and returns its full detail record. The tool SHALL accept `{name: string, parentTagId?: string}`. If `parentTagId` is provided the tag SHALL be created nested under that tag; otherwise it SHALL be created at the top level.
|
|
6
|
+
|
|
7
|
+
#### Scenario: Create top-level tag
|
|
8
|
+
- **WHEN** `create_tag` is called with `{name: "Waiting"}` and no `parentTagId`
|
|
9
|
+
- **THEN** the tool creates the tag at the top level and returns its full detail record including a stable `id`
|
|
10
|
+
|
|
11
|
+
#### Scenario: Create child tag
|
|
12
|
+
- **WHEN** `create_tag` is called with `{name: "Email", parentTagId: "abc123"}`
|
|
13
|
+
- **THEN** the tool creates the tag nested under the specified parent tag and returns its full detail record with `path` and `parentId` set correctly
|
|
14
|
+
|
|
15
|
+
#### Scenario: Non-existent parent tag returns not-found error
|
|
16
|
+
- **WHEN** `create_tag` is called with a `parentTagId` that does not correspond to any tag
|
|
17
|
+
- **THEN** the tool returns a structured not-found error
|
|
18
|
+
|
|
19
|
+
### Requirement: Edit tag
|
|
20
|
+
|
|
21
|
+
The system SHALL provide an `edit_tag` tool that modifies an existing tag and returns its updated full detail record. The tool SHALL accept `{id: string}` plus any subset of `{name?: string, status?: "active" | "onHold" | "dropped"}`. Fields omitted SHALL be left unchanged.
|
|
22
|
+
|
|
23
|
+
#### Scenario: Rename a tag
|
|
24
|
+
- **WHEN** `edit_tag` is called with `{id: "abc123", name: "Delegated"}`
|
|
25
|
+
- **THEN** the tag's name is updated and the tool returns the updated detail record
|
|
26
|
+
|
|
27
|
+
#### Scenario: Put tag on hold
|
|
28
|
+
- **WHEN** `edit_tag` is called with `{id: "abc123", status: "onHold"}`
|
|
29
|
+
- **THEN** the tag's status becomes `"onHold"` and the tool returns the updated detail record
|
|
30
|
+
|
|
31
|
+
#### Scenario: Non-existent tag returns not-found error
|
|
32
|
+
- **WHEN** `edit_tag` is called with an ID that does not correspond to any tag
|
|
33
|
+
- **THEN** the tool returns a structured not-found error
|
|
34
|
+
|
|
35
|
+
### Requirement: Delete tag
|
|
36
|
+
|
|
37
|
+
The system SHALL provide a `delete_tag` tool that permanently deletes a tag and all its child tags using OmniJS `deleteObject()`. Tasks and projects that held the tag have it removed automatically by OmniFocus. The tool description SHALL instruct the AI to confirm with the user before invoking, noting that child tags are also deleted.
|
|
38
|
+
|
|
39
|
+
#### Scenario: Delete a tag
|
|
40
|
+
- **WHEN** `delete_tag` is called with the ID of an existing tag
|
|
41
|
+
- **THEN** the tag is permanently removed from OmniFocus, all tasks that held it have it removed, and the tool returns a confirmation envelope
|
|
42
|
+
|
|
43
|
+
#### Scenario: Delete tag with children removes entire subtree
|
|
44
|
+
- **WHEN** `delete_tag` is called with the ID of a tag that has child tags
|
|
45
|
+
- **THEN** the tag and all its descendant tags are deleted
|
|
46
|
+
|
|
47
|
+
#### Scenario: Non-existent tag returns not-found error
|
|
48
|
+
- **WHEN** `delete_tag` is called with an ID that does not correspond to any tag
|
|
49
|
+
- **THEN** the tool returns a structured not-found error
|