@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,115 @@
1
+ # task-write
2
+
3
+ ## Purpose
4
+
5
+ TBD — Defines tools for creating and mutating OmniFocus tasks, including create, edit, complete, drop, and delete operations.
6
+
7
+ ## Requirements
8
+
9
+ ### Requirement: Create task
10
+
11
+ The system SHALL provide a `create_task` tool that creates a new OmniFocus task and returns its full detail record. The tool SHALL accept `{name: string, note?: string, flagged?: boolean, deferDate?: string, plannedDate?: string, dueDate?: string, estimatedMinutes?: number, projectId?: string, parentTaskId?: string, tagIds?: string[], repetitionRule?: RepetitionRuleInput}`. Placement SHALL be determined as follows: if both `projectId` and `parentTaskId` are provided the tool SHALL return an error; if only `projectId` is provided the task is placed at the project root; if only `parentTaskId` is provided the task is created as a subtask inheriting its parent's project; if neither is provided the task is placed in the inbox. When `repetitionRule` is provided, the task SHALL have the specified recurrence set at creation.
12
+
13
+ #### Scenario: Create inbox task
14
+ - **WHEN** `create_task` is called with `{name: "Buy milk"}` and no `projectId` or `parentTaskId`
15
+ - **THEN** the tool creates the task in the OmniFocus inbox and returns its full detail record including a stable `id`
16
+
17
+ #### Scenario: Create task in a project
18
+ - **WHEN** `create_task` is called with `{name: "Write tests", projectId: "abc123"}`
19
+ - **THEN** the tool creates the task at the root of the specified project and returns its full detail record
20
+
21
+ #### Scenario: Create subtask
22
+ - **WHEN** `create_task` is called with `{name: "Review PR", parentTaskId: "xyz789"}`
23
+ - **THEN** the tool creates the task as a child of the specified parent task and returns its full detail record
24
+
25
+ #### Scenario: Ambiguous placement is rejected
26
+ - **WHEN** `create_task` is called with both `projectId` and `parentTaskId`
27
+ - **THEN** the tool returns a validation error before any snippet executes
28
+
29
+ #### Scenario: Non-existent project returns not-found error
30
+ - **WHEN** `create_task` is called with a `projectId` that does not correspond to any project
31
+ - **THEN** the tool returns a structured not-found error
32
+
33
+ #### Scenario: Create task with repetition rule
34
+ - **WHEN** `create_task` is called with `{ name: "Weekly review", repetitionRule: { frequency: "weekly", interval: 1, method: "start" } }`
35
+ - **THEN** the task is created with the specified recurrence and the returned TaskDetail includes the parsed repetitionRule
36
+
37
+ #### Scenario: Create task with planned date
38
+ - **WHEN** `create_task` is called with `{ name: "Clean kitchen", plannedDate: "2026-04-15T09:00:00Z" }`
39
+ - **THEN** the task is created with the specified planned date and the returned TaskDetail includes `plannedDate`
40
+
41
+ ### Requirement: Edit task
42
+
43
+ The system SHALL provide an `edit_task` tool that modifies an existing task and returns its updated full detail record. The tool SHALL accept `{id: string}` plus any subset of `{name?: string, note?: string, flagged?: boolean, deferDate?: string | null, plannedDate?: string | null, dueDate?: string | null, estimatedMinutes?: number | null, tagIds?: string[], repetitionRule?: RepetitionRuleInput | null}`. Fields omitted from the call SHALL be left unchanged. When `tagIds` is provided it SHALL replace the task's entire tag set; when omitted, tags SHALL be unchanged. Passing `null` for a date or `estimatedMinutes` SHALL clear the field. Passing `repetitionRule: null` SHALL clear the task's recurrence; passing a `RepetitionRuleInput` object SHALL set or replace the recurrence; omitting `repetitionRule` SHALL leave the existing recurrence unchanged.
44
+
45
+ #### Scenario: Edit a single field
46
+ - **WHEN** `edit_task` is called with `{id: "abc123", flagged: true}`
47
+ - **THEN** only the `flagged` field is changed; all other fields retain their previous values
48
+
49
+ #### Scenario: Replace tag set
50
+ - **WHEN** `edit_task` is called with `{id: "abc123", tagIds: ["t1", "t2"]}`
51
+ - **THEN** the task's tags are set to exactly `["t1", "t2"]`, replacing any previously assigned tags
52
+
53
+ #### Scenario: Clear a date field
54
+ - **WHEN** `edit_task` is called with `{id: "abc123", dueDate: null}`
55
+ - **THEN** the task's due date is cleared
56
+
57
+ #### Scenario: Clear planned date
58
+ - **WHEN** `edit_task` is called with `{id: "abc123", plannedDate: null}`
59
+ - **THEN** the task's planned date is cleared
60
+
61
+ #### Scenario: Non-existent task returns not-found error
62
+ - **WHEN** `edit_task` is called with an ID that does not correspond to any task
63
+ - **THEN** the tool returns a structured not-found error
64
+
65
+ #### Scenario: Non-existent tag ID returns not-found error
66
+ - **WHEN** `edit_task` is called with a `tagIds` array containing an ID that does not correspond to any tag
67
+ - **THEN** the tool returns a structured not-found error and the task is not modified
68
+
69
+ #### Scenario: Set repetition via edit
70
+ - **WHEN** `edit_task` is called with `{ id: "t1", repetitionRule: { frequency: "monthly", interval: 1, method: "dueDate" } }`
71
+ - **THEN** the task's recurrence is set and all other fields are unchanged
72
+
73
+ #### Scenario: Clear repetition via edit
74
+ - **WHEN** `edit_task` is called with `{ id: "t1", repetitionRule: null }`
75
+ - **THEN** the task's recurrence is cleared and all other fields are unchanged
76
+
77
+ ### Requirement: Complete task
78
+
79
+ The system SHALL provide a `complete_task` tool that marks an existing task complete using OmniJS `markComplete()` and returns the task's updated full detail record.
80
+
81
+ #### Scenario: Complete an existing task
82
+ - **WHEN** `complete_task` is called with the ID of an available task
83
+ - **THEN** the task's status becomes `"complete"` and the tool returns the updated detail record
84
+
85
+ #### Scenario: Non-existent task returns not-found error
86
+ - **WHEN** `complete_task` is called with an ID that does not correspond to any task
87
+ - **THEN** the tool returns a structured not-found error
88
+
89
+ ### Requirement: Drop task
90
+
91
+ The system SHALL provide a `drop_task` tool that marks an existing task dropped using OmniJS `drop()` and returns the task's updated full detail record.
92
+
93
+ #### Scenario: Drop an existing task
94
+ - **WHEN** `drop_task` is called with the ID of an available task
95
+ - **THEN** the task's status becomes `"dropped"` and the tool returns the updated detail record
96
+
97
+ #### Scenario: Non-existent task returns not-found error
98
+ - **WHEN** `drop_task` is called with an ID that does not correspond to any task
99
+ - **THEN** the tool returns a structured not-found error
100
+
101
+ ### Requirement: Delete task
102
+
103
+ The system SHALL provide a `delete_task` tool that permanently deletes a task and all its subtasks using OmniJS `deleteObject()`. The tool description SHALL instruct the AI to confirm with the user before invoking this tool, noting that deletion is permanent and includes all subtasks.
104
+
105
+ #### Scenario: Delete an existing task
106
+ - **WHEN** `delete_task` is called with the ID of an existing task
107
+ - **THEN** the task and all its subtasks are permanently removed from OmniFocus and the tool returns a confirmation envelope
108
+
109
+ #### Scenario: Delete task with subtasks removes all children
110
+ - **WHEN** `delete_task` is called with the ID of a task that has subtasks
111
+ - **THEN** the task and all descendant subtasks are deleted
112
+
113
+ #### Scenario: Non-existent task returns not-found error
114
+ - **WHEN** `delete_task` is called with an ID that does not correspond to any task
115
+ - **THEN** the tool returns a structured not-found error
@@ -0,0 +1,15 @@
1
+ # url-automation
2
+
3
+ ## Purpose
4
+
5
+ Covers `omnifocus://` URL construction and parsing. Individual tools will be defined in the `url-automation` change.
6
+
7
+ ## Requirements
8
+
9
+ ### Requirement: Capability declared
10
+
11
+ 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.
12
+
13
+ #### Scenario: Capability is named and scoped
14
+ - **WHEN** a future change proposes adding URL-automation tools
15
+ - **THEN** it lands requirements under this capability rather than inventing a new capability name
@@ -0,0 +1,15 @@
1
+ # window-state
2
+
3
+ ## Purpose
4
+
5
+ Covers read-only inspection of OmniFocus document window state. Window mutation is an explicit non-goal. Individual tools will be defined in the `perspectives-and-windows` change.
6
+
7
+ ## Requirements
8
+
9
+ ### Requirement: Capability declared
10
+
11
+ 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.
12
+
13
+ #### Scenario: Capability is named and read-only scope is fixed
14
+ - **WHEN** a future change proposes adding window-state tools
15
+ - **THEN** it lands read-only requirements under this capability; any mutation proposal requires first revisiting the non-goal in design
package/package.json ADDED
@@ -0,0 +1,32 @@
1
+ {
2
+ "name": "@scardis/omnifocus-mcp",
3
+ "version": "0.1.0",
4
+ "description": "MCP server for OmniFocus via Omni Automation JavaScript API",
5
+ "license": "MIT",
6
+ "mcpName": "io.github.steveardis/omnifocus",
7
+ "type": "module",
8
+ "engines": {
9
+ "node": ">=20"
10
+ },
11
+ "bin": {
12
+ "omnifocus-mcp": "dist/server.js"
13
+ },
14
+ "scripts": {
15
+ "build": "tsc",
16
+ "typecheck": "tsc --noEmit",
17
+ "start": "tsx src/server.ts",
18
+ "test": "vitest run --config vitest.config.ts",
19
+ "test:integration": "node -e \"if(process.platform!=='darwin'){console.error('Integration tests require macOS');process.exit(1)}\" && vitest run --config vitest.integration.config.ts",
20
+ "test:cleanup-fixtures": "tsx scripts/cleanup-fixtures.ts"
21
+ },
22
+ "dependencies": {
23
+ "@modelcontextprotocol/sdk": "^1.0.0",
24
+ "zod": "^3.23.8"
25
+ },
26
+ "devDependencies": {
27
+ "@types/node": "^22.0.0",
28
+ "tsx": "^4.19.0",
29
+ "typescript": "^5.6.0",
30
+ "vitest": "^2.1.0"
31
+ }
32
+ }
@@ -0,0 +1,89 @@
1
+ /**
2
+ * Cleanup script: removes any stale __MCP_TEST_*__ folders, __mcp_*__ projects,
3
+ * __MCP_*__ tags, and __mcp_*__ inbox tasks from OmniFocus.
4
+ * Run via: npm run test:cleanup-fixtures
5
+ */
6
+
7
+ import { spawnSync } from "child_process";
8
+
9
+ const snippet = `(() => {
10
+ function deleteFolder(f) {
11
+ f.flattenedProjects.forEach(function(p) { deleteObject(p); });
12
+ f.folders.forEach(function(child) { deleteFolder(child); });
13
+ deleteObject(f);
14
+ }
15
+ var removed = [];
16
+
17
+ // Stale test folders
18
+ flattenedFolders.filter(function(f) {
19
+ return f.name.startsWith('__MCP_TEST_') && f.name.endsWith('__');
20
+ }).forEach(function(f) { removed.push("folder: " + f.name); deleteFolder(f); });
21
+
22
+ // Stale test projects (orphaned at top level by move_project tests)
23
+ flattenedProjects.filter(function(p) {
24
+ return p.name.startsWith('__mcp_') && p.name.endsWith('__');
25
+ }).forEach(function(p) { removed.push("project: " + p.name); deleteObject(p); });
26
+
27
+ // Stale test tags
28
+ flattenedTags.filter(function(t) {
29
+ return (t.name.startsWith('__mcp_') || t.name.startsWith('__MCP_')) && t.name.endsWith('__');
30
+ }).forEach(function(t) { removed.push("tag: " + t.name); deleteObject(t); });
31
+
32
+ // Stale test inbox tasks
33
+ inbox.filter(function(t) {
34
+ return t.name.startsWith('__mcp_') && t.name.endsWith('__');
35
+ }).forEach(function(t) { removed.push("task: " + t.name); deleteObject(t); });
36
+
37
+ return JSON.stringify({ok:true,data:{removed:removed,count:removed.length}});
38
+ })()`;
39
+
40
+ const script = `
41
+ (function() {
42
+ var app = Application('OmniFocus');
43
+ app.includeStandardAdditions = true;
44
+ try {
45
+ var r = app.evaluateJavascript(${JSON.stringify(snippet)});
46
+ $.NSFileHandle.fileHandleWithStandardOutput.writeData(
47
+ $.NSString.alloc.initWithString(r+'\\n').dataUsingEncoding($.NSUTF8StringEncoding)
48
+ );
49
+ } catch(e) {
50
+ $.NSFileHandle.fileHandleWithStandardOutput.writeData(
51
+ $.NSString.alloc.initWithString(JSON.stringify({ok:false,error:{message:e.message}})+'\\n').dataUsingEncoding($.NSUTF8StringEncoding)
52
+ );
53
+ }
54
+ })();
55
+ `;
56
+
57
+ const result = spawnSync("osascript", ["-l", "JavaScript"], {
58
+ input: script,
59
+ encoding: "utf-8",
60
+ timeout: 15_000,
61
+ });
62
+
63
+ const stdout = result.stdout || "";
64
+ const line = stdout.split("\n").find((l) => l.trim().startsWith("{"));
65
+ if (!line) {
66
+ console.error("No output from OmniFocus:", stdout);
67
+ process.exit(1);
68
+ }
69
+
70
+ const parsed = JSON.parse(line) as {
71
+ ok: boolean;
72
+ data?: { removed: string[]; count: number };
73
+ error?: { message: string };
74
+ };
75
+
76
+ if (!parsed.ok) {
77
+ console.error("Cleanup failed:", parsed.error?.message);
78
+ process.exit(1);
79
+ }
80
+
81
+ const { removed, count } = parsed.data!;
82
+ if (count === 0) {
83
+ console.log("No stale test fixtures found.");
84
+ } else {
85
+ console.log(`Removed ${count} stale test fixture(s):`);
86
+ for (const item of removed) {
87
+ console.log(` - ${item}`);
88
+ }
89
+ }
package/server.json ADDED
@@ -0,0 +1,21 @@
1
+ {
2
+ "$schema": "https://static.modelcontextprotocol.io/schemas/2025-12-11/server.schema.json",
3
+ "name": "io.github.steveardis/omnifocus",
4
+ "title": "OmniFocus",
5
+ "description": "MCP server for OmniFocus (macOS task manager) via Omni Automation JavaScript API. Full CRUD on tasks, projects, folders, and tags, plus repetition rules, planned dates, filtering, and move operations.",
6
+ "repository": {
7
+ "url": "https://github.com/steveardis/omnifocus-mcp",
8
+ "source": "github"
9
+ },
10
+ "version": "0.1.0",
11
+ "packages": [
12
+ {
13
+ "registryType": "npm",
14
+ "identifier": "@scardis/omnifocus-mcp",
15
+ "version": "0.1.0",
16
+ "transport": {
17
+ "type": "stdio"
18
+ }
19
+ }
20
+ ]
21
+ }
@@ -0,0 +1,97 @@
1
+ import { spawn } from "child_process";
2
+ import { loadSnippet } from "./snippetLoader.js";
3
+ import { buildJxaScript } from "./jxaShim.js";
4
+ import { parseResultLine, ExecutionError } from "./resultProtocol.js";
5
+
6
+ const DEFAULT_TIMEOUT_MS = 30_000;
7
+
8
+ export interface RunOptions {
9
+ timeoutMs?: number;
10
+ }
11
+
12
+ /**
13
+ * Execute an OmniJS snippet inside OmniFocus via the JXA bridge.
14
+ *
15
+ * @param name - Snippet filename (without .js extension) under src/snippets/
16
+ * @param args - Arguments to inject. Embedded as a JSON literal; safe for
17
+ * all unicode, apostrophes, quotes, etc. (Design Decision 2).
18
+ * @param opts - Optional timeout override (default 30s)
19
+ * @returns The `data` field from the success envelope
20
+ * @throws ExecutionError if the snippet throws inside OmniJS
21
+ * @throws Error on timeout, process errors, or protocol violations
22
+ */
23
+ export async function runSnippet(
24
+ name: string,
25
+ args: unknown,
26
+ opts: RunOptions = {}
27
+ ): Promise<unknown> {
28
+ const timeoutMs = opts.timeoutMs ?? DEFAULT_TIMEOUT_MS;
29
+
30
+ // Load and inject args — the only interpolation allowed (Decision 2)
31
+ const template = loadSnippet(name);
32
+ const snippet = template.replace("__ARGS__", JSON.stringify(args));
33
+
34
+ // Wrap snippet in JXA shim
35
+ const jxaScript = buildJxaScript(snippet);
36
+
37
+ return new Promise((resolve, reject) => {
38
+ const MAX_OUTPUT_BYTES = 10 * 1024 * 1024; // 10 MB
39
+
40
+ const ac = new AbortController();
41
+ const timer = setTimeout(() => {
42
+ ac.abort();
43
+ child.kill("SIGKILL");
44
+ reject(new Error(`Snippet "${name}" timed out after ${timeoutMs}ms`));
45
+ }, timeoutMs);
46
+
47
+ const child = spawn("osascript", ["-l", "JavaScript"], {
48
+ signal: ac.signal,
49
+ });
50
+
51
+ let stdout = "";
52
+ let stderr = "";
53
+
54
+ child.stdout.on("data", (chunk: Buffer) => {
55
+ stdout += chunk.toString();
56
+ if (stdout.length > MAX_OUTPUT_BYTES) {
57
+ child.kill("SIGKILL");
58
+ clearTimeout(timer);
59
+ reject(new Error(`Snippet "${name}" exceeded maximum output size`));
60
+ }
61
+ });
62
+
63
+ child.stderr.on("data", (chunk: Buffer) => {
64
+ stderr += chunk.toString();
65
+ });
66
+
67
+ child.on("error", (err) => {
68
+ clearTimeout(timer);
69
+ if ((err as NodeJS.ErrnoException).code === "ABORT_ERR") return; // handled by timer
70
+ reject(err);
71
+ });
72
+
73
+ child.on("close", (code) => {
74
+ clearTimeout(timer);
75
+ try {
76
+ const envelope = parseResultLine(stdout);
77
+ if (envelope.ok) {
78
+ resolve(envelope.data);
79
+ } else {
80
+ reject(new ExecutionError(envelope.error));
81
+ }
82
+ } catch (parseErr) {
83
+ const hint = stdout.slice(0, 200) || stderr.slice(0, 200);
84
+ reject(
85
+ new Error(
86
+ `Failed to parse bridge output for snippet "${name}" (exit ${code})` +
87
+ (hint ? `: ${hint}` : "")
88
+ )
89
+ );
90
+ }
91
+ });
92
+
93
+ // Write JXA script to stdin
94
+ child.stdin.write(jxaScript);
95
+ child.stdin.end();
96
+ });
97
+ }
@@ -0,0 +1,4 @@
1
+ export { runSnippet, type RunOptions } from "./bridge.js";
2
+ export { loadSnippet, clearSnippetCache } from "./snippetLoader.js";
3
+ export { parseResultLine, ExecutionError, type ResultEnvelope } from "./resultProtocol.js";
4
+ export { buildJxaScript } from "./jxaShim.js";
@@ -0,0 +1,55 @@
1
+ /**
2
+ * JXA wrapper template. The `__SNIPPET__` placeholder is replaced with the
3
+ * OmniJS snippet to execute. The resulting script is passed to
4
+ * `osascript -l JavaScript` via stdin.
5
+ *
6
+ * This shim:
7
+ * - Calls Application('OmniFocus').evaluateJavascript() with the snippet
8
+ * - Wraps in try/catch
9
+ * - Prints exactly one line of JSON: {ok, data} | {ok: false, error: {...}}
10
+ *
11
+ * The JXA scripting dictionary is used ONLY for evaluateJavascript.
12
+ * No other scripting-dictionary domain methods are invoked here.
13
+ */
14
+ export const JXA_SHIM_TEMPLATE = `
15
+ (function() {
16
+ var snippet = __SNIPPET__;
17
+ var app = Application('OmniFocus');
18
+ app.includeStandardAdditions = true;
19
+ try {
20
+ var result = app.evaluateJavascript(snippet);
21
+ // result is already a JSON string produced by the snippet
22
+ $.NSFileHandle.fileHandleWithStandardOutput.writeData(
23
+ $.NSString.alloc.initWithString(result + '\\n')
24
+ .dataUsingEncoding($.NSUTF8StringEncoding)
25
+ );
26
+ } catch(e) {
27
+ var errName = e.name || 'Error';
28
+ var errMsg = e.message || String(e);
29
+ // OmniJS wraps thrown errors; the original name is embedded in the message
30
+ // as "SomeName: original message". Recover it when the name is generic.
31
+ var nameMatch = errMsg.match(/^([A-Z]\\w*Error): ([\\s\\S]*)$/);
32
+ if (nameMatch) { errName = nameMatch[1]; errMsg = nameMatch[2]; }
33
+ var err = JSON.stringify({
34
+ ok: false,
35
+ error: {
36
+ name: errName,
37
+ message: errMsg
38
+ }
39
+ });
40
+ $.NSFileHandle.fileHandleWithStandardOutput.writeData(
41
+ $.NSString.alloc.initWithString(err + '\\n')
42
+ .dataUsingEncoding($.NSUTF8StringEncoding)
43
+ );
44
+ }
45
+ })();
46
+ `;
47
+
48
+ /**
49
+ * Build the complete JXA script to pass to osascript.
50
+ * The snippet is embedded as a JS string literal via JSON.stringify,
51
+ * so it is safe to contain any characters including quotes, backslashes, etc.
52
+ */
53
+ export function buildJxaScript(snippet: string): string {
54
+ return JXA_SHIM_TEMPLATE.replace("__SNIPPET__", JSON.stringify(snippet));
55
+ }
@@ -0,0 +1,62 @@
1
+ import { z } from "zod";
2
+
3
+ const ErrorDetail = z.object({
4
+ name: z.string(),
5
+ message: z.string(),
6
+ stack: z.string().optional(),
7
+ });
8
+
9
+ const SuccessEnvelope = z.object({
10
+ ok: z.literal(true),
11
+ data: z.unknown(),
12
+ });
13
+
14
+ const ErrorEnvelope = z.object({
15
+ ok: z.literal(false),
16
+ error: ErrorDetail,
17
+ });
18
+
19
+ const Envelope = z.discriminatedUnion("ok", [SuccessEnvelope, ErrorEnvelope]);
20
+
21
+ export type ResultEnvelope = z.infer<typeof Envelope>;
22
+
23
+ export class ExecutionError extends Error {
24
+ readonly errorName: string;
25
+ readonly remoteStack?: string;
26
+
27
+ constructor(detail: z.infer<typeof ErrorDetail>) {
28
+ super(detail.message);
29
+ this.name = "ExecutionError";
30
+ this.errorName = detail.name;
31
+ this.remoteStack = detail.stack;
32
+ }
33
+ }
34
+
35
+ /**
36
+ * Scans stdout for the first JSON-parseable line and validates it as a result
37
+ * envelope. Ignores any preceding non-JSON chatter (e.g. osascript warnings).
38
+ */
39
+ export function parseResultLine(stdout: string): ResultEnvelope {
40
+ const lines = stdout.split("\n");
41
+ for (const line of lines) {
42
+ const trimmed = line.trim();
43
+ if (!trimmed) continue;
44
+ let parsed: unknown;
45
+ try {
46
+ parsed = JSON.parse(trimmed);
47
+ } catch {
48
+ continue; // not JSON, skip
49
+ }
50
+ const result = Envelope.safeParse(parsed);
51
+ if (result.success) {
52
+ return result.data;
53
+ }
54
+ // Parsed as JSON but not a valid envelope — this is a protocol error
55
+ throw new Error(
56
+ `Bridge returned JSON that is not a valid result envelope: ${trimmed}`
57
+ );
58
+ }
59
+ throw new Error(
60
+ `No valid JSON result envelope found in osascript output:\n${stdout}`
61
+ );
62
+ }
@@ -0,0 +1,79 @@
1
+ import { readFileSync } from "fs";
2
+ import { join, dirname } from "path";
3
+ import { fileURLToPath } from "url";
4
+
5
+ const __dirname = dirname(fileURLToPath(import.meta.url));
6
+ const SNIPPETS_DIR = join(__dirname, "..", "snippets");
7
+
8
+ // Allowlist of valid snippet names. Adding a new snippet requires an explicit
9
+ // entry here, which prevents path-traversal attacks if a snippet name ever
10
+ // reaches this loader from a dynamic source.
11
+ const ALLOWED_SNIPPETS = new Set([
12
+ "get_folder",
13
+ "get_project",
14
+ "get_tag",
15
+ "get_task",
16
+ "list_folders",
17
+ "list_projects",
18
+ "list_tags",
19
+ "list_tasks",
20
+ "resolve_name",
21
+ "create_task",
22
+ "edit_task",
23
+ "complete_task",
24
+ "drop_task",
25
+ "delete_task",
26
+ "create_project",
27
+ "edit_project",
28
+ "complete_project",
29
+ "drop_project",
30
+ "delete_project",
31
+ "create_folder",
32
+ "edit_folder",
33
+ "delete_folder",
34
+ "create_tag",
35
+ "edit_tag",
36
+ "delete_tag",
37
+ "move_task",
38
+ "move_project",
39
+ ]);
40
+
41
+ const cache = new Map<string, string>();
42
+
43
+ export function loadSnippet(name: string): string {
44
+ if (!ALLOWED_SNIPPETS.has(name)) {
45
+ throw new Error(`Unknown snippet: "${name}"`);
46
+ }
47
+
48
+ if (cache.has(name)) {
49
+ return cache.get(name)!;
50
+ }
51
+
52
+ const filePath = join(SNIPPETS_DIR, `${name}.js`);
53
+ let content: string;
54
+ try {
55
+ content = readFileSync(filePath, "utf-8");
56
+ } catch {
57
+ throw new Error(`Snippet "${name}" could not be loaded`);
58
+ }
59
+
60
+ const matches = content.split("__ARGS__").length - 1;
61
+ if (matches === 0) {
62
+ throw new Error(
63
+ `Snippet "${name}" contains no __ARGS__ placeholder. Every snippet must have exactly one.`
64
+ );
65
+ }
66
+ if (matches > 1) {
67
+ throw new Error(
68
+ `Snippet "${name}" contains ${matches} __ARGS__ placeholders. Exactly one is required.`
69
+ );
70
+ }
71
+
72
+ cache.set(name, content);
73
+ return content;
74
+ }
75
+
76
+ /** For testing: clear the in-memory cache. */
77
+ export function clearSnippetCache(): void {
78
+ cache.clear();
79
+ }
@@ -0,0 +1,32 @@
1
+ import { z } from "zod";
2
+
3
+ export const IdSchema = z.string().min(1, "ID must be a non-empty string");
4
+
5
+ export const EntityType = z.enum([
6
+ "task",
7
+ "project",
8
+ "folder",
9
+ "tag",
10
+ "perspective",
11
+ ]);
12
+
13
+ export const ProjectType = z.enum(["parallel", "sequential", "singleActions"]);
14
+
15
+ export const ProjectStatus = z.enum(["active", "onHold", "done", "dropped"]);
16
+
17
+ export const TaskStatus = z.enum([
18
+ "available",
19
+ "incomplete",
20
+ "completedByChildren",
21
+ "complete",
22
+ "dropped",
23
+ "dueSoon",
24
+ "overdue",
25
+ "flagged",
26
+ "blocked",
27
+ "next",
28
+ ]);
29
+
30
+ export const TagStatus = z.enum(["active", "onHold", "dropped"]);
31
+
32
+ export const FolderStatus = z.enum(["active", "dropped"]);
@@ -0,0 +1,38 @@
1
+ export {
2
+ IdSchema,
3
+ EntityType,
4
+ ProjectType,
5
+ ProjectStatus,
6
+ TaskStatus,
7
+ TagStatus,
8
+ FolderStatus,
9
+ } from "./enums.js";
10
+
11
+ export {
12
+ RepetitionRuleInput,
13
+ RepetitionRuleDetail,
14
+ TaskSummary,
15
+ ListTasksFilter,
16
+ TaskDetail,
17
+ CreateTaskInput,
18
+ EditTaskInput,
19
+ MoveTaskInput,
20
+ ReviewIntervalInput,
21
+ CreateProjectInput,
22
+ EditProjectInput,
23
+ MoveProjectInput,
24
+ ListProjectsFilter,
25
+ ProjectSummary,
26
+ ProjectDetail,
27
+ ListFoldersFilter,
28
+ CreateFolderInput,
29
+ EditFolderInput,
30
+ FolderSummary,
31
+ FolderDetail,
32
+ ListTagsFilter,
33
+ CreateTagInput,
34
+ EditTagInput,
35
+ TagSummary,
36
+ TagDetail,
37
+ ResolveCandidate,
38
+ } from "./shapes.js";