@scardis/omnifocus-mcp 0.1.0 → 0.1.2

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.
Files changed (223) hide show
  1. package/dist/schemas/shapes.d.ts +3 -33
  2. package/dist/schemas/shapes.d.ts.map +1 -1
  3. package/dist/schemas/shapes.js +3 -5
  4. package/dist/schemas/shapes.js.map +1 -1
  5. package/dist/server.js +3 -5
  6. package/dist/server.js.map +1 -1
  7. package/dist/tools/createTask.d.ts +1 -11
  8. package/dist/tools/createTask.d.ts.map +1 -1
  9. package/dist/tools/editTask.d.ts +1 -11
  10. package/dist/tools/editTask.d.ts.map +1 -1
  11. package/dist/tools/index.d.ts +2 -22
  12. package/dist/tools/index.d.ts.map +1 -1
  13. package/package.json +7 -1
  14. package/src/snippets/edit_task.js +4 -6
  15. package/.claude/commands/opsx/apply.md +0 -152
  16. package/.claude/commands/opsx/archive.md +0 -157
  17. package/.claude/commands/opsx/bulk-archive.md +0 -242
  18. package/.claude/commands/opsx/continue.md +0 -114
  19. package/.claude/commands/opsx/explore.md +0 -173
  20. package/.claude/commands/opsx/ff.md +0 -97
  21. package/.claude/commands/opsx/new.md +0 -69
  22. package/.claude/commands/opsx/onboard.md +0 -550
  23. package/.claude/commands/opsx/propose.md +0 -106
  24. package/.claude/commands/opsx/sync.md +0 -134
  25. package/.claude/commands/opsx/verify.md +0 -164
  26. package/.claude/skills/openspec-apply-change/SKILL.md +0 -156
  27. package/.claude/skills/openspec-archive-change/SKILL.md +0 -114
  28. package/.claude/skills/openspec-bulk-archive-change/SKILL.md +0 -246
  29. package/.claude/skills/openspec-continue-change/SKILL.md +0 -118
  30. package/.claude/skills/openspec-explore/SKILL.md +0 -288
  31. package/.claude/skills/openspec-ff-change/SKILL.md +0 -101
  32. package/.claude/skills/openspec-new-change/SKILL.md +0 -74
  33. package/.claude/skills/openspec-onboard/SKILL.md +0 -554
  34. package/.claude/skills/openspec-propose/SKILL.md +0 -110
  35. package/.claude/skills/openspec-sync-specs/SKILL.md +0 -138
  36. package/.claude/skills/openspec-verify-change/SKILL.md +0 -168
  37. package/CONTRIBUTING.md +0 -83
  38. package/openspec/changes/archive/2026-04-09-bootstrap-omnifocus-mcp/.openspec.yaml +0 -2
  39. package/openspec/changes/archive/2026-04-09-bootstrap-omnifocus-mcp/design.md +0 -162
  40. package/openspec/changes/archive/2026-04-09-bootstrap-omnifocus-mcp/proposal.md +0 -49
  41. package/openspec/changes/archive/2026-04-09-bootstrap-omnifocus-mcp/specs/attachments/spec.md +0 -9
  42. package/openspec/changes/archive/2026-04-09-bootstrap-omnifocus-mcp/specs/batch-operations/spec.md +0 -9
  43. package/openspec/changes/archive/2026-04-09-bootstrap-omnifocus-mcp/specs/database-inspection/spec.md +0 -9
  44. package/openspec/changes/archive/2026-04-09-bootstrap-omnifocus-mcp/specs/execution-runtime/spec.md +0 -69
  45. package/openspec/changes/archive/2026-04-09-bootstrap-omnifocus-mcp/specs/folder-management/spec.md +0 -25
  46. package/openspec/changes/archive/2026-04-09-bootstrap-omnifocus-mcp/specs/forecast/spec.md +0 -9
  47. package/openspec/changes/archive/2026-04-09-bootstrap-omnifocus-mcp/specs/identity-resolution/spec.md +0 -45
  48. package/openspec/changes/archive/2026-04-09-bootstrap-omnifocus-mcp/specs/perspective-management/spec.md +0 -9
  49. package/openspec/changes/archive/2026-04-09-bootstrap-omnifocus-mcp/specs/project-management/spec.md +0 -25
  50. package/openspec/changes/archive/2026-04-09-bootstrap-omnifocus-mcp/specs/recurrence/spec.md +0 -9
  51. package/openspec/changes/archive/2026-04-09-bootstrap-omnifocus-mcp/specs/settings/spec.md +0 -9
  52. package/openspec/changes/archive/2026-04-09-bootstrap-omnifocus-mcp/specs/tag-management/spec.md +0 -25
  53. package/openspec/changes/archive/2026-04-09-bootstrap-omnifocus-mcp/specs/task-management/spec.md +0 -29
  54. package/openspec/changes/archive/2026-04-09-bootstrap-omnifocus-mcp/specs/url-automation/spec.md +0 -9
  55. package/openspec/changes/archive/2026-04-09-bootstrap-omnifocus-mcp/specs/window-state/spec.md +0 -9
  56. package/openspec/changes/archive/2026-04-09-bootstrap-omnifocus-mcp/tasks.md +0 -84
  57. package/openspec/changes/archive/2026-04-09-folder-crud/.openspec.yaml +0 -2
  58. package/openspec/changes/archive/2026-04-09-folder-crud/design.md +0 -58
  59. package/openspec/changes/archive/2026-04-09-folder-crud/proposal.md +0 -28
  60. package/openspec/changes/archive/2026-04-09-folder-crud/specs/folder-write/spec.md +0 -45
  61. package/openspec/changes/archive/2026-04-09-folder-crud/tasks.md +0 -41
  62. package/openspec/changes/archive/2026-04-09-folder-tag-list-filtering/.openspec.yaml +0 -2
  63. package/openspec/changes/archive/2026-04-09-folder-tag-list-filtering/design.md +0 -38
  64. package/openspec/changes/archive/2026-04-09-folder-tag-list-filtering/proposal.md +0 -30
  65. package/openspec/changes/archive/2026-04-09-folder-tag-list-filtering/specs/folder-management/spec.md +0 -21
  66. package/openspec/changes/archive/2026-04-09-folder-tag-list-filtering/specs/tag-management/spec.md +0 -21
  67. package/openspec/changes/archive/2026-04-09-folder-tag-list-filtering/tasks.md +0 -35
  68. package/openspec/changes/archive/2026-04-09-move-operations/.openspec.yaml +0 -2
  69. package/openspec/changes/archive/2026-04-09-move-operations/design.md +0 -43
  70. package/openspec/changes/archive/2026-04-09-move-operations/proposal.md +0 -25
  71. package/openspec/changes/archive/2026-04-09-move-operations/specs/move-operations/spec.md +0 -41
  72. package/openspec/changes/archive/2026-04-09-move-operations/tasks.md +0 -40
  73. package/openspec/changes/archive/2026-04-09-project-crud/.openspec.yaml +0 -2
  74. package/openspec/changes/archive/2026-04-09-project-crud/design.md +0 -60
  75. package/openspec/changes/archive/2026-04-09-project-crud/proposal.md +0 -29
  76. package/openspec/changes/archive/2026-04-09-project-crud/specs/project-write/spec.md +0 -74
  77. package/openspec/changes/archive/2026-04-09-project-crud/tasks.md +0 -48
  78. package/openspec/changes/archive/2026-04-09-project-filtering/.openspec.yaml +0 -2
  79. package/openspec/changes/archive/2026-04-09-project-filtering/design.md +0 -52
  80. package/openspec/changes/archive/2026-04-09-project-filtering/proposal.md +0 -26
  81. package/openspec/changes/archive/2026-04-09-project-filtering/specs/project-filtering/spec.md +0 -66
  82. package/openspec/changes/archive/2026-04-09-project-filtering/specs/project-management/spec.md +0 -13
  83. package/openspec/changes/archive/2026-04-09-project-filtering/tasks.md +0 -41
  84. package/openspec/changes/archive/2026-04-09-tag-crud/.openspec.yaml +0 -2
  85. package/openspec/changes/archive/2026-04-09-tag-crud/design.md +0 -45
  86. package/openspec/changes/archive/2026-04-09-tag-crud/proposal.md +0 -28
  87. package/openspec/changes/archive/2026-04-09-tag-crud/specs/tag-write/spec.md +0 -49
  88. package/openspec/changes/archive/2026-04-09-tag-crud/tasks.md +0 -41
  89. package/openspec/changes/archive/2026-04-09-task-crud/.openspec.yaml +0 -2
  90. package/openspec/changes/archive/2026-04-09-task-crud/design.md +0 -62
  91. package/openspec/changes/archive/2026-04-09-task-crud/proposal.md +0 -29
  92. package/openspec/changes/archive/2026-04-09-task-crud/specs/task-management/spec.md +0 -17
  93. package/openspec/changes/archive/2026-04-09-task-crud/specs/task-write/spec.md +0 -89
  94. package/openspec/changes/archive/2026-04-09-task-crud/tasks.md +0 -55
  95. package/openspec/changes/archive/2026-04-09-task-filtering/.openspec.yaml +0 -2
  96. package/openspec/changes/archive/2026-04-09-task-filtering/design.md +0 -61
  97. package/openspec/changes/archive/2026-04-09-task-filtering/proposal.md +0 -26
  98. package/openspec/changes/archive/2026-04-09-task-filtering/specs/task-filtering/spec.md +0 -63
  99. package/openspec/changes/archive/2026-04-09-task-filtering/specs/task-management/spec.md +0 -17
  100. package/openspec/changes/archive/2026-04-09-task-filtering/tasks.md +0 -42
  101. package/openspec/changes/archive/2026-04-10-planned-date/.openspec.yaml +0 -2
  102. package/openspec/changes/archive/2026-04-10-planned-date/design.md +0 -27
  103. package/openspec/changes/archive/2026-04-10-planned-date/proposal.md +0 -29
  104. package/openspec/changes/archive/2026-04-10-planned-date/specs/task-management/spec.md +0 -29
  105. package/openspec/changes/archive/2026-04-10-planned-date/specs/task-write/spec.md +0 -69
  106. package/openspec/changes/archive/2026-04-10-planned-date/tasks.md +0 -26
  107. package/openspec/changes/archive/2026-04-10-task-recurrence/.openspec.yaml +0 -2
  108. package/openspec/changes/archive/2026-04-10-task-recurrence/design.md +0 -81
  109. package/openspec/changes/archive/2026-04-10-task-recurrence/proposal.md +0 -28
  110. package/openspec/changes/archive/2026-04-10-task-recurrence/specs/recurrence/spec.md +0 -47
  111. package/openspec/changes/archive/2026-04-10-task-recurrence/specs/task-management/spec.md +0 -25
  112. package/openspec/changes/archive/2026-04-10-task-recurrence/specs/task-write/spec.md +0 -61
  113. package/openspec/changes/archive/2026-04-10-task-recurrence/tasks.md +0 -39
  114. package/openspec/config.yaml +0 -20
  115. package/openspec/specs/attachments/spec.md +0 -15
  116. package/openspec/specs/batch-operations/spec.md +0 -15
  117. package/openspec/specs/database-inspection/spec.md +0 -15
  118. package/openspec/specs/execution-runtime/spec.md +0 -75
  119. package/openspec/specs/folder-management/spec.md +0 -39
  120. package/openspec/specs/folder-write/spec.md +0 -45
  121. package/openspec/specs/forecast/spec.md +0 -15
  122. package/openspec/specs/identity-resolution/spec.md +0 -51
  123. package/openspec/specs/move-operations/spec.md +0 -41
  124. package/openspec/specs/perspective-management/spec.md +0 -15
  125. package/openspec/specs/project-filtering/spec.md +0 -72
  126. package/openspec/specs/project-management/spec.md +0 -31
  127. package/openspec/specs/project-write/spec.md +0 -79
  128. package/openspec/specs/recurrence/spec.md +0 -51
  129. package/openspec/specs/settings/spec.md +0 -15
  130. package/openspec/specs/tag-management/spec.md +0 -39
  131. package/openspec/specs/tag-write/spec.md +0 -49
  132. package/openspec/specs/task-filtering/spec.md +0 -63
  133. package/openspec/specs/task-management/spec.md +0 -51
  134. package/openspec/specs/task-write/spec.md +0 -115
  135. package/openspec/specs/url-automation/spec.md +0 -15
  136. package/openspec/specs/window-state/spec.md +0 -15
  137. package/scripts/cleanup-fixtures.ts +0 -89
  138. package/server.json +0 -21
  139. package/src/runtime/bridge.ts +0 -97
  140. package/src/runtime/index.ts +0 -4
  141. package/src/runtime/jxaShim.ts +0 -55
  142. package/src/runtime/resultProtocol.ts +0 -62
  143. package/src/runtime/snippetLoader.ts +0 -79
  144. package/src/schemas/enums.ts +0 -32
  145. package/src/schemas/index.ts +0 -38
  146. package/src/schemas/shapes.ts +0 -267
  147. package/src/server.ts +0 -58
  148. package/src/tools/completeProject.ts +0 -21
  149. package/src/tools/completeTask.ts +0 -23
  150. package/src/tools/createFolder.ts +0 -20
  151. package/src/tools/createProject.ts +0 -20
  152. package/src/tools/createTag.ts +0 -20
  153. package/src/tools/createTask.ts +0 -20
  154. package/src/tools/deleteFolder.ts +0 -24
  155. package/src/tools/deleteProject.ts +0 -24
  156. package/src/tools/deleteTag.ts +0 -24
  157. package/src/tools/deleteTask.ts +0 -26
  158. package/src/tools/dropProject.ts +0 -21
  159. package/src/tools/dropTask.ts +0 -23
  160. package/src/tools/editFolder.ts +0 -19
  161. package/src/tools/editProject.ts +0 -20
  162. package/src/tools/editTag.ts +0 -20
  163. package/src/tools/editTask.ts +0 -20
  164. package/src/tools/getFolder.ts +0 -24
  165. package/src/tools/getProject.ts +0 -24
  166. package/src/tools/getTag.ts +0 -24
  167. package/src/tools/getTask.ts +0 -24
  168. package/src/tools/index.ts +0 -85
  169. package/src/tools/listFolders.ts +0 -32
  170. package/src/tools/listProjects.ts +0 -32
  171. package/src/tools/listTags.ts +0 -32
  172. package/src/tools/listTasks.ts +0 -56
  173. package/src/tools/moveProject.ts +0 -20
  174. package/src/tools/moveTask.ts +0 -20
  175. package/src/tools/resolveName.ts +0 -37
  176. package/test/integration/.gitkeep +0 -0
  177. package/test/integration/completeProject.int.test.ts +0 -25
  178. package/test/integration/completeTask.int.test.ts +0 -30
  179. package/test/integration/createFolder.int.test.ts +0 -50
  180. package/test/integration/createProject.int.test.ts +0 -49
  181. package/test/integration/createTag.int.test.ts +0 -52
  182. package/test/integration/createTask.int.test.ts +0 -55
  183. package/test/integration/deleteFolder.int.test.ts +0 -64
  184. package/test/integration/deleteProject.int.test.ts +0 -31
  185. package/test/integration/deleteTag.int.test.ts +0 -61
  186. package/test/integration/deleteTask.int.test.ts +0 -36
  187. package/test/integration/dropProject.int.test.ts +0 -24
  188. package/test/integration/dropTask.int.test.ts +0 -29
  189. package/test/integration/editFolder.int.test.ts +0 -43
  190. package/test/integration/editProject.int.test.ts +0 -39
  191. package/test/integration/editTag.int.test.ts +0 -43
  192. package/test/integration/editTask.int.test.ts +0 -56
  193. package/test/integration/fixtures.ts +0 -219
  194. package/test/integration/getTask.int.test.ts +0 -64
  195. package/test/integration/listFoldersFiltered.int.test.ts +0 -98
  196. package/test/integration/listProjects.int.test.ts +0 -73
  197. package/test/integration/listProjectsFiltered.int.test.ts +0 -96
  198. package/test/integration/listTagsFiltered.int.test.ts +0 -54
  199. package/test/integration/listTasksFiltered.int.test.ts +0 -141
  200. package/test/integration/moveProject.int.test.ts +0 -57
  201. package/test/integration/moveTask.int.test.ts +0 -61
  202. package/test/integration/plannedDate.int.test.ts +0 -72
  203. package/test/integration/preflight.ts +0 -60
  204. package/test/integration/resolveName.int.test.ts +0 -86
  205. package/test/integration/taskRecurrence.int.test.ts +0 -106
  206. package/test/unit/.gitkeep +0 -0
  207. package/test/unit/bridge.injection.test.ts +0 -66
  208. package/test/unit/resultProtocol.test.ts +0 -71
  209. package/test/unit/schemas.createFolder.test.ts +0 -38
  210. package/test/unit/schemas.createProject.test.ts +0 -115
  211. package/test/unit/schemas.createTask.test.ts +0 -87
  212. package/test/unit/schemas.editTag.test.ts +0 -64
  213. package/test/unit/schemas.folderTagFiltering.test.ts +0 -42
  214. package/test/unit/schemas.listProjects.test.ts +0 -44
  215. package/test/unit/schemas.moveOperations.test.ts +0 -60
  216. package/test/unit/schemas.recurrence.test.ts +0 -120
  217. package/test/unit/schemas.test.ts +0 -126
  218. package/test/unit/snippetLoader.test.ts +0 -56
  219. package/test/unit/tools.deleteTask.test.ts +0 -19
  220. package/test/unit/tools.listTasks.test.ts +0 -126
  221. package/tsconfig.json +0 -19
  222. package/vitest.config.ts +0 -8
  223. package/vitest.integration.config.ts +0 -18
