@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,25 @@
1
+ ## ADDED Requirements
2
+
3
+ ### Requirement: List tags
4
+
5
+ The system SHALL provide a `list_tags` tool that returns every tag in the OmniFocus database as an array of summaries, each containing at minimum `{id, name, path, parentId, status}`. Tags form a tree in OmniFocus; the `path` field SHALL use the canonical ` ▸ ` separator and represent the full ancestor chain. The `parentId` field SHALL be `null` for top-level tags. The `status` field SHALL be one of `"active" | "onHold" | "dropped"`.
6
+
7
+ #### Scenario: All tags are returned including nested tags
8
+ - **WHEN** `list_tags` is called with no arguments
9
+ - **THEN** the tool returns every tag in the database, top-level and nested, each with its full path and correct parentId
10
+
11
+ #### Scenario: On-hold tag is reported with correct status
12
+ - **WHEN** the database contains a tag that has been placed on hold
13
+ - **THEN** that tag appears in the result with `status: "onHold"`
14
+
15
+ ### Requirement: Get tag by ID
16
+
17
+ The system SHALL provide a `get_tag` tool that accepts `{id: string}` and returns the full detail record of the named tag, including `{id, name, path, parentId, status, childTagIds}`. If no tag exists with that ID, the tool SHALL return a structured not-found error.
18
+
19
+ #### Scenario: Existing tag returns full detail
20
+ - **WHEN** `get_tag` is called with the ID of an existing tag
21
+ - **THEN** the tool returns the tag's full detail including the IDs of its immediate child tags
22
+
23
+ #### Scenario: Missing tag returns not-found error
24
+ - **WHEN** `get_tag` is called with an ID that does not correspond to any tag
25
+ - **THEN** the tool returns a structured error with a not-found code
@@ -0,0 +1,29 @@
1
+ ## ADDED Requirements
2
+
3
+ ### Requirement: List tasks with scope filter
4
+
5
+ The system SHALL provide a `list_tasks` tool that returns tasks within a caller-specified scope. The scope SHALL be one of: `{projectId: string}`, `{folderId: string}` (all tasks in projects under that folder, recursively), `{inbox: true}`, or `{all: true}` (the full flattened task list). The tool SHALL return an array of task summaries, each containing at minimum `{id, name, status, flagged, containerId, containerType}`.
6
+
7
+ #### Scenario: List tasks in a specific project
8
+ - **WHEN** `list_tasks` is called with `{projectId: "abc123"}`
9
+ - **THEN** the tool returns every task directly or transitively contained in that project, with each element carrying the project id as `containerId` and `"project"` as `containerType`
10
+
11
+ #### Scenario: List inbox tasks
12
+ - **WHEN** `list_tasks` is called with `{inbox: true}`
13
+ - **THEN** the tool returns every task in the OmniFocus inbox with `containerType` set to `"inbox"`
14
+
15
+ #### Scenario: Invalid scope is rejected at the TS boundary
16
+ - **WHEN** `list_tasks` is called with `{projectId: "abc", inbox: true}` (mutually exclusive scopes)
17
+ - **THEN** the tool returns a validation error before any snippet executes
18
+
19
+ ### Requirement: Get task by ID
20
+
21
+ 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}`. If no task exists with that ID, the tool SHALL return a structured not-found error.
22
+
23
+ #### Scenario: Existing task returns full detail
24
+ - **WHEN** `get_task` is called with the ID of an existing task
25
+ - **THEN** the tool returns the task's full detail record including all scalar fields and the list of tag IDs assigned to it
26
+
27
+ #### Scenario: Missing task returns not-found error
28
+ - **WHEN** `get_task` is called with an ID that does not correspond to any task
29
+ - **THEN** the tool returns a structured error with a not-found code and does not throw an unhandled exception
@@ -0,0 +1,9 @@
1
+ ## ADDED Requirements
2
+
3
+ ### Requirement: Capability declared
4
+
5
+ The `url-automation` capability SHALL cover `omnifocus://` URL construction (including task-paste format for bulk creation, add-to-inbox URLs, and deep links to entities by ID) and parsing of incoming `omnifocus://` URLs into structured form. Requirements for individual tools SHALL be added by the `url-automation` change.
6
+
7
+ #### Scenario: Capability is named and scoped
8
+ - **WHEN** a future change proposes adding URL-automation tools
9
+ - **THEN** it lands requirements under this capability rather than inventing a new capability name
@@ -0,0 +1,9 @@
1
+ ## ADDED Requirements
2
+
3
+ ### Requirement: Capability declared
4
+
5
+ The `window-state` capability SHALL cover read-only inspection of OmniFocus document window state, including the currently active window, active perspective, sidebar selection, and content selection. Window *mutation* (resize, close, focus, open) is an explicit non-goal. Requirements for individual tools SHALL be added by the `perspectives-and-windows` change.
6
+
7
+ #### Scenario: Capability is named and read-only scope is fixed
8
+ - **WHEN** a future change proposes adding window-state tools
9
+ - **THEN** it lands read-only requirements under this capability; any mutation proposal requires first revisiting the non-goal in design
@@ -0,0 +1,84 @@
1
+ ## 1. Repository scaffold
2
+
3
+ - [x] 1.1 Create `package.json` with dependencies: `@modelcontextprotocol/sdk`, `zod`; devDependencies: `typescript`, `vitest`, `@types/node`, `tsx`
4
+ - [x] 1.2 Create `tsconfig.json` targeting Node 20, strict mode, `moduleResolution: "bundler"`, `outDir: "dist"`, sourcemaps on
5
+ - [x] 1.3 Create directory structure: `src/runtime/`, `src/snippets/`, `src/tools/`, `src/schemas/`, `test/unit/`, `test/integration/`
6
+ - [x] 1.4 Add `.gitignore` covering `node_modules`, `dist`, `*.log`, macOS cruft
7
+ - [x] 1.5 Add root `README.md` stub with macOS-only note and integration-test warning about sync
8
+
9
+ ## 2. Execution runtime (src/runtime/)
10
+
11
+ - [x] 2.1 Implement `snippetLoader.ts`: reads `src/snippets/<name>.js` from the resolved snippet root, caches in memory, validates exactly one `__ARGS__` token, throws on zero or multiple
12
+ - [x] 2.2 Implement `resultProtocol.ts`: zod schema for the `{ok, data} | {ok: false, error}` envelope, `parseResultLine(stdout: string)` that selects the first JSON-parseable line and validates the envelope, `ExecutionError` class carrying name/message/stack from the error branch
13
+ - [x] 2.3 Implement `jxaShim.ts`: a template string containing the JXA wrapper that calls `Application('OmniFocus').evaluateJavascript(snippet)`, wraps in try/catch, prints the envelope as one JSON line
14
+ - [x] 2.4 Implement `bridge.ts`: `runSnippet(name: string, args: unknown, opts?: {timeoutMs?: number}): Promise<unknown>` — loads the snippet via the loader, injects args via `template.replace("__ARGS__", JSON.stringify(args))`, wraps the result in the JXA shim, spawns `osascript -l JavaScript`, enforces timeout via `AbortController` + SIGTERM, parses stdout via `parseResultLine`, throws `ExecutionError` on `ok: false`, returns `data` on success
15
+ - [x] 2.5 Export a single `runtime` barrel from `src/runtime/index.ts`
16
+
17
+ ## 3. Shared schemas (src/schemas/)
18
+
19
+ - [x] 3.1 Define zod schemas: `IdSchema` (non-empty string), `EntityType` enum (`"task" | "project" | "folder" | "tag" | "perspective"`), `ProjectType` enum, `ProjectStatus` enum, `TaskStatus` enum, `TagStatus` enum, `FolderStatus` enum
20
+ - [x] 3.2 Define shared return-shape schemas: `TaskSummary`, `TaskDetail`, `ProjectSummary`, `ProjectDetail`, `FolderSummary`, `FolderDetail`, `TagSummary`, `TagDetail`, `ResolveCandidate`
21
+ - [x] 3.3 Export a `schemas` barrel from `src/schemas/index.ts`
22
+
23
+ ## 4. Snippets (src/snippets/*.js) — read-only set for this change
24
+
25
+ - [x] 4.1 `list_projects.js`: iterate `flattenedProjects`, map to summary shape with `id.primaryKey`, canonical `status`/`type` enum values, folder path via ` ▸ ` separator; return `JSON.stringify({ok: true, data: [...]})`
26
+ - [x] 4.2 `get_project.js`: resolve project by `args.id` via `Project.byIdentifier` (or equivalent), build detail shape including review metadata and tag IDs, return envelope; throw on not-found with a `NotFoundError` constructor defined at the top of the snippet
27
+ - [x] 4.3 `list_folders.js`: iterate `flattenedFolders`, map to summary with full path; return envelope
28
+ - [x] 4.4 `get_folder.js`: resolve folder by ID, return detail including child folder IDs and immediate project IDs
29
+ - [x] 4.5 `list_tasks.js`: accept `args.scope` discriminated union (`projectId` / `folderId` / `inbox` / `all`), iterate the correct source, map to summary; return envelope
30
+ - [x] 4.6 `get_task.js`: resolve task by ID, return full detail including tag IDs
31
+ - [x] 4.7 `list_tags.js`: iterate `flattenedTags`, map to summary with full path, parentId, status
32
+ - [x] 4.8 `get_tag.js`: resolve tag by ID, return detail including child tag IDs
33
+ - [x] 4.9 `resolve_name.js`: accept `args.type`, `args.query`, optional `args.scope`; walk the relevant flat list, match by exact name (and path suffix if scope given), return array of candidates with `{id, name, path, type}`; never throw on zero matches
34
+ - [x] 4.10 Add a shared helper comment block at the top of each snippet documenting the paste-to-console procedure
35
+
36
+ ## 5. MCP tools (src/tools/)
37
+
38
+ - [x] 5.1 `listProjects.ts`: zod input schema (empty object), handler invokes `runSnippet("list_projects", {})`, validates output against `z.array(ProjectSummary)`, returns
39
+ - [x] 5.2 `getProject.ts`: input `{id}`, handler invokes `runSnippet("get_project", {id})`, validates `ProjectDetail`
40
+ - [x] 5.3 `listFolders.ts`: empty input, returns `z.array(FolderSummary)`
41
+ - [x] 5.4 `getFolder.ts`: input `{id}`, returns `FolderDetail`
42
+ - [x] 5.5 `listTasks.ts`: input discriminated union (`{projectId} | {folderId} | {inbox: true} | {all: true}`) with zod `discriminatedUnion` or refinement rejecting mutual exclusivity, returns `z.array(TaskSummary)`
43
+ - [x] 5.6 `getTask.ts`: input `{id}`, returns `TaskDetail`
44
+ - [x] 5.7 `listTags.ts`: empty input, returns `z.array(TagSummary)`
45
+ - [x] 5.8 `getTag.ts`: input `{id}`, returns `TagDetail`
46
+ - [x] 5.9 `resolveName.ts`: input `{type, query, scope?}`, returns `z.array(ResolveCandidate)`
47
+ - [x] 5.10 Export a `tools` barrel from `src/tools/index.ts` listing all nine tool definitions
48
+
49
+ ## 6. MCP server entrypoint
50
+
51
+ - [x] 6.1 `src/server.ts`: construct an `@modelcontextprotocol/sdk` server, register all nine tools from the barrel, wire stdio transport, start listening
52
+ - [x] 6.2 Add `bin` field to `package.json` pointing at the compiled entrypoint and a `scripts.start` for development via `tsx`
53
+
54
+ ## 7. Unit tests (test/unit/)
55
+
56
+ - [x] 7.1 `snippetLoader.test.ts`: golden-file test that loading each snippet in `src/snippets/` succeeds and contains exactly one `__ARGS__`; contract-violation tests for zero/multiple placeholders (using fixture files)
57
+ - [x] 7.2 `resultProtocol.test.ts`: parses well-formed success envelope, parses well-formed error envelope, rejects malformed JSON, selects first parseable line when preceded by chatter
58
+ - [x] 7.3 `bridge.injection.test.ts`: given a snippet template and args containing apostrophes, quotes, backslashes, newlines, and emoji, assert the generated script source is valid JavaScript (parse via `new Function`) and that evaluating it recovers the original args object unchanged — proves Decision 2 structurally
59
+ - [x] 7.4 `schemas.test.ts`: round-trip validation for each shared schema; rejection tests for the `sequential`-as-string and `items`-as-JSON-string regression patterns at the boundary
60
+ - [x] 7.5 `tools.listTasks.test.ts`: rejects mutually exclusive scope combinations at the zod boundary before any bridge call
61
+
62
+ ## 8. Integration test harness (test/integration/)
63
+
64
+ - [x] 8.1 `fixtures.ts`: `createTestFolder()` generates `__MCP_TEST_<uuid>__` top-level folder via the bridge; `cleanupTestFolder(id)` removes it; `withTestFolder(fn)` helper wraps a test body
65
+ - [x] 8.2 `preflight.ts`: detects OmniFocus sync status (via a read-only snippet that inspects the relevant setting); throws unless `MCP_TEST_ALLOW_SYNC=1` is set; invoked once in the vitest global setup
66
+ - [x] 8.3 `vitest.integration.config.ts`: separate vitest config for integration with `pool: "forks"`, `maxConcurrency: 1`, `globalSetup: ./preflight.ts`, `testMatch: test/integration/**`
67
+ - [x] 8.4 Integration test: `listProjects.int.test.ts` creates a fixture folder with a child project, calls the real bridge, asserts the fixture project appears in the result with the correct folder path
68
+ - [x] 8.5 Integration test: `getTask.int.test.ts` creates a fixture task with an apostrophe and unicode in its name, retrieves by ID, asserts name survives round-trip — proves Decision 2 end-to-end
69
+ - [x] 8.6 Integration test: `resolveName.int.test.ts` creates two fixture projects with identical names under different folders, asserts `resolve_name` returns both candidates with distinct paths
70
+ - [x] 8.7 Add a `scripts.test:integration` in `package.json` guarded by macOS check
71
+ - [x] 8.8 Add a `scripts.test:cleanup-fixtures` that scans for stale `__MCP_TEST_*` folders and removes them
72
+
73
+ ## 9. Documentation
74
+
75
+ - [x] 9.1 Expand `README.md` with: prerequisites (macOS, OmniFocus running), install, MCP client configuration snippet, the list of tools delivered in this change, the loud warning about integration tests mutating real data and the sync preflight
76
+ - [x] 9.2 Add `CONTRIBUTING.md` documenting the snippet authoring rules (the `__ARGS__` placeholder, no other interpolation, paste-to-console workflow) and the forbidden-scripting-dictionary rule from execution-runtime spec
77
+
78
+ ## 10. Verification
79
+
80
+ - [x] 10.1 Run `npm run typecheck` (or `tsc --noEmit`) clean
81
+ - [x] 10.2 Run `npm run test` (unit suite) clean
82
+ - [x] 10.3 Manually run integration suite against a real OmniFocus with sync disabled; verify fixture folder is created and torn down
83
+ - [x] 10.4 Manually connect the server to an MCP client (Claude Desktop or equivalent) and invoke each of the nine tools against a real database; verify results match the spec scenarios
84
+ - [x] 10.5 Confirm all spec scenarios in `specs/execution-runtime/spec.md` and `specs/identity-resolution/spec.md` have corresponding test coverage (unit + integration)
@@ -0,0 +1,2 @@
1
+ schema: spec-driven
2
+ created: 2026-04-09
@@ -0,0 +1,58 @@
1
+ ## Context
2
+
3
+ Folders in OmniJS are simpler than tasks or projects — they have only `name` and `status` as meaningful writable properties, and they nest via parent/child relationships. The main complexity is deletion: `deleteObject(folder)` leaves an empty shell (as discovered during bootstrap integration testing); the snippet must recursively delete contents first.
4
+
5
+ OmniJS folder write operations:
6
+ - `new Folder(name)` — creates a top-level folder (auto-inserted at root)
7
+ - `new Folder(name, folderObject)` — creates a nested folder inside `folderObject`
8
+ - `folder.name = "..."` — rename
9
+ - `deleteObject(folder)` — removes folder shell but NOT contents; must delete contents first
10
+ - Recursive deletion pattern (established in bootstrap fixtures): delete all projects in folder, recurse child folders, then delete the folder itself
11
+
12
+ ## Goals / Non-Goals
13
+
14
+ **Goals:**
15
+ - Create top-level and nested folders
16
+ - Rename folders
17
+ - Delete folders with full recursive cascade (projects, tasks, child folders)
18
+
19
+ **Non-Goals:**
20
+ - Moving a folder to a different parent (separate `move-operations` change)
21
+ - Changing folder status directly (OmniFocus manages active/dropped based on contents)
22
+
23
+ ## Decisions
24
+
25
+ ### Decision 1: Recursive deletion in the snippet
26
+
27
+ The snippet for `delete_folder` must implement recursive deletion rather than relying on `deleteObject` to cascade. The pattern is:
28
+
29
+ ```
30
+ function deleteFolder(f) {
31
+ f.flattenedProjects.forEach(p => deleteObject(p));
32
+ f.folders.forEach(child => deleteFolder(child));
33
+ deleteObject(f);
34
+ }
35
+ ```
36
+
37
+ This was validated during bootstrap integration testing (`fixtures.ts` uses this exact pattern).
38
+
39
+ ### Decision 2: `create_folder` placement via optional `parentFolderId`
40
+
41
+ - `parentFolderId` omitted → top-level folder
42
+ - `parentFolderId` provided → nested inside that folder
43
+
44
+ `new Folder(name)` for top-level; `new Folder(name, parentFolder)` for nested. Snippet resolves the parent by ID.
45
+
46
+ ### Decision 3: No `edit_folder` status field
47
+
48
+ Folder status (`active` / `dropped`) in OmniFocus reflects whether the folder has active contents — it is not directly user-settable in the same way as project status. `edit_folder` exposes only `name`. If this proves insufficient in practice it can be extended.
49
+
50
+ ### Decision 4: `delete_folder` tool description is the strongest warning in the server
51
+
52
+ This is the most destructive single operation: one call can delete an entire project hierarchy. The tool description must explicitly state that all child folders, projects, and tasks are permanently deleted, and that the AI must confirm with the user before calling it.
53
+
54
+ ## Risks / Trade-offs
55
+
56
+ - **`deleteObject` on folder leaves shell** — confirmed behavior; recursive snippet pattern mitigates this fully.
57
+ - **Deleting a folder containing hundreds of projects** — could be slow inside `evaluateJavascript`; the 30s timeout applies. Not worth special-casing in v1.
58
+ - **Race between read and delete** — if the user reads a folder ID then calls delete, OmniFocus has no transaction isolation. Acceptable for a single-user desktop app.
@@ -0,0 +1,28 @@
1
+ ## Why
2
+
3
+ The bootstrap change delivered read-only folder access. Callers can traverse the folder hierarchy but cannot create, rename, or delete folders — blocking any workflow that involves organizing projects into folders.
4
+
5
+ ## What Changes
6
+
7
+ - Add `create_folder` tool: create a folder at the top level or nested inside an existing folder
8
+ - Add `edit_folder` tool: rename a folder
9
+ - Add `delete_folder` tool: permanently delete a folder and all its contents — projects, tasks, and child folders (tool description instructs the AI to confirm with the user before invoking and calls out the destructive cascade)
10
+
11
+ ## Capabilities
12
+
13
+ ### New Capabilities
14
+
15
+ - `folder-write`: Create, rename, and permanently delete OmniFocus folders
16
+
17
+ ### Modified Capabilities
18
+
19
+ _(none — existing `folder-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
+ - Integration tests will mutate real OmniFocus data (scoped to fixture folder per existing pattern)
28
+ - `delete_folder` is the most destructive single operation in the server — the tool description warrants an explicit warning that the entire subtree (child folders, projects, and all tasks) is permanently removed
@@ -0,0 +1,45 @@
1
+ ## ADDED Requirements
2
+
3
+ ### Requirement: Create folder
4
+
5
+ The system SHALL provide a `create_folder` tool that creates a new OmniFocus folder and returns its full detail record. The tool SHALL accept `{name: string, parentFolderId?: string}`. If `parentFolderId` is provided the folder SHALL be created nested inside that folder; otherwise it SHALL be created at the top level.
6
+
7
+ #### Scenario: Create top-level folder
8
+ - **WHEN** `create_folder` is called with `{name: "Work"}` and no `parentFolderId`
9
+ - **THEN** the tool creates the folder at the top level and returns its full detail record including a stable `id`
10
+
11
+ #### Scenario: Create nested folder
12
+ - **WHEN** `create_folder` is called with `{name: "Active", parentFolderId: "abc123"}`
13
+ - **THEN** the tool creates the folder inside the specified parent folder and returns its full detail record with `path` reflecting the full ancestor chain
14
+
15
+ #### Scenario: Non-existent parent folder returns not-found error
16
+ - **WHEN** `create_folder` is called with a `parentFolderId` that does not correspond to any folder
17
+ - **THEN** the tool returns a structured not-found error
18
+
19
+ ### Requirement: Edit folder
20
+
21
+ The system SHALL provide an `edit_folder` tool that renames an existing folder and returns its updated full detail record. The tool SHALL accept `{id: string, name: string}`.
22
+
23
+ #### Scenario: Rename a folder
24
+ - **WHEN** `edit_folder` is called with `{id: "abc123", name: "Personal"}`
25
+ - **THEN** the folder's name is updated and the tool returns the updated detail record with the new name reflected in `path`
26
+
27
+ #### Scenario: Non-existent folder returns not-found error
28
+ - **WHEN** `edit_folder` is called with an ID that does not correspond to any folder
29
+ - **THEN** the tool returns a structured not-found error
30
+
31
+ ### Requirement: Delete folder
32
+
33
+ The system SHALL provide a `delete_folder` tool that permanently and recursively deletes a folder, all child folders, all projects within those folders, and all tasks within those projects. The tool description SHALL instruct the AI to confirm with the user before invoking, explicitly stating that the entire subtree — child folders, projects, and all tasks — is permanently deleted and cannot be undone.
34
+
35
+ #### Scenario: Delete a folder and all its contents
36
+ - **WHEN** `delete_folder` is called with the ID of an existing folder
37
+ - **THEN** the folder, all descendant folders, all projects within those folders, and all tasks within those projects are permanently removed from OmniFocus and the tool returns a confirmation envelope
38
+
39
+ #### Scenario: Delete empty folder
40
+ - **WHEN** `delete_folder` is called with the ID of a folder that contains no projects or child folders
41
+ - **THEN** the folder is removed and the tool returns a confirmation envelope
42
+
43
+ #### Scenario: Non-existent folder returns not-found error
44
+ - **WHEN** `delete_folder` is called with an ID that does not correspond to any folder
45
+ - **THEN** the tool returns a structured not-found error
@@ -0,0 +1,41 @@
1
+ ## 1. Snippets (src/snippets/)
2
+
3
+ - [x] 1.1 `create_folder.js`: accept `{name, parentFolderId?}`; if `parentFolderId` provided resolve parent by ID and throw `NotFoundError` if missing; create `new Folder(name)` or `new Folder(name, parentFolder)`; return full folder detail envelope
4
+ - [x] 1.2 `edit_folder.js`: accept `{id, name}`; resolve folder by ID; assign `folder.name = name`; return updated full folder detail envelope
5
+ - [x] 1.3 `delete_folder.js`: accept `{id}`; resolve folder by ID; recursively delete contents (projects then child folders) then `deleteObject(folder)`; return `{ok: true, data: {id}}`
6
+
7
+ ## 2. Snippet allowlist
8
+
9
+ - [x] 2.1 Add `create_folder`, `edit_folder`, `delete_folder` to `ALLOWED_SNIPPETS` in `src/runtime/snippetLoader.ts`
10
+
11
+ ## 3. Schemas (src/schemas/shapes.ts)
12
+
13
+ - [x] 3.1 Define `CreateFolderInput` zod schema: `{name: z.string().min(1), parentFolderId: IdSchema.optional()}`
14
+ - [x] 3.2 Define `EditFolderInput` zod schema: `{id: IdSchema, name: z.string().min(1)}`
15
+
16
+ ## 4. Tool handlers (src/tools/)
17
+
18
+ - [x] 4.1 `createFolder.ts`: validate input with `CreateFolderInput`; invoke `runSnippet("create_folder", args)`; validate result against `FolderDetail`; return
19
+ - [x] 4.2 `editFolder.ts`: validate input with `EditFolderInput`; invoke `runSnippet("edit_folder", args)`; validate result against `FolderDetail`; return
20
+ - [x] 4.3 `deleteFolder.ts`: input `{id: IdSchema}`; tool description MUST state the AI should confirm with the user before invoking and explicitly note that the entire subtree — child folders, projects, and all tasks — is permanently deleted; invoke `runSnippet("delete_folder", {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.createFolder.test.ts`: valid inputs pass; empty name rejected
29
+
30
+ ## 7. Integration tests (test/integration/)
31
+
32
+ - [x] 7.1 `createFolder.int.test.ts`: create top-level folder; create nested folder; verify `path` and `parentId`
33
+ - [x] 7.2 `editFolder.int.test.ts`: rename a folder; verify `name` and `path` updated
34
+ - [x] 7.3 `deleteFolder.int.test.ts`: create folder with child project and tasks; delete folder; verify `get_folder` returns not-found and projects are 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
@@ -0,0 +1,2 @@
1
+ schema: spec-driven
2
+ created: 2026-04-10
@@ -0,0 +1,38 @@
1
+ ## Context
2
+
3
+ `list_folders` and `list_tags` are the last two list tools without filtering or limit support. The pattern established by `list_tasks` and `list_projects` — filter inside the OmniJS snippet, expose filter object and limit from the TypeScript handler — applies directly.
4
+
5
+ Unlike tasks and projects, there is no universally agreed "terminal" status for folders (dropped folders are unusual but valid to show) or tags (onHold tags are actively used). Therefore the default behavior includes all statuses; filtering is opt-in.
6
+
7
+ ## Goals / Non-Goals
8
+
9
+ **Goals:**
10
+ - `list_folders`: add `limit` (default 200) and optional `status` filter (`active` | `dropped`)
11
+ - `list_tags`: add `limit` (default 200) and optional `status` filter (`active` | `onHold` | `dropped`)
12
+
13
+ **Non-Goals:**
14
+ - `parentId` / subtree scoping for folders or tags (flat list is sufficient in practice)
15
+ - Filtering by name or path pattern
16
+ - Pagination
17
+
18
+ ## Decisions
19
+
20
+ ### Decision 1: No default status exclusion
21
+
22
+ Unlike `list_tasks` and `list_projects`, the default returns all statuses. Dropped folders/tags are uncommon enough that including them doesn't overwhelm results, and an LLM may need to see them. Callers can pass `status: ["active"]` to filter.
23
+
24
+ ### Decision 2: Single status value, not array
25
+
26
+ For `list_folders` and `list_tags`, a single `status` string (not an array) is sufficient — users rarely need "active OR onHold" for tags. Simpler API. If multi-status becomes needed, it's a non-breaking addition later.
27
+
28
+ ### Decision 3: Limit default 200
29
+
30
+ Folders and tags are fewer than tasks; 200 is generous. Consistent with the spirit of existing defaults (tasks: 200, projects: 100).
31
+
32
+ ### Decision 4: Same filter-in-snippet pattern
33
+
34
+ All filtering in the OmniJS snippet before data crosses the JXA bridge. TypeScript layer validates and passes through. Consistent with list_tasks and list_projects.
35
+
36
+ ## Risks / Trade-offs
37
+
38
+ - **Minimal risk** — purely additive. Existing callers passing no args get all items (same as before but now capped at 200, which is a minor behavior change). Acceptable.
@@ -0,0 +1,30 @@
1
+ ## Why
2
+
3
+ `list_folders` and `list_tags` return all items with no way to cap results or filter by status. While folders and tags are typically fewer than tasks or projects, users with large databases may have hundreds of tags (including dropped ones). Adding `limit` and optional `status` filtering completes the filtering pattern applied to `list_tasks` and `list_projects`.
4
+
5
+ ## What Changes
6
+
7
+ - Add `limit` parameter to `list_folders` (default 200)
8
+ - Add `status` filter to `list_folders`: when provided, restrict to folders with that status (`active` or `dropped`); when omitted, return all folders
9
+ - Add `limit` parameter to `list_tags` (default 200)
10
+ - Add `status` filter to `list_tags`: when provided, restrict to tags with that status (`active`, `onHold`, or `dropped`); when omitted, return all tags
11
+ - All filtering happens inside the OmniJS snippet
12
+
13
+ ## Capabilities
14
+
15
+ ### New Capabilities
16
+
17
+ _(none)_
18
+
19
+ ### Modified Capabilities
20
+
21
+ - `folder-management`: `list_folders` gains `limit` and optional `status` filter
22
+ - `tag-management`: `list_tags` gains `limit` and optional `status` filter
23
+
24
+ ## Impact
25
+
26
+ - `src/schemas/shapes.ts`: add `ListFoldersFilter` and `ListTagsFilter` schemas
27
+ - `src/schemas/index.ts`: export new filter schemas
28
+ - `src/snippets/list_folders.js`, `list_tags.js`: add filter + limit logic
29
+ - `src/tools/listFolders.ts`, `listTags.ts`: add filter + limit params
30
+ - Unit and integration tests for both tools
@@ -0,0 +1,21 @@
1
+ ## MODIFIED Requirements
2
+
3
+ ### Requirement: List folders
4
+
5
+ The system SHALL provide a `list_folders` tool that returns folders in the OmniFocus database as an array of summaries, each containing `{id, name, path, parentId, status}`. The `path` field SHALL use the canonical ` ▸ ` separator and SHALL equal the folder's name for top-level folders. The `parentId` field SHALL be `null` for top-level folders. The `status` field SHALL be one of `"active" | "dropped"`. The tool SHALL accept an optional `status` filter string and an optional `limit` integer (default 200). When `status` is omitted, all folders are returned regardless of status. When `limit` is omitted, at most 200 folders are returned.
6
+
7
+ #### Scenario: All folders returned by default
8
+ - **WHEN** `list_folders` is called with no arguments
9
+ - **THEN** the tool returns all folders (active and dropped) up to the limit
10
+
11
+ #### Scenario: Top-level folders have null parent
12
+ - **WHEN** a folder exists at the root of the folder hierarchy
13
+ - **THEN** its summary carries `parentId: null` and `path` equal to its name
14
+
15
+ #### Scenario: Filter by status returns only matching folders
16
+ - **WHEN** `list_folders` is called with `{ filter: { status: "active" } }`
17
+ - **THEN** only active folders are returned
18
+
19
+ #### Scenario: Limit caps the number of returned folders
20
+ - **WHEN** `list_folders` is called with `{ limit: 5 }`
21
+ - **THEN** at most 5 folders are returned
@@ -0,0 +1,21 @@
1
+ ## MODIFIED Requirements
2
+
3
+ ### Requirement: List tags
4
+
5
+ The system SHALL provide a `list_tags` tool that returns tags in the OmniFocus database as an array of summaries, each containing `{id, name, path, parentId, status}`. Tags form a tree in OmniFocus; the `path` field SHALL use the canonical ` ▸ ` separator and represent the full ancestor chain. The `parentId` field SHALL be `null` for top-level tags. The `status` field SHALL be one of `"active" | "onHold" | "dropped"`. The tool SHALL accept an optional `status` filter string and an optional `limit` integer (default 200). When `status` is omitted, all tags are returned regardless of status. When `limit` is omitted, at most 200 tags are returned.
6
+
7
+ #### Scenario: All tags returned by default
8
+ - **WHEN** `list_tags` is called with no arguments
9
+ - **THEN** the tool returns all tags (active, onHold, and dropped) up to the limit
10
+
11
+ #### Scenario: On-hold tag is reported with correct status
12
+ - **WHEN** the database contains a tag that has been placed on hold
13
+ - **THEN** that tag appears in the result with `status: "onHold"`
14
+
15
+ #### Scenario: Filter by status returns only matching tags
16
+ - **WHEN** `list_tags` is called with `{ filter: { status: "active" } }`
17
+ - **THEN** only active tags are returned
18
+
19
+ #### Scenario: Limit caps the number of returned tags
20
+ - **WHEN** `list_tags` is called with `{ limit: 5 }`
21
+ - **THEN** at most 5 tags are returned
@@ -0,0 +1,35 @@
1
+ ## 1. Schema changes (src/schemas/)
2
+
3
+ - [x] 1.1 Define `ListFoldersFilter` zod schema: `{ status: FolderStatus.optional() }` in `shapes.ts`
4
+ - [x] 1.2 Define `ListTagsFilter` zod schema: `{ status: TagStatus.optional() }` in `shapes.ts`
5
+ - [x] 1.3 Export `ListFoldersFilter` and `ListTagsFilter` from `src/schemas/index.ts`
6
+
7
+ ## 2. Snippet updates (src/snippets/)
8
+
9
+ - [x] 2.1 Update `list_folders.js`: add `filter` and `limit` args; apply status filter when `args.filter?.status` is provided; apply limit (default 200) as slice after filter
10
+ - [x] 2.2 Update `list_tags.js`: add `filter` and `limit` args; apply status filter when `args.filter?.status` is provided; apply limit (default 200) as slice after filter
11
+
12
+ ## 3. Tool handler updates (src/tools/)
13
+
14
+ - [x] 3.1 Update `listFolders.ts`: add `filter: ListFoldersFilter.optional()` and `limit: z.number().int().positive().optional()` to schema; pass through to `runSnippet`; update description
15
+ - [x] 3.2 Update `listTags.ts`: add `filter: ListTagsFilter.optional()` and `limit: z.number().int().positive().optional()` to schema; pass through to `runSnippet`; update description
16
+
17
+ ## 4. Unit tests (test/unit/)
18
+
19
+ - [x] 4.1 Add schema tests for `ListFoldersFilter`: valid with status "active"; valid with status "dropped"; valid empty; invalid status value rejected
20
+ - [x] 4.2 Add schema tests for `ListTagsFilter`: valid with status "active"; valid with status "onHold"; valid with status "dropped"; valid empty; invalid status value rejected
21
+
22
+ ## 5. Integration tests (test/integration/)
23
+
24
+ - [x] 5.1 `listFoldersFiltered.int.test.ts`: verify status filter returns only folders with that status
25
+ - [x] 5.2 `listFoldersFiltered.int.test.ts`: verify limit caps results
26
+ - [x] 5.3 `listFoldersFiltered.int.test.ts`: verify no filter returns all folders (including dropped)
27
+ - [x] 5.4 `listTagsFiltered.int.test.ts`: verify status filter returns only tags with that status
28
+ - [x] 5.5 `listTagsFiltered.int.test.ts`: verify limit caps results
29
+ - [x] 5.6 `listTagsFiltered.int.test.ts`: verify no filter returns all tags
30
+
31
+ ## 6. Verification
32
+
33
+ - [x] 6.1 `npm run typecheck` clean
34
+ - [x] 6.2 `npm test` (unit suite) clean
35
+ - [x] 6.3 Manually run integration suite
@@ -0,0 +1,2 @@
1
+ schema: spec-driven
2
+ created: 2026-04-10
@@ -0,0 +1,43 @@
1
+ ## Context
2
+
3
+ OmniFocus supports moving tasks between containers (projects or parent tasks) and moving projects between folders via the `moveSections` and `append`/`prepend` OmniJS APIs. Currently no MCP tool exposes this capability.
4
+
5
+ The OmniJS API for moving: `moveTasks([task], destination)` where destination is a `Project` or `Task`. For projects: `moveProjects([project], folder)` where folder is a `Folder` or `null` for top-level. Direct property assignment (`task.containingProject`, `project.parentFolder`) is read-only and does not work.
6
+
7
+ ## Goals / Non-Goals
8
+
9
+ **Goals:**
10
+ - `move_task`: move a task to a project (top-level) or make it a subtask of another task
11
+ - `move_project`: move a project to a folder or to the top level
12
+
13
+ **Non-Goals:**
14
+ - Moving folders (rename/reparent of folders — separate concern)
15
+ - Reordering within a container (OmniJS exposes position but it's complex)
16
+ - Moving tags
17
+
18
+ ## Decisions
19
+
20
+ ### Decision 1: move_task requires exactly one destination
21
+
22
+ `move_task` accepts `{ id, projectId?, parentTaskId? }`. Exactly one of `projectId` or `parentTaskId` must be provided — enforced by Zod `.refine()` at the TypeScript boundary and validated again in the snippet. Returns the updated task summary.
23
+
24
+ ### Decision 2: Moving to a project places task at end of project's task list
25
+
26
+ OmniJS `task.containingProject = project` places the task at the end. This is the natural default; no position parameter needed.
27
+
28
+ ### Decision 3: move_project accepts folderId as string or null
29
+
30
+ `null` means move to top level (no parent folder). OmniJS: `project.parentFolder = null` removes the folder assignment.
31
+
32
+ ### Decision 4: Not-found errors for all ID lookups
33
+
34
+ Same pattern as existing write tools: `NotFoundError` if task, project, folder, or parentTask ID is not found. Snippet throws; bridge catches and wraps as `ExecutionError`.
35
+
36
+ ### Decision 5: Return updated summary, not full detail
37
+
38
+ `move_task` returns a `TaskSummary`. `move_project` returns a `ProjectSummary`. Consistent with create/edit patterns.
39
+
40
+ ## Risks / Trade-offs
41
+
42
+ - **OmniJS assignment semantics** — setting `task.containingProject` may behave differently than `task.parentTask`. Both need integration testing to verify the task ends up in the right place.
43
+ - **Inbox tasks** — moving an inbox task to a project is valid and expected to work. Moving a project task to inbox is not supported (OmniJS doesn't expose inbox assignment on tasks directly). Document this limitation.
@@ -0,0 +1,25 @@
1
+ ## Why
2
+
3
+ There is no way to move a task to a different project or make it a subtask, and no way to move a project to a different folder. These are common reorganization actions in OmniFocus that an LLM assistant should be able to perform on the user's behalf.
4
+
5
+ ## What Changes
6
+
7
+ - Add `move_task` tool: moves a task to a project (top-level) or makes it a subtask of another task
8
+ - Add `move_project` tool: moves a project to a folder or to the top level
9
+
10
+ ## Capabilities
11
+
12
+ ### New Capabilities
13
+
14
+ - `move-operations`: moving tasks between projects/parents and moving projects between folders
15
+
16
+ ### Modified Capabilities
17
+
18
+ _(none — new tools, no existing requirement changes)_
19
+
20
+ ## Impact
21
+
22
+ - `src/snippets/move_task.js`, `src/snippets/move_project.js`: new OmniJS snippets
23
+ - `src/tools/moveTask.ts`, `src/tools/moveProject.ts`: new tool handlers
24
+ - `src/tools/index.ts`: register new tools
25
+ - `src/runtime/snippetLoader.ts`: allowlist new snippet names
@@ -0,0 +1,41 @@
1
+ ## Requirements
2
+
3
+ ### Requirement: Move task to a new container
4
+
5
+ The system SHALL provide a `move_task` tool that accepts `{id: string, projectId?: string, parentTaskId?: string}` and moves the specified task to the given container. Exactly one of `projectId` or `parentTaskId` SHALL be provided; if both or neither are given, the tool SHALL return a validation error. When `projectId` is provided, the task SHALL become a top-level task in that project. When `parentTaskId` is provided, the task SHALL become a direct subtask of that task. The tool SHALL return a structured not-found error if any of the IDs do not correspond to existing objects.
6
+
7
+ #### Scenario: Move task to a project
8
+ - **WHEN** `move_task` is called with `{id: "t1", projectId: "p2"}`
9
+ - **THEN** the task appears as a top-level task in project p2 and is no longer in its previous container
10
+
11
+ #### Scenario: Make task a subtask
12
+ - **WHEN** `move_task` is called with `{id: "t1", parentTaskId: "t2"}`
13
+ - **THEN** the task becomes a direct child of task t2
14
+
15
+ #### Scenario: Both destinations provided is a validation error
16
+ - **WHEN** `move_task` is called with both `projectId` and `parentTaskId`
17
+ - **THEN** the tool returns a validation error before any snippet executes
18
+
19
+ #### Scenario: Non-existent task ID returns not-found error
20
+ - **WHEN** `move_task` is called with an ID that does not correspond to any task
21
+ - **THEN** the tool returns a structured not-found error
22
+
23
+ ### Requirement: Move project to a folder
24
+
25
+ The system SHALL provide a `move_project` tool that accepts `{id: string, folderId: string | null}` and moves the specified project to the given folder. When `folderId` is `null`, the project SHALL be moved to the top level (no containing folder). The tool SHALL return a structured not-found error if the project ID or folder ID does not correspond to an existing object.
26
+
27
+ #### Scenario: Move project to a folder
28
+ - **WHEN** `move_project` is called with `{id: "p1", folderId: "f2"}`
29
+ - **THEN** the project is now contained within folder f2
30
+
31
+ #### Scenario: Move project to top level
32
+ - **WHEN** `move_project` is called with `{id: "p1", folderId: null}`
33
+ - **THEN** the project has no parent folder
34
+
35
+ #### Scenario: Non-existent project ID returns not-found error
36
+ - **WHEN** `move_project` is called with a project ID that does not exist
37
+ - **THEN** the tool returns a structured not-found error
38
+
39
+ #### Scenario: Non-existent folder ID returns not-found error
40
+ - **WHEN** `move_project` is called with a folderId that does not correspond to any folder
41
+ - **THEN** the tool returns a structured not-found error