@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,219 @@
1
+ import { randomUUID } from "crypto";
2
+ import { runSnippet } from "../../src/runtime/bridge.js";
3
+
4
+ export interface TestFixture {
5
+ folderId: string;
6
+ folderName: string;
7
+ }
8
+
9
+ /**
10
+ * Creates a top-level OmniFocus folder named __MCP_TEST_<uuid>__.
11
+ * All test fixtures must be created inside this folder.
12
+ * Returns the folder's id.primaryKey.
13
+ */
14
+ export async function createTestFolder(): Promise<TestFixture> {
15
+ const name = `__MCP_TEST_${randomUUID()}__`;
16
+
17
+ // We need a create-folder snippet for fixtures.
18
+ // Since core CRUD lands in the next change, we inline a minimal creation here.
19
+ const { spawnSync } = await import("child_process");
20
+ const script = `
21
+ (function() {
22
+ try {
23
+ var app = Application('OmniFocus');
24
+ app.includeStandardAdditions = true;
25
+ var snippet = (function() {
26
+ var name = ${JSON.stringify(name)};
27
+ var snip = "(() => { var f = new Folder(" + JSON.stringify(name) + "); return JSON.stringify({ok:true,data:{id:f.id.primaryKey,name:f.name}}); })()";
28
+ return snip;
29
+ })();
30
+ var result = app.evaluateJavascript(snippet);
31
+ $.NSFileHandle.fileHandleWithStandardOutput.writeData(
32
+ $.NSString.alloc.initWithString(result + '\\n').dataUsingEncoding($.NSUTF8StringEncoding)
33
+ );
34
+ } catch(e) {
35
+ $.NSFileHandle.fileHandleWithStandardOutput.writeData(
36
+ $.NSString.alloc.initWithString(JSON.stringify({ok:false,error:{name:e.name||'Error',message:e.message||String(e)}}) + '\\n').dataUsingEncoding($.NSUTF8StringEncoding)
37
+ );
38
+ }
39
+ })();
40
+ `;
41
+
42
+ const result = spawnSync("osascript", ["-l", "JavaScript"], {
43
+ input: script,
44
+ encoding: "utf-8",
45
+ timeout: 15_000,
46
+ });
47
+
48
+ const stdout = result.stdout || "";
49
+ const line = stdout.split("\n").find((l) => l.trim().startsWith("{"));
50
+ if (!line) throw new Error(`createTestFolder: no JSON in output: ${stdout}`);
51
+ const parsed = JSON.parse(line) as {
52
+ ok: boolean;
53
+ data?: { id: string; name: string };
54
+ error?: { message: string };
55
+ };
56
+ if (!parsed.ok || !parsed.data) {
57
+ throw new Error(`createTestFolder failed: ${parsed.error?.message}`);
58
+ }
59
+
60
+ return { folderId: parsed.data.id, folderName: parsed.data.name };
61
+ }
62
+
63
+ /**
64
+ * Removes the test fixture folder (and all contents) by ID.
65
+ */
66
+ export async function cleanupTestFolder(folderId: string): Promise<void> {
67
+ const { spawnSync } = await import("child_process");
68
+ // deleteObject(folder) alone leaves the folder shell behind in OmniFocus.
69
+ // Must explicitly delete projects first, child folders recursively, then the folder.
70
+ const snippet = `(() => {
71
+ function deleteFolder(f) {
72
+ f.flattenedProjects.forEach(function(p) { deleteObject(p); });
73
+ f.folders.forEach(function(child) { deleteFolder(child); });
74
+ deleteObject(f);
75
+ }
76
+ var f = flattenedFolders.find(function(f){ return f.id.primaryKey === ${JSON.stringify(folderId)}; });
77
+ if (f) { deleteFolder(f); }
78
+ return JSON.stringify({ok:true,data:null});
79
+ })()`;
80
+ const script = `
81
+ (function() {
82
+ var app = Application('OmniFocus');
83
+ app.includeStandardAdditions = true;
84
+ try {
85
+ var result = app.evaluateJavascript(${JSON.stringify(snippet)});
86
+ $.NSFileHandle.fileHandleWithStandardOutput.writeData(
87
+ $.NSString.alloc.initWithString(result + '\\n').dataUsingEncoding($.NSUTF8StringEncoding)
88
+ );
89
+ } catch(e) {
90
+ $.NSFileHandle.fileHandleWithStandardOutput.writeData(
91
+ $.NSString.alloc.initWithString(JSON.stringify({ok:true,data:null}) + '\\n').dataUsingEncoding($.NSUTF8StringEncoding)
92
+ );
93
+ }
94
+ })();
95
+ `;
96
+ spawnSync("osascript", ["-l", "JavaScript"], {
97
+ input: script,
98
+ encoding: "utf-8",
99
+ timeout: 15_000,
100
+ });
101
+ }
102
+
103
+ /**
104
+ * Creates a project inside a fixture folder. Returns the project's id.primaryKey.
105
+ * Uses inline JXA so it does not depend on the create_project snippet.
106
+ */
107
+ export async function createTestProject(
108
+ folderId: string,
109
+ name: string
110
+ ): Promise<string> {
111
+ const { spawnSync } = await import("child_process");
112
+ const snippet = `(() => {
113
+ var folder = flattenedFolders.find(function(f){ return f.id.primaryKey === ${JSON.stringify(folderId)}; });
114
+ if (!folder) throw new Error("Fixture folder not found: " + ${JSON.stringify(folderId)});
115
+ var proj = new Project(${JSON.stringify(name)}, folder);
116
+ return JSON.stringify({ok:true,data:{id:proj.id.primaryKey}});
117
+ })()`;
118
+ const script = `
119
+ (function() {
120
+ var app = Application('OmniFocus');
121
+ app.includeStandardAdditions = true;
122
+ try {
123
+ var r = app.evaluateJavascript(${JSON.stringify(snippet)});
124
+ $.NSFileHandle.fileHandleWithStandardOutput.writeData($.NSString.alloc.initWithString(r+'\\n').dataUsingEncoding($.NSUTF8StringEncoding));
125
+ } catch(e) {
126
+ $.NSFileHandle.fileHandleWithStandardOutput.writeData($.NSString.alloc.initWithString(JSON.stringify({ok:false,error:{name:e.name||'Error',message:e.message||String(e)}})+'\\n').dataUsingEncoding($.NSUTF8StringEncoding));
127
+ }
128
+ })();
129
+ `;
130
+ const result = spawnSync("osascript", ["-l", "JavaScript"], {
131
+ input: script,
132
+ encoding: "utf-8",
133
+ timeout: 15_000,
134
+ });
135
+ const line = (result.stdout || "").split("\n").find((l) => l.trim().startsWith("{"));
136
+ if (!line) throw new Error(`createTestProject: no JSON in output: ${result.stdout}`);
137
+ const parsed = JSON.parse(line) as { ok: boolean; data?: { id: string }; error?: { message: string } };
138
+ if (!parsed.ok || !parsed.data) throw new Error(`createTestProject failed: ${parsed.error?.message}`);
139
+ return parsed.data.id;
140
+ }
141
+
142
+ /**
143
+ * Creates a top-level tag. Returns the tag's id.primaryKey.
144
+ * Uses inline JXA so it does not depend on the create_tag snippet.
145
+ */
146
+ export async function createTestTag(name: string): Promise<string> {
147
+ const { spawnSync } = await import("child_process");
148
+ const snippet = `(() => {
149
+ var tag = new Tag(${JSON.stringify(name)});
150
+ return JSON.stringify({ok:true,data:{id:tag.id.primaryKey}});
151
+ })()`;
152
+ const script = `
153
+ (function() {
154
+ var app = Application('OmniFocus');
155
+ app.includeStandardAdditions = true;
156
+ try {
157
+ var r = app.evaluateJavascript(${JSON.stringify(snippet)});
158
+ $.NSFileHandle.fileHandleWithStandardOutput.writeData($.NSString.alloc.initWithString(r+'\\n').dataUsingEncoding($.NSUTF8StringEncoding));
159
+ } catch(e) {
160
+ $.NSFileHandle.fileHandleWithStandardOutput.writeData($.NSString.alloc.initWithString(JSON.stringify({ok:false,error:{name:e.name||'Error',message:e.message||String(e)}})+'\\n').dataUsingEncoding($.NSUTF8StringEncoding));
161
+ }
162
+ })();
163
+ `;
164
+ const result = spawnSync("osascript", ["-l", "JavaScript"], {
165
+ input: script,
166
+ encoding: "utf-8",
167
+ timeout: 15_000,
168
+ });
169
+ const line = (result.stdout || "").split("\n").find((l) => l.trim().startsWith("{"));
170
+ if (!line) throw new Error(`createTestTag: no JSON in output: ${result.stdout}`);
171
+ const parsed = JSON.parse(line) as { ok: boolean; data?: { id: string }; error?: { message: string } };
172
+ if (!parsed.ok || !parsed.data) throw new Error(`createTestTag failed: ${parsed.error?.message}`);
173
+ return parsed.data.id;
174
+ }
175
+
176
+ /**
177
+ * Deletes a tag by id.primaryKey. Silent if not found.
178
+ * Uses inline JXA so it does not depend on the delete_tag snippet.
179
+ */
180
+ export async function deleteTestTag(tagId: string): Promise<void> {
181
+ const { spawnSync } = await import("child_process");
182
+ const snippet = `(() => {
183
+ var tag = flattenedTags.find(function(t){ return t.id.primaryKey === ${JSON.stringify(tagId)}; });
184
+ if (tag) deleteObject(tag);
185
+ return JSON.stringify({ok:true,data:null});
186
+ })()`;
187
+ const script = `
188
+ (function() {
189
+ var app = Application('OmniFocus');
190
+ app.includeStandardAdditions = true;
191
+ try {
192
+ var r = app.evaluateJavascript(${JSON.stringify(snippet)});
193
+ $.NSFileHandle.fileHandleWithStandardOutput.writeData($.NSString.alloc.initWithString(r+'\\n').dataUsingEncoding($.NSUTF8StringEncoding));
194
+ } catch(e) {
195
+ $.NSFileHandle.fileHandleWithStandardOutput.writeData($.NSString.alloc.initWithString(JSON.stringify({ok:true,data:null})+'\\n').dataUsingEncoding($.NSUTF8StringEncoding));
196
+ }
197
+ })();
198
+ `;
199
+ spawnSync("osascript", ["-l", "JavaScript"], {
200
+ input: script,
201
+ encoding: "utf-8",
202
+ timeout: 15_000,
203
+ });
204
+ }
205
+
206
+ /**
207
+ * Helper: creates a fixture folder, runs fn, cleans up in afterAll.
208
+ * Usage: const fixture = await withTestFolder(async (f) => { ... });
209
+ */
210
+ export async function withTestFolder<T>(
211
+ fn: (fixture: TestFixture) => Promise<T>
212
+ ): Promise<T> {
213
+ const fixture = await createTestFolder();
214
+ try {
215
+ return await fn(fixture);
216
+ } finally {
217
+ await cleanupTestFolder(fixture.folderId);
218
+ }
219
+ }
@@ -0,0 +1,64 @@
1
+ import { describe, it, expect, beforeAll, afterAll } from "vitest";
2
+ import { runSnippet } from "../../src/runtime/bridge.js";
3
+ import { createTestFolder, cleanupTestFolder, type TestFixture } from "./fixtures.js";
4
+ import { TaskDetail } from "../../src/schemas/index.js";
5
+
6
+ /**
7
+ * Integration test: get_task
8
+ *
9
+ * Creates a task with apostrophes and unicode in its name, retrieves by ID,
10
+ * asserts the name survives the round-trip. Proves Decision 2 end-to-end.
11
+ */
12
+ describe("get_task (integration)", () => {
13
+ let fixture: TestFixture;
14
+ let taskId: string;
15
+ const taskName = "Finn's \"birthday\" — ▸ 🎉";
16
+ const taskNote = "Note with apostrophe's and line1\nline2";
17
+
18
+ beforeAll(async () => {
19
+ fixture = await createTestFolder();
20
+
21
+ // Create a project then a task inside it
22
+ const { spawnSync } = await import("child_process");
23
+ const createSnippet = `(() => {
24
+ var folder = flattenedFolders.find(function(f){ return f.id.primaryKey === ${JSON.stringify(fixture.folderId)}; });
25
+ if (!folder) throw new Error("Fixture folder not found");
26
+ var proj = new Project("TestProject", folder);
27
+ var task = new Task(${JSON.stringify(taskName)}, proj);
28
+ task.note = ${JSON.stringify(taskNote)};
29
+ return JSON.stringify({ok:true,data:{id:task.id.primaryKey}});
30
+ })()`;
31
+ const script = `
32
+ (function() {
33
+ var app = Application('OmniFocus');
34
+ app.includeStandardAdditions = true;
35
+ try {
36
+ var r = app.evaluateJavascript(${JSON.stringify(createSnippet)});
37
+ $.NSFileHandle.fileHandleWithStandardOutput.writeData($.NSString.alloc.initWithString(r+'\\n').dataUsingEncoding($.NSUTF8StringEncoding));
38
+ } catch(e) {
39
+ $.NSFileHandle.fileHandleWithStandardOutput.writeData($.NSString.alloc.initWithString(JSON.stringify({ok:false,error:{name:e.name,message:e.message}})+'\\n').dataUsingEncoding($.NSUTF8StringEncoding));
40
+ }
41
+ })();
42
+ `;
43
+ const result = spawnSync("osascript", ["-l", "JavaScript"], {
44
+ input: script, encoding: "utf-8", timeout: 15_000,
45
+ });
46
+ const line = (result.stdout || "").split("\n").find((l) => l.trim().startsWith("{"));
47
+ if (!line) throw new Error(`Could not create fixture task: ${result.stdout}`);
48
+ const parsed = JSON.parse(line) as { ok: boolean; data?: { id: string }; error?: { message: string } };
49
+ if (!parsed.ok || !parsed.data) throw new Error(`Create task failed: ${parsed.error?.message}`);
50
+ taskId = parsed.data.id;
51
+ });
52
+
53
+ afterAll(async () => {
54
+ await cleanupTestFolder(fixture.folderId);
55
+ });
56
+
57
+ it("retrieves task by ID with name and note intact (proves apostrophe/unicode safety)", async () => {
58
+ const raw = await runSnippet("get_task", { id: taskId });
59
+ const task = TaskDetail.parse(raw);
60
+ expect(task.id).toBe(taskId);
61
+ expect(task.name).toBe(taskName);
62
+ expect(task.note).toBe(taskNote);
63
+ });
64
+ });
@@ -0,0 +1,98 @@
1
+ import { describe, it, expect, beforeAll, afterAll } from "vitest";
2
+ import { spawnSync } from "child_process";
3
+ import { runSnippet } from "../../src/runtime/bridge.js";
4
+ import { createTestFolder, cleanupTestFolder, type TestFixture } from "./fixtures.js";
5
+ import { FolderSummary } from "../../src/schemas/index.js";
6
+ import { z } from "zod";
7
+
8
+ const FolderSummaryArray = z.array(FolderSummary);
9
+
10
+ /**
11
+ * Creates a folder and immediately drops it (active = false).
12
+ * Returns the folder id.
13
+ */
14
+ async function createDroppedFolder(name: string): Promise<string> {
15
+ const snippet = `(() => {
16
+ var f = new Folder(${JSON.stringify(name)});
17
+ f.active = false;
18
+ return JSON.stringify({ok:true,data:{id:f.id.primaryKey}});
19
+ })()`;
20
+ const script = `(function(){
21
+ var app = Application('OmniFocus');
22
+ app.includeStandardAdditions = true;
23
+ try {
24
+ var r = app.evaluateJavascript(${JSON.stringify(snippet)});
25
+ $.NSFileHandle.fileHandleWithStandardOutput.writeData($.NSString.alloc.initWithString(r+'\\n').dataUsingEncoding($.NSUTF8StringEncoding));
26
+ } catch(e) {
27
+ $.NSFileHandle.fileHandleWithStandardOutput.writeData($.NSString.alloc.initWithString(JSON.stringify({ok:false,error:{message:String(e)}})+'\\n').dataUsingEncoding($.NSUTF8StringEncoding));
28
+ }
29
+ })();`;
30
+ const result = spawnSync("osascript", ["-l", "JavaScript"], { input: script, encoding: "utf-8", timeout: 15_000 });
31
+ const line = (result.stdout || "").split("\n").find((l) => l.trim().startsWith("{"));
32
+ if (!line) throw new Error(`createDroppedFolder: no JSON in output: ${result.stdout}`);
33
+ const parsed = JSON.parse(line) as { ok: boolean; data?: { id: string }; error?: { message: string } };
34
+ if (!parsed.ok || !parsed.data) throw new Error(`createDroppedFolder failed: ${parsed.error?.message}`);
35
+ return parsed.data.id;
36
+ }
37
+
38
+ async function deleteFolder(folderId: string): Promise<void> {
39
+ const snippet = `(() => {
40
+ var f = flattenedFolders.find(function(f){ return f.id.primaryKey === ${JSON.stringify(folderId)}; });
41
+ if (f) deleteObject(f);
42
+ return JSON.stringify({ok:true,data:null});
43
+ })()`;
44
+ const script = `(function(){
45
+ var app = Application('OmniFocus');
46
+ app.includeStandardAdditions = true;
47
+ try {
48
+ var r = app.evaluateJavascript(${JSON.stringify(snippet)});
49
+ $.NSFileHandle.fileHandleWithStandardOutput.writeData($.NSString.alloc.initWithString(r+'\\n').dataUsingEncoding($.NSUTF8StringEncoding));
50
+ } catch(_) {}
51
+ })();`;
52
+ spawnSync("osascript", ["-l", "JavaScript"], { input: script, encoding: "utf-8", timeout: 15_000 });
53
+ }
54
+
55
+ describe("list_folders filtering (integration)", () => {
56
+ let activeFixture: TestFixture;
57
+ let droppedFolderId: string;
58
+
59
+ beforeAll(async () => {
60
+ activeFixture = await createTestFolder();
61
+ droppedFolderId = await createDroppedFolder(`__MCP_DROPPED_FOLDER_TEST__`);
62
+ });
63
+
64
+ afterAll(async () => {
65
+ await cleanupTestFolder(activeFixture.folderId);
66
+ await deleteFolder(droppedFolderId);
67
+ });
68
+
69
+ it("no filter returns all folders including dropped", async () => {
70
+ const raw = await runSnippet("list_folders", {});
71
+ const folders = FolderSummaryArray.parse(raw);
72
+ const ids = folders.map((f) => f.id);
73
+ expect(ids).toContain(activeFixture.folderId);
74
+ expect(ids).toContain(droppedFolderId);
75
+ });
76
+
77
+ it("status filter 'active' returns only active folders", async () => {
78
+ const raw = await runSnippet("list_folders", { filter: { status: "active" } });
79
+ const folders = FolderSummaryArray.parse(raw);
80
+ expect(folders.every((f) => f.status === "active")).toBe(true);
81
+ expect(folders.some((f) => f.id === activeFixture.folderId)).toBe(true);
82
+ expect(folders.some((f) => f.id === droppedFolderId)).toBe(false);
83
+ });
84
+
85
+ it("status filter 'dropped' returns only dropped folders", async () => {
86
+ const raw = await runSnippet("list_folders", { filter: { status: "dropped" } });
87
+ const folders = FolderSummaryArray.parse(raw);
88
+ expect(folders.every((f) => f.status === "dropped")).toBe(true);
89
+ expect(folders.some((f) => f.id === droppedFolderId)).toBe(true);
90
+ expect(folders.some((f) => f.id === activeFixture.folderId)).toBe(false);
91
+ });
92
+
93
+ it("limit caps the number of returned folders", async () => {
94
+ const raw = await runSnippet("list_folders", { limit: 1 });
95
+ const folders = FolderSummaryArray.parse(raw);
96
+ expect(folders.length).toBeLessThanOrEqual(1);
97
+ });
98
+ });
@@ -0,0 +1,73 @@
1
+ import { describe, it, expect, beforeAll, afterAll } from "vitest";
2
+ import { runSnippet } from "../../src/runtime/bridge.js";
3
+ import { createTestFolder, cleanupTestFolder, type TestFixture } from "./fixtures.js";
4
+ import { z } from "zod";
5
+ import { ProjectSummary } from "../../src/schemas/index.js";
6
+
7
+ /**
8
+ * Integration test: list_projects
9
+ *
10
+ * Creates a fixture folder with a child project, calls the real bridge,
11
+ * asserts the fixture project appears with the correct folder path.
12
+ */
13
+ describe("list_projects (integration)", () => {
14
+ let fixture: TestFixture;
15
+ let projectId: string;
16
+ const projectName = `TestProject_${Date.now()}`;
17
+
18
+ beforeAll(async () => {
19
+ fixture = await createTestFolder();
20
+
21
+ // Create a project inside the fixture folder via inline snippet
22
+ const { spawnSync } = await import("child_process");
23
+ const folderName = fixture.folderName;
24
+ const createSnippet = `(() => {
25
+ var folder = flattenedFolders.find(function(f){ return f.id.primaryKey === ${JSON.stringify(fixture.folderId)}; });
26
+ if (!folder) throw new Error("Fixture folder not found");
27
+ var p = new Project(${JSON.stringify(projectName)}, folder);
28
+ return JSON.stringify({ok:true,data:{id:p.id.primaryKey}});
29
+ })()`;
30
+ const script = `
31
+ (function() {
32
+ var app = Application('OmniFocus');
33
+ app.includeStandardAdditions = true;
34
+ try {
35
+ var r = app.evaluateJavascript(${JSON.stringify(createSnippet)});
36
+ $.NSFileHandle.fileHandleWithStandardOutput.writeData($.NSString.alloc.initWithString(r+'\\n').dataUsingEncoding($.NSUTF8StringEncoding));
37
+ } catch(e) {
38
+ $.NSFileHandle.fileHandleWithStandardOutput.writeData($.NSString.alloc.initWithString(JSON.stringify({ok:false,error:{name:e.name,message:e.message}})+'\\n').dataUsingEncoding($.NSUTF8StringEncoding));
39
+ }
40
+ })();
41
+ `;
42
+ const result = spawnSync("osascript", ["-l", "JavaScript"], {
43
+ input: script, encoding: "utf-8", timeout: 15_000,
44
+ });
45
+ const line = (result.stdout || "").split("\n").find((l) => l.trim().startsWith("{"));
46
+ if (!line) throw new Error(`Could not create fixture project: ${result.stdout}`);
47
+ const parsed = JSON.parse(line) as { ok: boolean; data?: { id: string }; error?: { message: string } };
48
+ if (!parsed.ok || !parsed.data) throw new Error(`Create project failed: ${parsed.error?.message}`);
49
+ projectId = parsed.data.id;
50
+ });
51
+
52
+ afterAll(async () => {
53
+ await cleanupTestFolder(fixture.folderId);
54
+ });
55
+
56
+ it("returns the fixture project with correct folder path", async () => {
57
+ const raw = await runSnippet("list_projects", {});
58
+ const projects = z.array(ProjectSummary).parse(raw);
59
+ const found = projects.find((p) => p.id === projectId);
60
+ expect(found).toBeDefined();
61
+ expect(found!.name).toBe(projectName);
62
+ expect(found!.folderPath).toBe(fixture.folderName);
63
+ });
64
+
65
+ it("returns id.primaryKey for every project", async () => {
66
+ const raw = await runSnippet("list_projects", {});
67
+ const projects = z.array(ProjectSummary).parse(raw);
68
+ for (const p of projects) {
69
+ expect(p.id).toBeTruthy();
70
+ expect(typeof p.id).toBe("string");
71
+ }
72
+ });
73
+ });
@@ -0,0 +1,96 @@
1
+ import { describe, it, expect, beforeAll, afterAll } from "vitest";
2
+ import { runSnippet } from "../../src/runtime/bridge.js";
3
+ import { createTestFolder, cleanupTestFolder, createTestProject, type TestFixture } from "./fixtures.js";
4
+ import { ProjectSummary } from "../../src/schemas/index.js";
5
+ import { z } from "zod";
6
+
7
+ const ProjectSummaryArray = z.array(ProjectSummary);
8
+
9
+ describe("list_projects filtering (integration)", () => {
10
+ let fixture: TestFixture;
11
+ let activeProjectId: string;
12
+ let flaggedProjectId: string;
13
+ let completedProjectId: string;
14
+
15
+ beforeAll(async () => {
16
+ fixture = await createTestFolder();
17
+
18
+ // Active unflagged project
19
+ activeProjectId = await createTestProject(fixture.folderId, "__mcp_active_proj__");
20
+
21
+ // Flagged project
22
+ const flaggedRaw = await runSnippet("create_project", {
23
+ name: "__mcp_flagged_proj__",
24
+ folderId: fixture.folderId,
25
+ flagged: true,
26
+ });
27
+ flaggedProjectId = (flaggedRaw as { id: string }).id;
28
+
29
+ // Completed project
30
+ completedProjectId = await createTestProject(fixture.folderId, "__mcp_done_proj__");
31
+ await runSnippet("complete_project", { id: completedProjectId });
32
+ });
33
+
34
+ afterAll(async () => {
35
+ await cleanupTestFolder(fixture.folderId);
36
+ });
37
+
38
+ it("returns folderId and flagged on each project summary", async () => {
39
+ const raw = await runSnippet("list_projects", { filter: { folderId: fixture.folderId } });
40
+ const projects = ProjectSummaryArray.parse(raw);
41
+ expect(projects.length).toBeGreaterThan(0);
42
+ for (const p of projects) {
43
+ expect(p).toHaveProperty("folderId");
44
+ expect(p).toHaveProperty("flagged");
45
+ expect(typeof p.flagged).toBe("boolean");
46
+ }
47
+ });
48
+
49
+ it("default excludes done and dropped projects", async () => {
50
+ const raw = await runSnippet("list_projects", { filter: { folderId: fixture.folderId } });
51
+ const projects = ProjectSummaryArray.parse(raw);
52
+ expect(projects.some((p) => p.id === completedProjectId)).toBe(false);
53
+ expect(projects.some((p) => p.id === activeProjectId)).toBe(true);
54
+ });
55
+
56
+ it("explicit status filter retrieves done projects", async () => {
57
+ const raw = await runSnippet("list_projects", {
58
+ filter: { folderId: fixture.folderId, status: ["done"] },
59
+ });
60
+ const projects = ProjectSummaryArray.parse(raw);
61
+ expect(projects.some((p) => p.id === completedProjectId)).toBe(true);
62
+ expect(projects.every((p) => p.status === "done")).toBe(true);
63
+ });
64
+
65
+ it("folderId filter returns only projects in that subtree", async () => {
66
+ const raw = await runSnippet("list_projects", { filter: { folderId: fixture.folderId } });
67
+ const projects = ProjectSummaryArray.parse(raw);
68
+ expect(projects.every((p) => p.folderId === fixture.folderId)).toBe(true);
69
+ });
70
+
71
+ it("flagged filter returns only flagged projects", async () => {
72
+ const raw = await runSnippet("list_projects", {
73
+ filter: { folderId: fixture.folderId, flagged: true },
74
+ });
75
+ const projects = ProjectSummaryArray.parse(raw);
76
+ expect(projects.length).toBeGreaterThan(0);
77
+ expect(projects.every((p) => p.flagged)).toBe(true);
78
+ expect(projects.some((p) => p.id === flaggedProjectId)).toBe(true);
79
+ expect(projects.some((p) => p.id === activeProjectId)).toBe(false);
80
+ });
81
+
82
+ it("limit caps the number of returned projects", async () => {
83
+ const raw = await runSnippet("list_projects", { limit: 1 });
84
+ const projects = ProjectSummaryArray.parse(raw);
85
+ expect(projects.length).toBeLessThanOrEqual(1);
86
+ });
87
+
88
+ it("non-existent folderId returns not-found error", async () => {
89
+ await expect(
90
+ runSnippet("list_projects", { filter: { folderId: "nonexistent-id-xyz" } })
91
+ ).rejects.toSatisfy((e: unknown) => {
92
+ const err = e as Record<string, unknown>;
93
+ return err.name === "ExecutionError" && err.errorName === "NotFoundError";
94
+ });
95
+ });
96
+ });
@@ -0,0 +1,54 @@
1
+ import { describe, it, expect, beforeAll, afterAll } from "vitest";
2
+ import { runSnippet } from "../../src/runtime/bridge.js";
3
+ import { createTestTag, deleteTestTag } from "./fixtures.js";
4
+ import { TagSummary } from "../../src/schemas/index.js";
5
+ import { z } from "zod";
6
+
7
+ const TagSummaryArray = z.array(TagSummary);
8
+
9
+ describe("list_tags filtering (integration)", () => {
10
+ let activeTagId: string;
11
+ let onHoldTagId: string;
12
+
13
+ beforeAll(async () => {
14
+ activeTagId = await createTestTag("__MCP_ACTIVE_TAG_TEST__");
15
+ onHoldTagId = await createTestTag("__MCP_ONHOLD_TAG_TEST__");
16
+ // Set onHold via edit_tag
17
+ await runSnippet("edit_tag", { id: onHoldTagId, status: "onHold" });
18
+ });
19
+
20
+ afterAll(async () => {
21
+ await deleteTestTag(activeTagId);
22
+ await deleteTestTag(onHoldTagId);
23
+ });
24
+
25
+ it("no filter returns all tags including onHold", async () => {
26
+ const raw = await runSnippet("list_tags", {});
27
+ const tags = TagSummaryArray.parse(raw);
28
+ const ids = tags.map((t) => t.id);
29
+ expect(ids).toContain(activeTagId);
30
+ expect(ids).toContain(onHoldTagId);
31
+ });
32
+
33
+ it("status filter 'active' returns only active tags", async () => {
34
+ const raw = await runSnippet("list_tags", { filter: { status: "active" } });
35
+ const tags = TagSummaryArray.parse(raw);
36
+ expect(tags.every((t) => t.status === "active")).toBe(true);
37
+ expect(tags.some((t) => t.id === activeTagId)).toBe(true);
38
+ expect(tags.some((t) => t.id === onHoldTagId)).toBe(false);
39
+ });
40
+
41
+ it("status filter 'onHold' returns only onHold tags", async () => {
42
+ const raw = await runSnippet("list_tags", { filter: { status: "onHold" } });
43
+ const tags = TagSummaryArray.parse(raw);
44
+ expect(tags.every((t) => t.status === "onHold")).toBe(true);
45
+ expect(tags.some((t) => t.id === onHoldTagId)).toBe(true);
46
+ expect(tags.some((t) => t.id === activeTagId)).toBe(false);
47
+ });
48
+
49
+ it("limit caps the number of returned tags", async () => {
50
+ const raw = await runSnippet("list_tags", { limit: 1 });
51
+ const tags = TagSummaryArray.parse(raw);
52
+ expect(tags.length).toBeLessThanOrEqual(1);
53
+ });
54
+ });