@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.
Files changed (387) hide show
  1. package/.claude/commands/opsx/apply.md +152 -0
  2. package/.claude/commands/opsx/archive.md +157 -0
  3. package/.claude/commands/opsx/bulk-archive.md +242 -0
  4. package/.claude/commands/opsx/continue.md +114 -0
  5. package/.claude/commands/opsx/explore.md +173 -0
  6. package/.claude/commands/opsx/ff.md +97 -0
  7. package/.claude/commands/opsx/new.md +69 -0
  8. package/.claude/commands/opsx/onboard.md +550 -0
  9. package/.claude/commands/opsx/propose.md +106 -0
  10. package/.claude/commands/opsx/sync.md +134 -0
  11. package/.claude/commands/opsx/verify.md +164 -0
  12. package/.claude/skills/openspec-apply-change/SKILL.md +156 -0
  13. package/.claude/skills/openspec-archive-change/SKILL.md +114 -0
  14. package/.claude/skills/openspec-bulk-archive-change/SKILL.md +246 -0
  15. package/.claude/skills/openspec-continue-change/SKILL.md +118 -0
  16. package/.claude/skills/openspec-explore/SKILL.md +288 -0
  17. package/.claude/skills/openspec-ff-change/SKILL.md +101 -0
  18. package/.claude/skills/openspec-new-change/SKILL.md +74 -0
  19. package/.claude/skills/openspec-onboard/SKILL.md +554 -0
  20. package/.claude/skills/openspec-propose/SKILL.md +110 -0
  21. package/.claude/skills/openspec-sync-specs/SKILL.md +138 -0
  22. package/.claude/skills/openspec-verify-change/SKILL.md +168 -0
  23. package/CONTRIBUTING.md +83 -0
  24. package/LICENSE +21 -0
  25. package/README.md +198 -0
  26. package/dist/runtime/bridge.d.ts +16 -0
  27. package/dist/runtime/bridge.d.ts.map +1 -0
  28. package/dist/runtime/bridge.js +76 -0
  29. package/dist/runtime/bridge.js.map +1 -0
  30. package/dist/runtime/index.d.ts +5 -0
  31. package/dist/runtime/index.d.ts.map +1 -0
  32. package/dist/runtime/index.js +5 -0
  33. package/dist/runtime/index.js.map +1 -0
  34. package/dist/runtime/jxaShim.d.ts +21 -0
  35. package/dist/runtime/jxaShim.d.ts.map +1 -0
  36. package/dist/runtime/jxaShim.js +55 -0
  37. package/dist/runtime/jxaShim.js.map +1 -0
  38. package/dist/runtime/resultProtocol.d.ts +66 -0
  39. package/dist/runtime/resultProtocol.d.ts.map +1 -0
  40. package/dist/runtime/resultProtocol.js +52 -0
  41. package/dist/runtime/resultProtocol.js.map +1 -0
  42. package/dist/runtime/snippetLoader.d.ts +4 -0
  43. package/dist/runtime/snippetLoader.d.ts.map +1 -0
  44. package/dist/runtime/snippetLoader.js +68 -0
  45. package/dist/runtime/snippetLoader.js.map +1 -0
  46. package/dist/schemas/enums.d.ts +9 -0
  47. package/dist/schemas/enums.d.ts.map +1 -0
  48. package/dist/schemas/enums.js +26 -0
  49. package/dist/schemas/enums.js.map +1 -0
  50. package/dist/schemas/index.d.ts +3 -0
  51. package/dist/schemas/index.d.ts.map +1 -0
  52. package/dist/schemas/index.js +3 -0
  53. package/dist/schemas/index.js.map +1 -0
  54. package/dist/schemas/shapes.d.ts +726 -0
  55. package/dist/schemas/shapes.d.ts.map +1 -0
  56. package/dist/schemas/shapes.js +221 -0
  57. package/dist/schemas/shapes.js.map +1 -0
  58. package/dist/server.d.ts +2 -0
  59. package/dist/server.d.ts.map +1 -0
  60. package/dist/server.js +50 -0
  61. package/dist/server.js.map +1 -0
  62. package/dist/tools/completeProject.d.ts +24 -0
  63. package/dist/tools/completeProject.d.ts.map +1 -0
  64. package/dist/tools/completeProject.js +17 -0
  65. package/dist/tools/completeProject.js.map +1 -0
  66. package/dist/tools/completeTask.d.ts +25 -0
  67. package/dist/tools/completeTask.d.ts.map +1 -0
  68. package/dist/tools/completeTask.js +17 -0
  69. package/dist/tools/completeTask.js.map +1 -0
  70. package/dist/tools/createFolder.d.ts +20 -0
  71. package/dist/tools/createFolder.d.ts.map +1 -0
  72. package/dist/tools/createFolder.js +13 -0
  73. package/dist/tools/createFolder.js.map +1 -0
  74. package/dist/tools/createProject.d.ts +59 -0
  75. package/dist/tools/createProject.d.ts.map +1 -0
  76. package/dist/tools/createProject.js +13 -0
  77. package/dist/tools/createProject.js.map +1 -0
  78. package/dist/tools/createTag.d.ts +20 -0
  79. package/dist/tools/createTag.d.ts.map +1 -0
  80. package/dist/tools/createTag.js +13 -0
  81. package/dist/tools/createTag.js.map +1 -0
  82. package/dist/tools/createTask.d.ts +116 -0
  83. package/dist/tools/createTask.d.ts.map +1 -0
  84. package/dist/tools/createTask.js +13 -0
  85. package/dist/tools/createTask.js.map +1 -0
  86. package/dist/tools/deleteFolder.d.ts +30 -0
  87. package/dist/tools/deleteFolder.d.ts.map +1 -0
  88. package/dist/tools/deleteFolder.js +18 -0
  89. package/dist/tools/deleteFolder.js.map +1 -0
  90. package/dist/tools/deleteProject.d.ts +30 -0
  91. package/dist/tools/deleteProject.d.ts.map +1 -0
  92. package/dist/tools/deleteProject.js +18 -0
  93. package/dist/tools/deleteProject.js.map +1 -0
  94. package/dist/tools/deleteTag.d.ts +30 -0
  95. package/dist/tools/deleteTag.d.ts.map +1 -0
  96. package/dist/tools/deleteTag.js +18 -0
  97. package/dist/tools/deleteTag.js.map +1 -0
  98. package/dist/tools/deleteTask.d.ts +31 -0
  99. package/dist/tools/deleteTask.d.ts.map +1 -0
  100. package/dist/tools/deleteTask.js +18 -0
  101. package/dist/tools/deleteTask.js.map +1 -0
  102. package/dist/tools/dropProject.d.ts +24 -0
  103. package/dist/tools/dropProject.d.ts.map +1 -0
  104. package/dist/tools/dropProject.js +17 -0
  105. package/dist/tools/dropProject.js.map +1 -0
  106. package/dist/tools/dropTask.d.ts +25 -0
  107. package/dist/tools/dropTask.d.ts.map +1 -0
  108. package/dist/tools/dropTask.js +17 -0
  109. package/dist/tools/dropTask.js.map +1 -0
  110. package/dist/tools/editFolder.d.ts +20 -0
  111. package/dist/tools/editFolder.d.ts.map +1 -0
  112. package/dist/tools/editFolder.js +13 -0
  113. package/dist/tools/editFolder.js.map +1 -0
  114. package/dist/tools/editProject.d.ts +59 -0
  115. package/dist/tools/editProject.d.ts.map +1 -0
  116. package/dist/tools/editProject.js +13 -0
  117. package/dist/tools/editProject.js.map +1 -0
  118. package/dist/tools/editTag.d.ts +31 -0
  119. package/dist/tools/editTag.d.ts.map +1 -0
  120. package/dist/tools/editTag.js +13 -0
  121. package/dist/tools/editTag.js.map +1 -0
  122. package/dist/tools/editTask.d.ts +79 -0
  123. package/dist/tools/editTask.d.ts.map +1 -0
  124. package/dist/tools/editTask.js +13 -0
  125. package/dist/tools/editTask.js.map +1 -0
  126. package/dist/tools/getFolder.d.ts +24 -0
  127. package/dist/tools/getFolder.d.ts.map +1 -0
  128. package/dist/tools/getFolder.js +17 -0
  129. package/dist/tools/getFolder.js.map +1 -0
  130. package/dist/tools/getProject.d.ts +24 -0
  131. package/dist/tools/getProject.d.ts.map +1 -0
  132. package/dist/tools/getProject.js +17 -0
  133. package/dist/tools/getProject.js.map +1 -0
  134. package/dist/tools/getTag.d.ts +24 -0
  135. package/dist/tools/getTag.d.ts.map +1 -0
  136. package/dist/tools/getTag.js +17 -0
  137. package/dist/tools/getTag.js.map +1 -0
  138. package/dist/tools/getTask.d.ts +24 -0
  139. package/dist/tools/getTask.d.ts.map +1 -0
  140. package/dist/tools/getTask.js +17 -0
  141. package/dist/tools/getTask.js.map +1 -0
  142. package/dist/tools/index.d.ts +732 -0
  143. package/dist/tools/index.d.ts.map +1 -0
  144. package/dist/tools/index.js +84 -0
  145. package/dist/tools/index.js.map +1 -0
  146. package/dist/tools/listFolders.d.ts +50 -0
  147. package/dist/tools/listFolders.d.ts.map +1 -0
  148. package/dist/tools/listFolders.js +21 -0
  149. package/dist/tools/listFolders.js.map +1 -0
  150. package/dist/tools/listProjects.d.ts +70 -0
  151. package/dist/tools/listProjects.d.ts.map +1 -0
  152. package/dist/tools/listProjects.js +21 -0
  153. package/dist/tools/listProjects.js.map +1 -0
  154. package/dist/tools/listTags.d.ts +50 -0
  155. package/dist/tools/listTags.d.ts.map +1 -0
  156. package/dist/tools/listTags.js +21 -0
  157. package/dist/tools/listTags.js.map +1 -0
  158. package/dist/tools/listTasks.d.ts +156 -0
  159. package/dist/tools/listTasks.d.ts.map +1 -0
  160. package/dist/tools/listTasks.js +36 -0
  161. package/dist/tools/listTasks.js.map +1 -0
  162. package/dist/tools/moveProject.d.ts +20 -0
  163. package/dist/tools/moveProject.d.ts.map +1 -0
  164. package/dist/tools/moveProject.js +13 -0
  165. package/dist/tools/moveProject.js.map +1 -0
  166. package/dist/tools/moveTask.d.ts +31 -0
  167. package/dist/tools/moveTask.d.ts.map +1 -0
  168. package/dist/tools/moveTask.js +13 -0
  169. package/dist/tools/moveTask.js.map +1 -0
  170. package/dist/tools/resolveName.d.ts +36 -0
  171. package/dist/tools/resolveName.d.ts.map +1 -0
  172. package/dist/tools/resolveName.js +26 -0
  173. package/dist/tools/resolveName.js.map +1 -0
  174. package/openspec/changes/archive/2026-04-09-bootstrap-omnifocus-mcp/.openspec.yaml +2 -0
  175. package/openspec/changes/archive/2026-04-09-bootstrap-omnifocus-mcp/design.md +162 -0
  176. package/openspec/changes/archive/2026-04-09-bootstrap-omnifocus-mcp/proposal.md +49 -0
  177. package/openspec/changes/archive/2026-04-09-bootstrap-omnifocus-mcp/specs/attachments/spec.md +9 -0
  178. package/openspec/changes/archive/2026-04-09-bootstrap-omnifocus-mcp/specs/batch-operations/spec.md +9 -0
  179. package/openspec/changes/archive/2026-04-09-bootstrap-omnifocus-mcp/specs/database-inspection/spec.md +9 -0
  180. package/openspec/changes/archive/2026-04-09-bootstrap-omnifocus-mcp/specs/execution-runtime/spec.md +69 -0
  181. package/openspec/changes/archive/2026-04-09-bootstrap-omnifocus-mcp/specs/folder-management/spec.md +25 -0
  182. package/openspec/changes/archive/2026-04-09-bootstrap-omnifocus-mcp/specs/forecast/spec.md +9 -0
  183. package/openspec/changes/archive/2026-04-09-bootstrap-omnifocus-mcp/specs/identity-resolution/spec.md +45 -0
  184. package/openspec/changes/archive/2026-04-09-bootstrap-omnifocus-mcp/specs/perspective-management/spec.md +9 -0
  185. package/openspec/changes/archive/2026-04-09-bootstrap-omnifocus-mcp/specs/project-management/spec.md +25 -0
  186. package/openspec/changes/archive/2026-04-09-bootstrap-omnifocus-mcp/specs/recurrence/spec.md +9 -0
  187. package/openspec/changes/archive/2026-04-09-bootstrap-omnifocus-mcp/specs/settings/spec.md +9 -0
  188. package/openspec/changes/archive/2026-04-09-bootstrap-omnifocus-mcp/specs/tag-management/spec.md +25 -0
  189. package/openspec/changes/archive/2026-04-09-bootstrap-omnifocus-mcp/specs/task-management/spec.md +29 -0
  190. package/openspec/changes/archive/2026-04-09-bootstrap-omnifocus-mcp/specs/url-automation/spec.md +9 -0
  191. package/openspec/changes/archive/2026-04-09-bootstrap-omnifocus-mcp/specs/window-state/spec.md +9 -0
  192. package/openspec/changes/archive/2026-04-09-bootstrap-omnifocus-mcp/tasks.md +84 -0
  193. package/openspec/changes/archive/2026-04-09-folder-crud/.openspec.yaml +2 -0
  194. package/openspec/changes/archive/2026-04-09-folder-crud/design.md +58 -0
  195. package/openspec/changes/archive/2026-04-09-folder-crud/proposal.md +28 -0
  196. package/openspec/changes/archive/2026-04-09-folder-crud/specs/folder-write/spec.md +45 -0
  197. package/openspec/changes/archive/2026-04-09-folder-crud/tasks.md +41 -0
  198. package/openspec/changes/archive/2026-04-09-folder-tag-list-filtering/.openspec.yaml +2 -0
  199. package/openspec/changes/archive/2026-04-09-folder-tag-list-filtering/design.md +38 -0
  200. package/openspec/changes/archive/2026-04-09-folder-tag-list-filtering/proposal.md +30 -0
  201. package/openspec/changes/archive/2026-04-09-folder-tag-list-filtering/specs/folder-management/spec.md +21 -0
  202. package/openspec/changes/archive/2026-04-09-folder-tag-list-filtering/specs/tag-management/spec.md +21 -0
  203. package/openspec/changes/archive/2026-04-09-folder-tag-list-filtering/tasks.md +35 -0
  204. package/openspec/changes/archive/2026-04-09-move-operations/.openspec.yaml +2 -0
  205. package/openspec/changes/archive/2026-04-09-move-operations/design.md +43 -0
  206. package/openspec/changes/archive/2026-04-09-move-operations/proposal.md +25 -0
  207. package/openspec/changes/archive/2026-04-09-move-operations/specs/move-operations/spec.md +41 -0
  208. package/openspec/changes/archive/2026-04-09-move-operations/tasks.md +40 -0
  209. package/openspec/changes/archive/2026-04-09-project-crud/.openspec.yaml +2 -0
  210. package/openspec/changes/archive/2026-04-09-project-crud/design.md +60 -0
  211. package/openspec/changes/archive/2026-04-09-project-crud/proposal.md +29 -0
  212. package/openspec/changes/archive/2026-04-09-project-crud/specs/project-write/spec.md +74 -0
  213. package/openspec/changes/archive/2026-04-09-project-crud/tasks.md +48 -0
  214. package/openspec/changes/archive/2026-04-09-project-filtering/.openspec.yaml +2 -0
  215. package/openspec/changes/archive/2026-04-09-project-filtering/design.md +52 -0
  216. package/openspec/changes/archive/2026-04-09-project-filtering/proposal.md +26 -0
  217. package/openspec/changes/archive/2026-04-09-project-filtering/specs/project-filtering/spec.md +66 -0
  218. package/openspec/changes/archive/2026-04-09-project-filtering/specs/project-management/spec.md +13 -0
  219. package/openspec/changes/archive/2026-04-09-project-filtering/tasks.md +41 -0
  220. package/openspec/changes/archive/2026-04-09-tag-crud/.openspec.yaml +2 -0
  221. package/openspec/changes/archive/2026-04-09-tag-crud/design.md +45 -0
  222. package/openspec/changes/archive/2026-04-09-tag-crud/proposal.md +28 -0
  223. package/openspec/changes/archive/2026-04-09-tag-crud/specs/tag-write/spec.md +49 -0
  224. package/openspec/changes/archive/2026-04-09-tag-crud/tasks.md +41 -0
  225. package/openspec/changes/archive/2026-04-09-task-crud/.openspec.yaml +2 -0
  226. package/openspec/changes/archive/2026-04-09-task-crud/design.md +62 -0
  227. package/openspec/changes/archive/2026-04-09-task-crud/proposal.md +29 -0
  228. package/openspec/changes/archive/2026-04-09-task-crud/specs/task-management/spec.md +17 -0
  229. package/openspec/changes/archive/2026-04-09-task-crud/specs/task-write/spec.md +89 -0
  230. package/openspec/changes/archive/2026-04-09-task-crud/tasks.md +55 -0
  231. package/openspec/changes/archive/2026-04-09-task-filtering/.openspec.yaml +2 -0
  232. package/openspec/changes/archive/2026-04-09-task-filtering/design.md +61 -0
  233. package/openspec/changes/archive/2026-04-09-task-filtering/proposal.md +26 -0
  234. package/openspec/changes/archive/2026-04-09-task-filtering/specs/task-filtering/spec.md +63 -0
  235. package/openspec/changes/archive/2026-04-09-task-filtering/specs/task-management/spec.md +17 -0
  236. package/openspec/changes/archive/2026-04-09-task-filtering/tasks.md +42 -0
  237. package/openspec/changes/archive/2026-04-10-planned-date/.openspec.yaml +2 -0
  238. package/openspec/changes/archive/2026-04-10-planned-date/design.md +27 -0
  239. package/openspec/changes/archive/2026-04-10-planned-date/proposal.md +29 -0
  240. package/openspec/changes/archive/2026-04-10-planned-date/specs/task-management/spec.md +29 -0
  241. package/openspec/changes/archive/2026-04-10-planned-date/specs/task-write/spec.md +69 -0
  242. package/openspec/changes/archive/2026-04-10-planned-date/tasks.md +26 -0
  243. package/openspec/changes/archive/2026-04-10-task-recurrence/.openspec.yaml +2 -0
  244. package/openspec/changes/archive/2026-04-10-task-recurrence/design.md +81 -0
  245. package/openspec/changes/archive/2026-04-10-task-recurrence/proposal.md +28 -0
  246. package/openspec/changes/archive/2026-04-10-task-recurrence/specs/recurrence/spec.md +47 -0
  247. package/openspec/changes/archive/2026-04-10-task-recurrence/specs/task-management/spec.md +25 -0
  248. package/openspec/changes/archive/2026-04-10-task-recurrence/specs/task-write/spec.md +61 -0
  249. package/openspec/changes/archive/2026-04-10-task-recurrence/tasks.md +39 -0
  250. package/openspec/config.yaml +20 -0
  251. package/openspec/specs/attachments/spec.md +15 -0
  252. package/openspec/specs/batch-operations/spec.md +15 -0
  253. package/openspec/specs/database-inspection/spec.md +15 -0
  254. package/openspec/specs/execution-runtime/spec.md +75 -0
  255. package/openspec/specs/folder-management/spec.md +39 -0
  256. package/openspec/specs/folder-write/spec.md +45 -0
  257. package/openspec/specs/forecast/spec.md +15 -0
  258. package/openspec/specs/identity-resolution/spec.md +51 -0
  259. package/openspec/specs/move-operations/spec.md +41 -0
  260. package/openspec/specs/perspective-management/spec.md +15 -0
  261. package/openspec/specs/project-filtering/spec.md +72 -0
  262. package/openspec/specs/project-management/spec.md +31 -0
  263. package/openspec/specs/project-write/spec.md +79 -0
  264. package/openspec/specs/recurrence/spec.md +51 -0
  265. package/openspec/specs/settings/spec.md +15 -0
  266. package/openspec/specs/tag-management/spec.md +39 -0
  267. package/openspec/specs/tag-write/spec.md +49 -0
  268. package/openspec/specs/task-filtering/spec.md +63 -0
  269. package/openspec/specs/task-management/spec.md +51 -0
  270. package/openspec/specs/task-write/spec.md +115 -0
  271. package/openspec/specs/url-automation/spec.md +15 -0
  272. package/openspec/specs/window-state/spec.md +15 -0
  273. package/package.json +32 -0
  274. package/scripts/cleanup-fixtures.ts +89 -0
  275. package/server.json +21 -0
  276. package/src/runtime/bridge.ts +97 -0
  277. package/src/runtime/index.ts +4 -0
  278. package/src/runtime/jxaShim.ts +55 -0
  279. package/src/runtime/resultProtocol.ts +62 -0
  280. package/src/runtime/snippetLoader.ts +79 -0
  281. package/src/schemas/enums.ts +32 -0
  282. package/src/schemas/index.ts +38 -0
  283. package/src/schemas/shapes.ts +267 -0
  284. package/src/server.ts +58 -0
  285. package/src/snippets/complete_project.js +73 -0
  286. package/src/snippets/complete_task.js +85 -0
  287. package/src/snippets/create_folder.js +52 -0
  288. package/src/snippets/create_project.js +107 -0
  289. package/src/snippets/create_tag.js +55 -0
  290. package/src/snippets/create_task.js +159 -0
  291. package/src/snippets/delete_folder.js +26 -0
  292. package/src/snippets/delete_project.js +20 -0
  293. package/src/snippets/delete_tag.js +20 -0
  294. package/src/snippets/delete_task.js +20 -0
  295. package/src/snippets/drop_project.js +73 -0
  296. package/src/snippets/drop_task.js +85 -0
  297. package/src/snippets/edit_folder.js +46 -0
  298. package/src/snippets/edit_project.js +106 -0
  299. package/src/snippets/edit_tag.js +56 -0
  300. package/src/snippets/edit_task.js +146 -0
  301. package/src/snippets/get_folder.js +48 -0
  302. package/src/snippets/get_project.js +77 -0
  303. package/src/snippets/get_tag.js +51 -0
  304. package/src/snippets/get_task.js +96 -0
  305. package/src/snippets/list_folders.js +50 -0
  306. package/src/snippets/list_projects.js +98 -0
  307. package/src/snippets/list_tags.js +54 -0
  308. package/src/snippets/list_tasks.js +127 -0
  309. package/src/snippets/move_project.js +79 -0
  310. package/src/snippets/move_task.js +113 -0
  311. package/src/snippets/resolve_name.js +83 -0
  312. package/src/tools/completeProject.ts +21 -0
  313. package/src/tools/completeTask.ts +23 -0
  314. package/src/tools/createFolder.ts +20 -0
  315. package/src/tools/createProject.ts +20 -0
  316. package/src/tools/createTag.ts +20 -0
  317. package/src/tools/createTask.ts +20 -0
  318. package/src/tools/deleteFolder.ts +24 -0
  319. package/src/tools/deleteProject.ts +24 -0
  320. package/src/tools/deleteTag.ts +24 -0
  321. package/src/tools/deleteTask.ts +26 -0
  322. package/src/tools/dropProject.ts +21 -0
  323. package/src/tools/dropTask.ts +23 -0
  324. package/src/tools/editFolder.ts +19 -0
  325. package/src/tools/editProject.ts +20 -0
  326. package/src/tools/editTag.ts +20 -0
  327. package/src/tools/editTask.ts +20 -0
  328. package/src/tools/getFolder.ts +24 -0
  329. package/src/tools/getProject.ts +24 -0
  330. package/src/tools/getTag.ts +24 -0
  331. package/src/tools/getTask.ts +24 -0
  332. package/src/tools/index.ts +85 -0
  333. package/src/tools/listFolders.ts +32 -0
  334. package/src/tools/listProjects.ts +32 -0
  335. package/src/tools/listTags.ts +32 -0
  336. package/src/tools/listTasks.ts +56 -0
  337. package/src/tools/moveProject.ts +20 -0
  338. package/src/tools/moveTask.ts +20 -0
  339. package/src/tools/resolveName.ts +37 -0
  340. package/test/integration/.gitkeep +0 -0
  341. package/test/integration/completeProject.int.test.ts +25 -0
  342. package/test/integration/completeTask.int.test.ts +30 -0
  343. package/test/integration/createFolder.int.test.ts +50 -0
  344. package/test/integration/createProject.int.test.ts +49 -0
  345. package/test/integration/createTag.int.test.ts +52 -0
  346. package/test/integration/createTask.int.test.ts +55 -0
  347. package/test/integration/deleteFolder.int.test.ts +64 -0
  348. package/test/integration/deleteProject.int.test.ts +31 -0
  349. package/test/integration/deleteTag.int.test.ts +61 -0
  350. package/test/integration/deleteTask.int.test.ts +36 -0
  351. package/test/integration/dropProject.int.test.ts +24 -0
  352. package/test/integration/dropTask.int.test.ts +29 -0
  353. package/test/integration/editFolder.int.test.ts +43 -0
  354. package/test/integration/editProject.int.test.ts +39 -0
  355. package/test/integration/editTag.int.test.ts +43 -0
  356. package/test/integration/editTask.int.test.ts +56 -0
  357. package/test/integration/fixtures.ts +219 -0
  358. package/test/integration/getTask.int.test.ts +64 -0
  359. package/test/integration/listFoldersFiltered.int.test.ts +98 -0
  360. package/test/integration/listProjects.int.test.ts +73 -0
  361. package/test/integration/listProjectsFiltered.int.test.ts +96 -0
  362. package/test/integration/listTagsFiltered.int.test.ts +54 -0
  363. package/test/integration/listTasksFiltered.int.test.ts +141 -0
  364. package/test/integration/moveProject.int.test.ts +57 -0
  365. package/test/integration/moveTask.int.test.ts +61 -0
  366. package/test/integration/plannedDate.int.test.ts +72 -0
  367. package/test/integration/preflight.ts +60 -0
  368. package/test/integration/resolveName.int.test.ts +86 -0
  369. package/test/integration/taskRecurrence.int.test.ts +106 -0
  370. package/test/unit/.gitkeep +0 -0
  371. package/test/unit/bridge.injection.test.ts +66 -0
  372. package/test/unit/resultProtocol.test.ts +71 -0
  373. package/test/unit/schemas.createFolder.test.ts +38 -0
  374. package/test/unit/schemas.createProject.test.ts +115 -0
  375. package/test/unit/schemas.createTask.test.ts +87 -0
  376. package/test/unit/schemas.editTag.test.ts +64 -0
  377. package/test/unit/schemas.folderTagFiltering.test.ts +42 -0
  378. package/test/unit/schemas.listProjects.test.ts +44 -0
  379. package/test/unit/schemas.moveOperations.test.ts +60 -0
  380. package/test/unit/schemas.recurrence.test.ts +120 -0
  381. package/test/unit/schemas.test.ts +126 -0
  382. package/test/unit/snippetLoader.test.ts +56 -0
  383. package/test/unit/tools.deleteTask.test.ts +19 -0
  384. package/test/unit/tools.listTasks.test.ts +126 -0
  385. package/tsconfig.json +19 -0
  386. package/vitest.config.ts +8 -0
  387. 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,2 @@
1
+ schema: spec-driven
2
+ created: 2026-04-09
@@ -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,2 @@
1
+ schema: spec-driven
2
+ created: 2026-04-10
@@ -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`
@@ -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,2 @@
1
+ schema: spec-driven
2
+ created: 2026-04-09
@@ -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