@@ -1,66 +0,0 @@
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`
@@ -1,13 +0,0 @@
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"`
@@ -1,41 +0,0 @@
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
@@ -1,2 +0,0 @@
1
- schema: spec-driven
2
- created: 2026-04-09
@@ -1,45 +0,0 @@
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).
@@ -1,28 +0,0 @@
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)
@@ -1,49 +0,0 @@
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
@@ -1,41 +0,0 @@
1
- ## 1. Snippets (src/snippets/)
2
-
3
- - [x] 1.1 `create_tag.js`: accept `{name, parentTagId?}`; if `parentTagId` provided resolve parent tag by ID and throw `NotFoundError` if missing; create `new Tag(name)` or `new Tag(name, parentTag)`; return full tag detail envelope
4
- - [x] 1.2 `edit_tag.js`: accept `{id, name?, status?}`; resolve tag by ID; apply only provided fields; map `status` string to `Tag.Status` enum member; return updated full tag detail envelope
5
- - [x] 1.3 `delete_tag.js`: accept `{id}`; resolve tag by ID; call `deleteObject(tag)` (OmniFocus handles child tag and association cleanup automatically); return `{ok: true, data: {id}}`
6
-
7
- ## 2. Snippet allowlist
8
-
9
- - [x] 2.1 Add `create_tag`, `edit_tag`, `delete_tag` to `ALLOWED_SNIPPETS` in `src/runtime/snippetLoader.ts`
10
-
11
- ## 3. Schemas (src/schemas/shapes.ts)
12
-
13
- - [x] 3.1 Define `CreateTagInput` zod schema: `{name: z.string().min(1), parentTagId: IdSchema.optional()}`
14
- - [x] 3.2 Define `EditTagInput` zod schema: `{id: IdSchema, name: z.string().min(1).optional(), status: TagStatus.optional()}`; add `.refine()` requiring at least one of `name` or `status`
15
-
16
- ## 4. Tool handlers (src/tools/)
17
-
18
- - [x] 4.1 `createTag.ts`: validate input with `CreateTagInput`; invoke `runSnippet("create_tag", args)`; validate result against `TagDetail`; return
19
- - [x] 4.2 `editTag.ts`: validate input with `EditTagInput`; invoke `runSnippet("edit_tag", args)`; validate result against `TagDetail`; return
20
- - [x] 4.3 `deleteTag.ts`: input `{id: IdSchema}`; tool description MUST state the AI should confirm with the user before invoking and note that child tags are also permanently deleted; invoke `runSnippet("delete_tag", {id})`; return confirmation
21
-
22
- ## 5. Server registration
23
-
24
- - [x] 5.1 Export all three new tool definitions from `src/tools/index.ts`
25
-
26
- ## 6. Unit tests (test/unit/)
27
-
28
- - [x] 6.1 `schemas.editTag.test.ts`: valid inputs pass; empty object (neither name nor status) rejected by refine; invalid status enum rejected
29
-
30
- ## 7. Integration tests (test/integration/)
31
-
32
- - [x] 7.1 `createTag.int.test.ts`: create top-level tag; create child tag; verify `path`, `parentId`, `childTagIds`
33
- - [x] 7.2 `editTag.int.test.ts`: rename a tag; put tag on hold; verify status and name
34
- - [x] 7.3 `deleteTag.int.test.ts`: delete a tag; verify subsequent `get_tag` returns not-found; verify child tags also gone
35
-
36
- ## 8. Verification
37
-
38
- - [x] 8.1 `npm run typecheck` clean
39
- - [x] 8.2 `npm test` (unit suite) clean
40
- - [x] 8.3 Manually run integration suite; verify fixture cleanup
41
- - [ ] 8.4 Connect to Claude Desktop; exercise all three tools against a real database
@@ -1,2 +0,0 @@
1
- schema: spec-driven
2
- created: 2026-04-09
@@ -1,62 +0,0 @@
1
- ## Context
2
-
3
- The bootstrap change established the JXA→OmniJS bridge, snippet pattern, and result protocol. All five write tools follow those same patterns. The key question for writes is: what does OmniJS expose for creating and mutating tasks, and what are the edge cases?
4
-
5
- OmniJS task write operations:
6
- - `new Task(name, position)` — creates a task; `position` is a `Project` or `Task` (for subtasks)
7
- - `task.name = "..."` — property assignment for most scalar fields
8
- - `task.markComplete()` — dedicated method (not property assignment)
9
- - `task.drop()` — dedicated method
10
- - `deleteObject(task)` — permanent deletion
11
- - Tags: `task.addTag(tag)` / `task.removeTag(tag)` — manipulate by Tag object, not ID
12
-
13
- ## Goals / Non-Goals
14
-
15
- **Goals:**
16
- - Expose create, edit, complete, drop, and delete for tasks
17
- - Support inbox tasks (no project), project tasks, and subtasks
18
- - Edit any subset of fields in one call (fat edit — no single-field tools)
19
- - Permanently delete with a tool description that instructs the AI to confirm before invoking
20
-
21
- **Non-Goals:**
22
- - Moving tasks between projects/containers (separate `move-operations` change)
23
- - Recurrence rules (separate `recurrence` change)
24
- - Batch operations (separate change)
25
-
26
- ## Decisions
27
-
28
- ### Decision 1: Tag manipulation by ID, resolved inside the snippet
29
-
30
- Tags are assigned via `task.addTag(tagObject)` / `task.removeTag(tagObject)`, which require a live `Tag` object. The snippet will receive tag IDs and resolve them via `flattenedTags.find(t => t.id.primaryKey === id)` inline. This keeps the bridge contract clean (IDs only) without requiring a separate resolution round-trip.
31
-
32
- **Alternative considered:** Accept tag names. Rejected — names are ambiguous; IDs are stable.
33
-
34
- ### Decision 2: `edit_task` replaces tags, not merges
35
-
36
- When `tagIds` is provided to `edit_task`, the snippet replaces the task's full tag set (remove all, add new). When `tagIds` is omitted, tags are untouched. This is the least surprising contract: callers who want to add one tag still need to pass the full desired set, but they can read it from `get_task` first.
37
-
38
- **Alternative considered:** Separate `add_tags` / `remove_tags` fields. Rejected — more surface area with no material benefit given that `get_task` is cheap.
39
-
40
- ### Decision 3: `create_task` placement via optional `projectId` and `parentTaskId`
41
-
42
- - Both omitted → inbox
43
- - `projectId` only → task added to project root
44
- - `parentTaskId` only → subtask; inherits project from parent
45
- - Both provided → error (ambiguous placement; caller must choose)
46
-
47
- This mirrors how OmniFocus itself treats task placement.
48
-
49
- ### Decision 4: `complete_task` and `drop_task` as separate tools from `edit_task`
50
-
51
- Status transitions in OmniJS require method calls (`markComplete()`, `drop()`), not property assignment. Exposing them as dedicated tools makes the intent unambiguous and avoids a complex status-to-method dispatch inside `edit_task`. It also makes tool descriptions cleaner for the LLM.
52
-
53
- ### Decision 5: `delete_task` tool description as the confirmation guardrail
54
-
55
- The server executes `deleteObject(task)` unconditionally when called. The confirmation requirement is enforced through the MCP tool description, which instructs the AI to ask the user explicitly before invoking this tool. This is consistent with how MCP tool descriptions guide AI behavior.
56
-
57
- ## Risks / Trade-offs
58
-
59
- - **Partial edit with bad field value** → OmniJS will throw; the error envelope propagates back as an `ExecutionError`. No partial writes occur because OmniJS is synchronous within `evaluateJavascript`.
60
- - **Tag ID not found during edit** → Snippet should throw a descriptive `NotFoundError` for the missing tag ID, not silently skip it.
61
- - **Inbox placement** → `new Task(name, inbox)` requires passing the `inbox` object. In OmniJS the inbox is accessible as `inbox` (bare global). Verified pattern from the read-side exploration.
62
- - **deleteObject on a task with subtasks** → OmniJS deletes the task and all subtasks. Tool description should note this.
@@ -1,29 +0,0 @@
1
- ## Why
2
-
3
- The bootstrap change delivered read-only task access. Callers can observe tasks but cannot create, modify, complete, drop, or delete them — making the server unsuitable for any workflow that involves acting on tasks, not just reading them.
4
-
5
- ## What Changes
6
-
7
- - Add `create_task` tool: create a task in a project, as a subtask, or in the inbox
8
- - Add `edit_task` tool: modify any subset of a task's scalar fields and tag assignments in a single call
9
- - Add `complete_task` tool: mark a task complete
10
- - Add `drop_task` tool: mark a task dropped
11
- - Add `delete_task` tool: permanently delete a task (tool description instructs the AI to confirm with the user before invoking)
12
-
13
- ## Capabilities
14
-
15
- ### New Capabilities
16
-
17
- - `task-write`: Create, edit, complete, drop, and permanently delete OmniFocus tasks
18
-
19
- ### Modified Capabilities
20
-
21
- - `task-management`: Extend existing spec to add write requirements alongside the existing read requirements
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)
@@ -1,17 +0,0 @@
1
- ## MODIFIED Requirements
2
-
3
- ### Requirement: Get task by ID
4
-
5
- The system SHALL provide a `get_task` tool that accepts `{id: string}` and returns the full detail record of the named task, including `{id, name, note, status, flagged, deferDate, dueDate, completionDate, estimatedMinutes, containerId, containerType, tagIds, parentTaskId}`. The `parentTaskId` field SHALL be `null` for top-level tasks and SHALL contain the parent task's `id.primaryKey` for subtasks. If no task exists with that ID, the tool SHALL return a structured not-found error.
6
-
7
- #### Scenario: Existing task returns full detail
8
- - **WHEN** `get_task` is called with the ID of an existing task
9
- - **THEN** the tool returns the task's full detail record including all scalar fields, the list of tag IDs assigned to it, and `parentTaskId`
10
-
11
- #### Scenario: Subtask includes parentTaskId
12
- - **WHEN** `get_task` is called with the ID of a subtask
13
- - **THEN** the returned record includes `parentTaskId` set to the parent task's stable ID
14
-
15
- #### Scenario: Missing task returns not-found error
16
- - **WHEN** `get_task` is called with an ID that does not correspond to any task
17
- - **THEN** the tool returns a structured error with a not-found code and does not throw an unhandled exception
@@ -1,89 +0,0 @@
1
- ## ADDED Requirements
2
-
3
- ### Requirement: Create task
4
-
5
- The system SHALL provide a `create_task` tool that creates a new OmniFocus task and returns its full detail record. The tool SHALL accept `{name: string, note?: string, flagged?: boolean, deferDate?: string, dueDate?: string, estimatedMinutes?: number, projectId?: string, parentTaskId?: string, tagIds?: string[]}`. Placement SHALL be determined as follows: if both `projectId` and `parentTaskId` are provided the tool SHALL return an error; if only `projectId` is provided the task is placed at the project root; if only `parentTaskId` is provided the task is created as a subtask inheriting its parent's project; if neither is provided the task is placed in the inbox.
6
-
7
- #### Scenario: Create inbox task
8
- - **WHEN** `create_task` is called with `{name: "Buy milk"}` and no `projectId` or `parentTaskId`
9
- - **THEN** the tool creates the task in the OmniFocus inbox and returns its full detail record including a stable `id`
10
-
11
- #### Scenario: Create task in a project
12
- - **WHEN** `create_task` is called with `{name: "Write tests", projectId: "abc123"}`
13
- - **THEN** the tool creates the task at the root of the specified project and returns its full detail record
14
-
15
- #### Scenario: Create subtask
16
- - **WHEN** `create_task` is called with `{name: "Review PR", parentTaskId: "xyz789"}`
17
- - **THEN** the tool creates the task as a child of the specified parent task and returns its full detail record
18
-
19
- #### Scenario: Ambiguous placement is rejected
20
- - **WHEN** `create_task` is called with both `projectId` and `parentTaskId`
21
- - **THEN** the tool returns a validation error before any snippet executes
22
-
23
- #### Scenario: Non-existent project returns not-found error
24
- - **WHEN** `create_task` is called with a `projectId` that does not correspond to any project
25
- - **THEN** the tool returns a structured not-found error
26
-
27
- ### Requirement: Edit task
28
-
29
- The system SHALL provide an `edit_task` tool that modifies an existing task and returns its updated full detail record. The tool SHALL accept `{id: string}` plus any subset of `{name?: string, note?: string, flagged?: boolean, deferDate?: string | null, dueDate?: string | null, estimatedMinutes?: number | null, tagIds?: string[]}`. Fields omitted from the call SHALL be left unchanged. When `tagIds` is provided it SHALL replace the task's entire tag set; when omitted, tags SHALL be unchanged. Passing `null` for a date or `estimatedMinutes` SHALL clear the field.
30
-
31
- #### Scenario: Edit a single field
32
- - **WHEN** `edit_task` is called with `{id: "abc123", flagged: true}`
33
- - **THEN** only the `flagged` field is changed; all other fields retain their previous values
34
-
35
- #### Scenario: Replace tag set
36
- - **WHEN** `edit_task` is called with `{id: "abc123", tagIds: ["t1", "t2"]}`
37
- - **THEN** the task's tags are set to exactly `["t1", "t2"]`, replacing any previously assigned tags
38
-
39
- #### Scenario: Clear a date field
40
- - **WHEN** `edit_task` is called with `{id: "abc123", dueDate: null}`
41
- - **THEN** the task's due date is cleared
42
-
43
- #### Scenario: Non-existent task returns not-found error
44
- - **WHEN** `edit_task` is called with an ID that does not correspond to any task
45
- - **THEN** the tool returns a structured not-found error
46
-
47
- #### Scenario: Non-existent tag ID returns not-found error
48
- - **WHEN** `edit_task` is called with a `tagIds` array containing an ID that does not correspond to any tag
49
- - **THEN** the tool returns a structured not-found error and the task is not modified
50
-
51
- ### Requirement: Complete task
52
-
53
- The system SHALL provide a `complete_task` tool that marks an existing task complete using OmniJS `markComplete()` and returns the task's updated full detail record.
54
-
55
- #### Scenario: Complete an existing task
56
- - **WHEN** `complete_task` is called with the ID of an available task
57
- - **THEN** the task's status becomes `"complete"` and the tool returns the updated detail record
58
-
59
- #### Scenario: Non-existent task returns not-found error
60
- - **WHEN** `complete_task` is called with an ID that does not correspond to any task
61
- - **THEN** the tool returns a structured not-found error
62
-
63
- ### Requirement: Drop task
64
-
65
- The system SHALL provide a `drop_task` tool that marks an existing task dropped using OmniJS `drop()` and returns the task's updated full detail record.
66
-
67
- #### Scenario: Drop an existing task
68
- - **WHEN** `drop_task` is called with the ID of an available task
69
- - **THEN** the task's status becomes `"dropped"` and the tool returns the updated detail record
70
-
71
- #### Scenario: Non-existent task returns not-found error
72
- - **WHEN** `drop_task` is called with an ID that does not correspond to any task
73
- - **THEN** the tool returns a structured not-found error
74
-
75
- ### Requirement: Delete task
76
-
77
- The system SHALL provide a `delete_task` tool that permanently deletes a task and all its subtasks using OmniJS `deleteObject()`. The tool description SHALL instruct the AI to confirm with the user before invoking this tool, noting that deletion is permanent and includes all subtasks.
78
-
79
- #### Scenario: Delete an existing task
80
- - **WHEN** `delete_task` is called with the ID of an existing task
81
- - **THEN** the task and all its subtasks are permanently removed from OmniFocus and the tool returns a confirmation envelope
82
-
83
- #### Scenario: Delete task with subtasks removes all children
84
- - **WHEN** `delete_task` is called with the ID of a task that has subtasks
85
- - **THEN** the task and all descendant subtasks are deleted
86
-
87
- #### Scenario: Non-existent task returns not-found error
88
- - **WHEN** `delete_task` is called with an ID that does not correspond to any task
89
- - **THEN** the tool returns a structured not-found error
@@ -1,55 +0,0 @@
1
- ## 1. Snippets (src/snippets/)
2
-
3
- - [x] 1.1 `create_task.js`: accept `{name, note?, flagged?, deferDate?, dueDate?, estimatedMinutes?, projectId?, parentTaskId?, tagIds?}`; resolve placement (inbox / project root / subtask); throw `ConflictError` if both `projectId` and `parentTaskId` provided; throw `NotFoundError` on missing project, parent task, or tag ID; return full task detail envelope
4
- - [x] 1.2 `edit_task.js`: accept `{id, name?, note?, flagged?, deferDate?, dueDate?, estimatedMinutes?, tagIds?}`; resolve task by ID; apply only provided fields; when `tagIds` present replace full tag set (remove all, add each by ID); pass `null` dates/estimatedMinutes to clear; return updated full detail envelope
5
- - [x] 1.3 `complete_task.js`: accept `{id}`; resolve task by ID; call `task.markComplete()`; return updated full detail envelope
6
- - [x] 1.4 `drop_task.js`: accept `{id}`; resolve task by ID; call `task.drop()`; return updated full detail envelope
7
- - [x] 1.5 `delete_task.js`: accept `{id}`; resolve task by ID; call `deleteObject(task)`; return `{ok: true, data: {id}}`
8
-
9
- ## 2. Snippet allowlist
10
-
11
- - [x] 2.1 Add `create_task`, `edit_task`, `complete_task`, `drop_task`, `delete_task` to `ALLOWED_SNIPPETS` in `src/runtime/snippetLoader.ts`
12
-
13
- ## 3. Schemas (src/schemas/shapes.ts)
14
-
15
- - [x] 3.1 Add `parentTaskId: z.string().nullable()` to `TaskDetail` schema
16
- - [x] 3.2 Define `CreateTaskInput` zod schema with all optional fields and the mutual-exclusion refinement for `projectId` + `parentTaskId`
17
- - [x] 3.3 Define `EditTaskInput` zod schema — `id` required, all other fields optional; date fields accept `string | null`; `estimatedMinutes` accepts `number | null`
18
-
19
- ## 4. Tool handlers (src/tools/)
20
-
21
- - [x] 4.1 `createTask.ts`: validate input with `CreateTaskInput`; invoke `runSnippet("create_task", args)`; validate result against `TaskDetail`; return
22
- - [x] 4.2 `editTask.ts`: validate input with `EditTaskInput`; invoke `runSnippet("edit_task", args)`; validate result against `TaskDetail`; return
23
- - [x] 4.3 `completeTask.ts`: input `{id: IdSchema}`; invoke `runSnippet("complete_task", {id})`; validate result against `TaskDetail`; return
24
- - [x] 4.4 `dropTask.ts`: input `{id: IdSchema}`; invoke `runSnippet("drop_task", {id})`; validate result against `TaskDetail`; return
25
- - [x] 4.5 `deleteTask.ts`: input `{id: IdSchema}`; tool description MUST state the AI should confirm with the user before invoking and note that deletion is permanent and includes all subtasks; invoke `runSnippet("delete_task", {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
- - [x] 5.2 Verify `src/server.ts` picks them up via the `allTools` barrel (no change needed if barrel is already dynamic)
31
-
32
- ## 6. get_task update
33
-
34
- - [x] 6.1 Update `src/snippets/get_task.js` to include `parentTaskId` in the returned detail (`t.parentTask ? t.parentTask.id.primaryKey : null`)
35
- - [x] 6.2 Update `src/tools/getTask.ts` to validate against the updated `TaskDetail` schema
36
-
37
- ## 7. Unit tests (test/unit/)
38
-
39
- - [x] 7.1 `schemas.createTask.test.ts`: valid inputs pass; both `projectId` + `parentTaskId` rejected; missing name rejected; null dates accepted in edit schema
40
- - [x] 7.2 `tools.deleteTask.test.ts`: verify tool description contains confirmation language
41
-
42
- ## 8. Integration tests (test/integration/)
43
-
44
- - [x] 8.1 `createTask.int.test.ts`: create inbox task, verify returned ID; create project task, verify containerId; create subtask, verify parentTaskId
45
- - [x] 8.2 `editTask.int.test.ts`: edit name; edit flags; replace tag set; clear due date
46
- - [x] 8.3 `completeTask.int.test.ts`: complete a task, verify status = `"complete"`
47
- - [x] 8.4 `dropTask.int.test.ts`: drop a task, verify status = `"dropped"`
48
- - [x] 8.5 `deleteTask.int.test.ts`: delete a task, verify subsequent `get_task` returns not-found
49
-
50
- ## 9. Verification
51
-
52
- - [x] 9.1 `npm run typecheck` clean
53
- - [x] 9.2 `npm test` (unit suite) clean
54
- - [x] 9.3 Manually run integration suite; verify fixture cleanup
55
- - [ ] 9.4 Connect to Claude Desktop; exercise all five tools against a real database
@@ -1,2 +0,0 @@
1
- schema: spec-driven
2
- created: 2026-04-09
@@ -1,61 +0,0 @@
1
- ## Context
2
-
3
- `list_tasks` currently fetches all tasks in scope and returns a flat `TaskSummary` array with no server-side filtering. For large databases, `scope: { all: true }` can return thousands of tasks — far too many for an LLM to reason over. Filtering must happen inside the OmniJS snippet before data crosses the JXA bridge; post-hoc filtering in TypeScript would still pay the full serialization cost.
4
-
5
- `TaskSummary` currently omits `dueDate` and `tagIds`. Without these, filtered results are opaque: if the LLM asks "what's due today?" it can't present due dates in its response without issuing a `get_task` call per result.
6
-
7
- ## Goals / Non-Goals
8
-
9
- **Goals:**
10
- - Filter tasks by `flagged`, `status[]`, `tagId`, and `dueBeforeDate` inside the snippet
11
- - Enrich `TaskSummary` with `dueDate` and `tagIds`
12
- - Change default behavior: exclude `complete` and `dropped` tasks when no `status` filter is provided
13
- - Cap results with a configurable `limit` (default 200)
14
-
15
- **Non-Goals:**
16
- - Multi-tag filtering with AND/OR semantics (future work)
17
- - Full-text search on task name or note
18
- - Pagination / cursor-based continuation (hard cap only)
19
- - Filtering `list_projects` (separate change)
20
-
21
- ## Decisions
22
-
23
- ### Decision 1: Filter in snippet, not TypeScript
24
-
25
- All filter logic runs inside `evaluateJavascript`. The TypeScript layer passes filter args through and validates the schema. This avoids deserializing thousands of tasks only to discard most of them.
26
-
27
- Alternative considered: filter in TS. Rejected because the JXA serialization cost is paid per-task regardless — a 5000-task DB would serialize all 5000 even if only 10 match.
28
-
29
- ### Decision 2: Enrich TaskSummary with dueDate and tagIds
30
-
31
- `TaskSummary` gains two fields:
32
- - `dueDate: string | null` — ISO datetime
33
- - `tagIds: string[]`
34
-
35
- These are the fields most likely to appear in filtering queries and most useful to display in results. Other detail fields (`note`, `deferDate`, `estimatedMinutes`) remain in `TaskDetail` only.
36
-
37
- This is a non-breaking addition to the shape — existing consumers receive new optional fields.
38
-
39
- ### Decision 3: Default excludes complete and dropped
40
-
41
- When `filter.status` is omitted, the snippet filters out `Task.Status.Completed` and `Task.Status.Dropped`. This is a breaking behavior change but the correct default for LLM-driven queries. The previous default (return everything) was only useful for bulk inspection, which can still be achieved with `status: ["available", "blocked", "dueSoon", "next", "overdue", "complete", "dropped"]`.
42
-
43
- ### Decision 4: Status filter as array, tag filter as single ID
44
-
45
- `status` accepts an array of `TaskStatus` values. This lets callers express compound queries ("available OR overdue") in a single call.
46
-
47
- `tagId` accepts a single ID — the 80% use case. Multi-tag AND/OR filtering adds significant complexity for uncertain benefit in v1.
48
-
49
- ### Decision 5: dueBeforeDate is inclusive
50
-
51
- `dueBeforeDate` matches tasks where `task.dueDate <= dueBeforeDate`. "Due before end of today" is expressed as the end-of-day ISO timestamp. Null due dates never match.
52
-
53
- ### Decision 6: limit default 200, passed as snippet arg
54
-
55
- The limit is enforced inside the snippet (slice after filtering) so the cap applies before serialization. Default 200. Callers may increase it up to any value — there is no server-side maximum, just the JXA timeout.
56
-
57
- ## Risks / Trade-offs
58
-
59
- - **Breaking default behavior** — consumers that relied on `list_tasks` returning complete tasks will see a behavior change. Risk is low: this is a local MCP server with a single consumer (Claude Desktop), and the new default is strictly more useful.
60
- - **tagId filter requires flattenedTags lookup** — to check `task.tags`, OmniJS traverses the tag tree. On databases with thousands of tags this is fast in practice but not free.
61
- - **limit does not paginate** — if a user has 500 overdue tasks and limit is 200, they see the first 200 with no way to retrieve the rest. Acceptable for v1; pagination is future work.