omnifocus-mcp-enhanced 1.6.6 โ†’ 1.6.8

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 (51) hide show
  1. package/README.md +64 -20
  2. package/README.zh.md +47 -20
  3. package/dist/server.js +2 -0
  4. package/dist/tools/definitions/editItem.js +5 -0
  5. package/dist/tools/definitions/getCustomPerspectiveTasks.js +15 -1
  6. package/dist/tools/definitions/getCustomPerspectiveTasks.test.js +32 -0
  7. package/dist/tools/definitions/getPerspectiveTasksV2.js +15 -3
  8. package/dist/tools/definitions/moveTask.js +54 -0
  9. package/dist/tools/definitions/moveTask.test.js +27 -0
  10. package/dist/tools/definitions/plannedDateSchemas.test.js +10 -0
  11. package/dist/tools/primitives/addOmniFocusTask.js +11 -9
  12. package/dist/tools/primitives/addOmniFocusTask.test.js +18 -1
  13. package/dist/tools/primitives/addProject.js +11 -9
  14. package/dist/tools/primitives/addProject.test.js +20 -0
  15. package/dist/tools/primitives/editItem.js +209 -42
  16. package/dist/tools/primitives/editItem.test.js +82 -0
  17. package/dist/tools/primitives/getCustomPerspectiveTasks.js +119 -223
  18. package/dist/tools/primitives/getPerspectiveTasksV2.js +17 -2
  19. package/dist/tools/primitives/getPerspectiveTasksV2.test.js +103 -0
  20. package/dist/tools/primitives/moveTask.js +55 -0
  21. package/dist/tools/primitives/moveTask.test.js +29 -0
  22. package/dist/tools/primitives/perspectiveTaskTree.js +168 -0
  23. package/dist/tools/primitives/perspectiveTaskTree.test.js +37 -0
  24. package/dist/utils/dateFormatter.js +32 -0
  25. package/dist/utils/dateFormatter.test.js +15 -1
  26. package/docs/roadmap/2026-02-25-batch-move-tasks-plan.md +55 -0
  27. package/docs/roadmap/2026-02-25-batch-move-tasks-plan.zh.md +55 -0
  28. package/package.json +1 -1
  29. package/src/server.ts +8 -0
  30. package/src/tools/definitions/editItem.ts +5 -0
  31. package/src/tools/definitions/getCustomPerspectiveTasks.test.ts +40 -0
  32. package/src/tools/definitions/getCustomPerspectiveTasks.ts +21 -2
  33. package/src/tools/definitions/getPerspectiveTasksV2.ts +19 -4
  34. package/src/tools/definitions/moveTask.test.ts +33 -0
  35. package/src/tools/definitions/moveTask.ts +63 -0
  36. package/src/tools/definitions/plannedDateSchemas.test.ts +12 -0
  37. package/src/tools/primitives/addOmniFocusTask.test.ts +22 -1
  38. package/src/tools/primitives/addOmniFocusTask.ts +11 -9
  39. package/src/tools/primitives/addProject.test.ts +24 -0
  40. package/src/tools/primitives/addProject.ts +11 -9
  41. package/src/tools/primitives/editItem.test.ts +99 -0
  42. package/src/tools/primitives/editItem.ts +228 -42
  43. package/src/tools/primitives/getCustomPerspectiveTasks.ts +162 -245
  44. package/src/tools/primitives/getPerspectiveTasksV2.test.ts +109 -0
  45. package/src/tools/primitives/getPerspectiveTasksV2.ts +33 -3
  46. package/src/tools/primitives/moveTask.test.ts +35 -0
  47. package/src/tools/primitives/moveTask.ts +80 -0
  48. package/src/tools/primitives/perspectiveTaskTree.test.ts +44 -0
  49. package/src/tools/primitives/perspectiveTaskTree.ts +273 -0
  50. package/src/utils/dateFormatter.test.ts +18 -1
  51. package/src/utils/dateFormatter.ts +38 -0
package/README.md CHANGED
@@ -17,6 +17,7 @@ Enhanced Model Context Protocol (MCP) server for OmniFocus featuring **native cu
17
17
 
18
18
  ## ๐Ÿ†• Latest Release
19
19
 
20
+ - **v1.6.8** - Added stable task move support via `move_task` and `edit_item` (`newProjectId/newProjectName/newParentTaskId/newParentTaskName/moveToInbox`) with duplicate-name protection and cycle-prevention checks.
20
21
  - **v1.6.6** - Added full Planned Date support (create/edit/read/filter/sort/export), including `plannedDate`/`newPlannedDate` and updated task displays.
21
22
 
22
23
  ## โœจ Key Features
@@ -156,28 +157,31 @@ Access your OmniFocus custom perspectives with hierarchical task display:
156
157
  # ๐ŸŒŸ NEW: List all your custom perspectives
157
158
  list_custom_perspectives {"format": "detailed"}
158
159
 
159
- # ๐ŸŒณ NEW: Get tasks from custom perspective with tree display
160
+ # ๐ŸŒณ NEW: Project tree view (default)
160
161
  get_custom_perspective_tasks {
161
162
  "perspectiveName": "ไปŠๆ—ฅๅทฅไฝœๅฎ‰ๆŽ’", # Your custom perspective name
162
- "showHierarchy": true, # Enable tree display
163
+ "displayMode": "project_tree", # project_tree | task_tree | flat
163
164
  "hideCompleted": true
164
165
  }
165
166
 
166
- # Examples with different perspectives
167
+ # Global task tree (legacy showHierarchy=true equivalent)
167
168
  get_custom_perspective_tasks {
168
169
  "perspectiveName": "Today Review",
169
- "showHierarchy": true
170
+ "displayMode": "task_tree"
170
171
  }
171
172
 
173
+ # Flat list (legacy groupByProject=false equivalent)
172
174
  get_custom_perspective_tasks {
173
175
  "perspectiveName": "Weekly Planning",
174
- "showHierarchy": false # Flat display
176
+ "displayMode": "flat"
175
177
  }
176
178
  ```
177
179
 
178
180
  **Why This Is Powerful:**
179
181
  - โœ… **Native Integration** - Uses OmniFocus `Perspective.Custom` API directly
180
182
  - โœ… **Tree Structure** - Visual parent-child task relationships with โ”œโ”€, โ””โ”€ symbols
183
+ - โœ… **Project-First Grouping** - Project header first, then nested subtasks
184
+ - โœ… **Readable Metadata** - Full notes and `#tags` in tree output
181
185
  - โœ… **AI-Friendly** - Enhanced descriptions prevent tool selection confusion
182
186
  - โœ… **Professional Output** - Clean, readable task hierarchies
183
187
 
@@ -218,24 +222,27 @@ Efficiently manage multiple tasks:
218
222
  2. **add_omnifocus_task** - Create tasks (enhanced with subtask support)
219
223
  3. **add_project** - Create projects
220
224
  4. **remove_item** - Delete tasks or projects
221
- 5. **edit_item** - Edit tasks or projects
222
- 6. **batch_add_items** - Bulk add (enhanced with subtask support)
223
- 7. **batch_remove_items** - Bulk remove
224
- 8. **get_task_by_id** - Query task information
225
+ 5. **edit_item** - Edit tasks or projects (now supports task moves: project/parent/inbox)
226
+ 6. **move_task** - Move an existing task to project/parent task/inbox
227
+ 7. **batch_add_items** - Bulk add (enhanced with subtask support)
228
+ 8. **batch_remove_items** - Bulk remove
229
+ 9. **get_task_by_id** - Query task information
225
230
 
226
231
  ### ๐Ÿ” Built-in Perspective Tools
227
- 9. **get_inbox_tasks** - Inbox perspective
228
- 10. **get_flagged_tasks** - Flagged perspective
229
- 11. **get_forecast_tasks** - Forecast perspective (due/deferred/planned task data included)
230
- 12. **get_tasks_by_tag** - Tag-based filtering
231
- 13. **filter_tasks** - Ultimate filtering with unlimited combinations
232
+ 10. **get_inbox_tasks** - Inbox perspective
233
+ 11. **get_flagged_tasks** - Flagged perspective
234
+ 12. **get_forecast_tasks** - Forecast perspective (due/deferred/planned task data included)
235
+ 13. **get_tasks_by_tag** - Tag-based filtering
236
+ 14. **filter_tasks** - Ultimate filtering with unlimited combinations
232
237
 
233
238
  ### ๐ŸŒŸ Custom Perspective Tools (NEW)
234
- 14. **list_custom_perspectives** - ๐ŸŒŸ **NEW**: List all custom perspectives with details
235
- 15. **get_custom_perspective_tasks** - ๐ŸŒŸ **NEW**: Access custom perspective with hierarchical display
239
+ 15. **list_custom_perspectives** - ๐ŸŒŸ **NEW**: List all custom perspectives with details
240
+ 16. **get_custom_perspective_tasks** - ๐ŸŒŸ **NEW**: Access custom perspective with hierarchical display
236
241
 
237
242
  ### ๐Ÿ“Š Analytics & Tracking
238
- 16. **get_today_completed_tasks** - View today's completed tasks
243
+ 17. **get_today_completed_tasks** - View today's completed tasks
244
+
245
+ Batch move feature roadmap (future): [docs/roadmap/2026-02-25-batch-move-tasks-plan.md](docs/roadmap/2026-02-25-batch-move-tasks-plan.md)
239
246
 
240
247
  ## ๐Ÿš€ Quick Start Examples
241
248
 
@@ -269,6 +276,43 @@ add_omnifocus_task {
269
276
  }
270
277
  ```
271
278
 
279
+ ### Task Move Operations
280
+ ```bash
281
+ # Move task to a project
282
+ move_task {
283
+ "id": "task-id-123",
284
+ "targetProjectName": "Planning"
285
+ }
286
+
287
+ # Move task under another task
288
+ move_task {
289
+ "id": "task-id-123",
290
+ "targetParentTaskId": "parent-task-id-456"
291
+ }
292
+
293
+ # Move task back to inbox
294
+ move_task {
295
+ "id": "task-id-123",
296
+ "targetInbox": true
297
+ }
298
+ ```
299
+
300
+ Task move safety rules:
301
+ - Name lookups fail fast on duplicates and ask you to use IDs.
302
+ - Destination must be exactly one type: project OR parent task OR inbox.
303
+ - Moving a task into itself/its descendants is blocked to prevent cycles.
304
+
305
+ You can also move with `edit_item` and combine move + field updates:
306
+ ```bash
307
+ edit_item {
308
+ "itemType": "task",
309
+ "id": "task-id-123",
310
+ "newProjectName": "Planning",
311
+ "newName": "Review tmux workflow",
312
+ "newFlagged": true
313
+ }
314
+ ```
315
+
272
316
  ### Smart Task Discovery
273
317
  ```bash
274
318
  # Find high-priority work
@@ -292,17 +336,17 @@ filter_tasks {
292
336
  # List your custom perspectives
293
337
  list_custom_perspectives {"format": "detailed"}
294
338
 
295
- # Access a custom perspective with hierarchy
339
+ # Access a custom perspective with project tree
296
340
  get_custom_perspective_tasks {
297
341
  "perspectiveName": "Today Review",
298
- "showHierarchy": true,
342
+ "displayMode": "project_tree",
299
343
  "hideCompleted": true
300
344
  }
301
345
 
302
346
  # Quick flat view of weekly planning
303
347
  get_custom_perspective_tasks {
304
348
  "perspectiveName": "Weekly Planning",
305
- "showHierarchy": false
349
+ "displayMode": "flat"
306
350
  }
307
351
  ```
308
352
 
package/README.zh.md CHANGED
@@ -152,28 +152,31 @@ filter_tasks {
152
152
  # ๐ŸŒŸ ๆ–ฐๅŠŸ่ƒฝ๏ผšๅˆ—ๅ‡บๆ‰€ๆœ‰่‡ชๅฎšไน‰้€่ง†
153
153
  list_custom_perspectives {"format": "detailed"}
154
154
 
155
- # ๐ŸŒณ ๆ–ฐๅŠŸ่ƒฝ๏ผšไปŽ่‡ชๅฎšไน‰้€่ง†่Žทๅ–ไปปๅŠก๏ผŒๆ”ฏๆŒๆ ‘็Šถๆ˜พ็คบ
155
+ # ๐ŸŒณ ๆ–ฐๅŠŸ่ƒฝ๏ผš้กน็›ฎๆ ‘่ง†ๅ›พ๏ผˆ้ป˜่ฎค๏ผ‰
156
156
  get_custom_perspective_tasks {
157
157
  "perspectiveName": "ไปŠๆ—ฅๅทฅไฝœๅฎ‰ๆŽ’", # ๆ‚จ็š„่‡ชๅฎšไน‰้€่ง†ๅ็งฐ
158
- "showHierarchy": true, # ๅฏ็”จๆ ‘็Šถๆ˜พ็คบ
158
+ "displayMode": "project_tree", # project_tree | task_tree | flat
159
159
  "hideCompleted": true
160
160
  }
161
161
 
162
- # ไธๅŒ้€่ง†็š„็คบไพ‹
162
+ # ๅ…จๅฑ€ไปปๅŠกๆ ‘๏ผˆ็ญ‰ไปทไบŽๆ—งๅ‚ๆ•ฐ showHierarchy=true๏ผ‰
163
163
  get_custom_perspective_tasks {
164
164
  "perspectiveName": "ไปŠๆ—ฅๅค็›˜",
165
- "showHierarchy": true
165
+ "displayMode": "task_tree"
166
166
  }
167
167
 
168
+ # ๅนณ้“บ่ง†ๅ›พ๏ผˆ็ญ‰ไปทไบŽๆ—งๅ‚ๆ•ฐ groupByProject=false๏ผ‰
168
169
  get_custom_perspective_tasks {
169
170
  "perspectiveName": "ๆœฌๅ‘จ้กน็›ฎ",
170
- "showHierarchy": false # ๅนณ้“บๆ˜พ็คบ
171
+ "displayMode": "flat"
171
172
  }
172
173
  ```
173
174
 
174
175
  **ๅŠŸ่ƒฝๅผบๅคง็š„ๅŽŸๅ› ๏ผš**
175
176
  - โœ… **ๅŽŸ็”Ÿ้›†ๆˆ** - ็›ดๆŽฅไฝฟ็”จ OmniFocus `Perspective.Custom` API
176
177
  - โœ… **ๆ ‘็Šถ็ป“ๆž„** - ไฝฟ็”จ โ”œโ”€ใ€โ””โ”€ ็ฌฆๅทๆ˜พ็คบ็ˆถๅญไปปๅŠกๅ…ณ็ณป
178
+ - โœ… **้กน็›ฎไผ˜ๅ…ˆๅˆ†็ป„** - ๅ…ˆๆŒ‰้กน็›ฎๅˆ†็ป„๏ผŒๅ†ๅฑ•็คบๅญไปปๅŠกๅฑ‚็บง
179
+ - โœ… **ไฟกๆฏ่กจ่พพๆธ…ๆ™ฐ** - ไปปๅŠกๆ ‘ไธญ้ป˜่ฎคๅฑ•็คบๅฎŒๆ•ดๅค‡ๆณจไธŽ `#ๆ ‡็ญพ`
177
180
  - โœ… **AI ๅ‹ๅฅฝ** - ๅขžๅผบ็š„ๆ่ฟฐ้˜ฒๆญขๅทฅๅ…ท้€‰ๆ‹ฉๆททๆท†
178
181
  - โœ… **ไธ“ไธš่พ“ๅ‡บ** - ๆธ…ๆ™ฐใ€ๅฏ่ฏป็š„ไปปๅŠกๅฑ‚็บง
179
182
 
@@ -214,24 +217,27 @@ get_custom_perspective_tasks {
214
217
  2. **add_omnifocus_task** - ๅˆ›ๅปบไปปๅŠก๏ผˆๅขžๅผบๅญไปปๅŠกๆ”ฏๆŒ๏ผ‰
215
218
  3. **add_project** - ๅˆ›ๅปบ้กน็›ฎ
216
219
  4. **remove_item** - ๅˆ ้™คไปปๅŠกๆˆ–้กน็›ฎ
217
- 5. **edit_item** - ็ผ–่พ‘ไปปๅŠกๆˆ–้กน็›ฎ
218
- 6. **batch_add_items** - ๆ‰น้‡ๆทปๅŠ ๏ผˆๅขžๅผบๅญไปปๅŠกๆ”ฏๆŒ๏ผ‰
219
- 7. **batch_remove_items** - ๆ‰น้‡ๅˆ ้™ค
220
- 8. **get_task_by_id** - ๆŸฅ่ฏขไปปๅŠกไฟกๆฏ
220
+ 5. **edit_item** - ็ผ–่พ‘ไปปๅŠกๆˆ–้กน็›ฎ๏ผˆ็Žฐๅทฒๆ”ฏๆŒไปปๅŠก่ฝฌ็งป๏ผš้กน็›ฎ/็ˆถไปปๅŠก/Inbox๏ผ‰
221
+ 6. **move_task** - ๅฐ†ๅทฒๆœ‰ไปปๅŠก่ฝฌ็งปๅˆฐ้กน็›ฎ/็ˆถไปปๅŠก/Inbox
222
+ 7. **batch_add_items** - ๆ‰น้‡ๆทปๅŠ ๏ผˆๅขžๅผบๅญไปปๅŠกๆ”ฏๆŒ๏ผ‰
223
+ 8. **batch_remove_items** - ๆ‰น้‡ๅˆ ้™ค
224
+ 9. **get_task_by_id** - ๆŸฅ่ฏขไปปๅŠกไฟกๆฏ
221
225
 
222
226
  ### ๐Ÿ” ๅ†…็ฝฎ้€่ง†ๅทฅๅ…ท
223
- 9. **get_inbox_tasks** - ๆ”ถไปถ็ฎฑ้€่ง†
224
- 10. **get_flagged_tasks** - ๅทฒๆ ‡่ฎฐ้€่ง†
225
- 11. **get_forecast_tasks** - ้ข„ๆต‹้€่ง†๏ผˆๅŒ…ๅซๆˆชๆญข/ๆŽจ่ฟŸ/่ฎกๅˆ’ๆ—ฅๆœŸไปปๅŠกๆ•ฐๆฎ๏ผ‰
226
- 12. **get_tasks_by_tag** - ๅŸบไบŽๆ ‡็ญพ็š„่ฟ‡ๆปค
227
- 13. **filter_tasks** - ๆ— ้™็ป„ๅˆ็š„็ปˆๆž่ฟ‡ๆปค
227
+ 10. **get_inbox_tasks** - ๆ”ถไปถ็ฎฑ้€่ง†
228
+ 11. **get_flagged_tasks** - ๅทฒๆ ‡่ฎฐ้€่ง†
229
+ 12. **get_forecast_tasks** - ้ข„ๆต‹้€่ง†๏ผˆๅŒ…ๅซๆˆชๆญข/ๆŽจ่ฟŸ/่ฎกๅˆ’ๆ—ฅๆœŸไปปๅŠกๆ•ฐๆฎ๏ผ‰
230
+ 13. **get_tasks_by_tag** - ๅŸบไบŽๆ ‡็ญพ็š„่ฟ‡ๆปค
231
+ 14. **filter_tasks** - ๆ— ้™็ป„ๅˆ็š„็ปˆๆž่ฟ‡ๆปค
228
232
 
229
233
  ### ๐ŸŒŸ ่‡ชๅฎšไน‰้€่ง†ๅทฅๅ…ท๏ผˆๆ–ฐๅŠŸ่ƒฝ๏ผ‰
230
- 14. **list_custom_perspectives** - ๐ŸŒŸ **ๆ–ฐๅŠŸ่ƒฝ**๏ผšๅˆ—ๅ‡บๆ‰€ๆœ‰่‡ชๅฎšไน‰้€่ง†ๅŠ่ฏฆๆƒ…
231
- 15. **get_custom_perspective_tasks** - ๐ŸŒŸ **ๆ–ฐๅŠŸ่ƒฝ**๏ผš่ฎฟ้—ฎ่‡ชๅฎšไน‰้€่ง†๏ผŒๆ”ฏๆŒๅฑ‚็บงๆ˜พ็คบ
234
+ 15. **list_custom_perspectives** - ๐ŸŒŸ **ๆ–ฐๅŠŸ่ƒฝ**๏ผšๅˆ—ๅ‡บๆ‰€ๆœ‰่‡ชๅฎšไน‰้€่ง†ๅŠ่ฏฆๆƒ…
235
+ 16. **get_custom_perspective_tasks** - ๐ŸŒŸ **ๆ–ฐๅŠŸ่ƒฝ**๏ผš่ฎฟ้—ฎ่‡ชๅฎšไน‰้€่ง†๏ผŒๆ”ฏๆŒๅฑ‚็บงๆ˜พ็คบ
232
236
 
233
237
  ### ๐Ÿ“Š ๅˆ†ๆžไธŽ่ทŸ่ธช
234
- 16. **get_today_completed_tasks** - ๆŸฅ็œ‹ไปŠๆ—ฅๅฎŒๆˆ็š„ไปปๅŠก
238
+ 17. **get_today_completed_tasks** - ๆŸฅ็œ‹ไปŠๆ—ฅๅฎŒๆˆ็š„ไปปๅŠก
239
+
240
+ ๆ‰น้‡่ฝฌ็งปๅŠŸ่ƒฝๅŽ็ปญ่ฎกๅˆ’๏ผˆRoadmap๏ผ‰๏ผš[docs/roadmap/2026-02-25-batch-move-tasks-plan.zh.md](docs/roadmap/2026-02-25-batch-move-tasks-plan.zh.md)
235
241
 
236
242
  ## ๐Ÿš€ ๅฟซ้€Ÿๅผ€ๅง‹็คบไพ‹
237
243
 
@@ -265,6 +271,27 @@ add_omnifocus_task {
265
271
  }
266
272
  ```
267
273
 
274
+ ### ไปปๅŠก่ฝฌ็งปๆ“ไฝœ
275
+ ```bash
276
+ # ่ฝฌ็งปๅˆฐ้กน็›ฎ
277
+ move_task {
278
+ "id": "task-id-123",
279
+ "targetProjectName": "่ง„ๅˆ’"
280
+ }
281
+
282
+ # ่ฝฌ็งปๅˆฐ็ˆถไปปๅŠกไธ‹
283
+ move_task {
284
+ "id": "task-id-123",
285
+ "targetParentTaskId": "parent-task-id-456"
286
+ }
287
+
288
+ # ่ฝฌ็งปๅ›ž Inbox
289
+ move_task {
290
+ "id": "task-id-123",
291
+ "targetInbox": true
292
+ }
293
+ ```
294
+
268
295
  ### ๆ™บ่ƒฝไปปๅŠกๅ‘็Žฐ
269
296
  ```bash
270
297
  # ๆ‰พๅˆฐ้ซ˜ไผ˜ๅ…ˆ็บงๅทฅไฝœ
@@ -288,17 +315,17 @@ filter_tasks {
288
315
  # ๅˆ—ๅ‡บๆ‚จ็š„่‡ชๅฎšไน‰้€่ง†
289
316
  list_custom_perspectives {"format": "detailed"}
290
317
 
291
- # ่ฎฟ้—ฎๅธฆๅฑ‚็บง็š„่‡ชๅฎšไน‰้€่ง†
318
+ # ่ฎฟ้—ฎๅธฆ้กน็›ฎๆ ‘็š„่‡ชๅฎšไน‰้€่ง†
292
319
  get_custom_perspective_tasks {
293
320
  "perspectiveName": "ไปŠๆ—ฅๅค็›˜",
294
- "showHierarchy": true,
321
+ "displayMode": "project_tree",
295
322
  "hideCompleted": true
296
323
  }
297
324
 
298
325
  # ๅฟซ้€ŸๆŸฅ็œ‹ๅ‘จ่ฎกๅˆ’็š„ๅนณ้“บ่ง†ๅ›พ
299
326
  get_custom_perspective_tasks {
300
327
  "perspectiveName": "ๆœฌๅ‘จ้กน็›ฎ",
301
- "showHierarchy": false
328
+ "displayMode": "flat"
302
329
  }
303
330
  ```
304
331
 
package/dist/server.js CHANGED
@@ -7,6 +7,7 @@ import * as addOmniFocusTaskTool from './tools/definitions/addOmniFocusTask.js';
7
7
  import * as addProjectTool from './tools/definitions/addProject.js';
8
8
  import * as removeItemTool from './tools/definitions/removeItem.js';
9
9
  import * as editItemTool from './tools/definitions/editItem.js';
10
+ import * as moveTaskTool from './tools/definitions/moveTask.js';
10
11
  import * as batchAddItemsTool from './tools/definitions/batchAddItems.js';
11
12
  import * as batchRemoveItemsTool from './tools/definitions/batchRemoveItems.js';
12
13
  import * as getTaskByIdTool from './tools/definitions/getTaskById.js';
@@ -32,6 +33,7 @@ server.tool("add_omnifocus_task", "Add a new task to OmniFocus", addOmniFocusTas
32
33
  server.tool("add_project", "Add a new project to OmniFocus", addProjectTool.schema.shape, addProjectTool.handler);
33
34
  server.tool("remove_item", "Remove a task or project from OmniFocus", removeItemTool.schema.shape, removeItemTool.handler);
34
35
  server.tool("edit_item", "Edit a task or project in OmniFocus", editItemTool.schema.shape, editItemTool.handler);
36
+ server.tool("move_task", "Move an existing task to a project, parent task, or inbox", moveTaskTool.schema.shape, moveTaskTool.handler);
35
37
  server.tool("batch_add_items", "Add multiple tasks or projects to OmniFocus in a single operation", batchAddItemsTool.schema.shape, batchAddItemsTool.handler);
36
38
  server.tool("batch_remove_items", "Remove multiple tasks or projects from OmniFocus in a single operation", batchRemoveItemsTool.schema.shape, batchRemoveItemsTool.handler);
37
39
  server.tool("get_task_by_id", "Get information about a specific task by ID or name", getTaskByIdTool.schema.shape, getTaskByIdTool.handler);
@@ -17,6 +17,11 @@ export const schema = z.object({
17
17
  addTags: z.array(z.string()).optional().describe("Tags to add to the task"),
18
18
  removeTags: z.array(z.string()).optional().describe("Tags to remove from the task"),
19
19
  replaceTags: z.array(z.string()).optional().describe("Tags to replace all existing tags with"),
20
+ newProjectId: z.string().optional().describe("For tasks: move task to this project ID"),
21
+ newProjectName: z.string().optional().describe("For tasks: move task to this project name (errors on duplicate names)"),
22
+ newParentTaskId: z.string().optional().describe("For tasks: move task under this parent task ID"),
23
+ newParentTaskName: z.string().optional().describe("For tasks: move task under this parent task name (errors on duplicate names)"),
24
+ moveToInbox: z.boolean().optional().describe("For tasks: move task to inbox"),
20
25
  // Project-specific fields
21
26
  newSequential: z.boolean().optional().describe("Whether the project should be sequential"),
22
27
  newFolderName: z.string().optional().describe("New folder to move the project to"),
@@ -4,15 +4,29 @@ export const schema = z.object({
4
4
  perspectiveName: z.string().describe("Exact name of the OmniFocus custom perspective (e.g., 'ไปŠๆ—ฅๅทฅไฝœๅฎ‰ๆŽ’', 'ไปŠๆ—ฅๅค็›˜', 'ๆœฌๅ‘จ้กน็›ฎ'). This is NOT a tag name."),
5
5
  hideCompleted: z.boolean().optional().describe("Whether to hide completed tasks. Set to false to show all tasks including completed ones (default: true)"),
6
6
  limit: z.number().optional().describe("Maximum number of tasks to return in flat view mode (default: 1000, ignored in hierarchy mode)"),
7
+ displayMode: z.enum(['project_tree', 'task_tree', 'flat']).optional().describe("Display mode for perspective tasks: project_tree (group by project + task hierarchy), task_tree (global task hierarchy), or flat (simple list). Default: project_tree"),
7
8
  showHierarchy: z.boolean().optional().describe("Display tasks in hierarchical tree structure showing parent-child relationships. Use this when user wants 'ๅฑ‚็บงๆ˜พ็คบ' or 'tree view' (default: false)"),
8
- groupByProject: z.boolean().optional().describe("Group tasks by their containing project. Use this when user wants to see tasks organized under project headers like 'ๆŒ‰้กน็›ฎๅˆ†็ป„' (default: true)")
9
+ groupByProject: z.boolean().optional().describe("Legacy parameter. Group tasks by project when displayMode is not provided. Default: true")
9
10
  });
11
+ export function resolveCustomPerspectiveDisplayMode(args) {
12
+ if (args.displayMode) {
13
+ return args.displayMode;
14
+ }
15
+ if (args.showHierarchy) {
16
+ return 'task_tree';
17
+ }
18
+ if (args.groupByProject === false) {
19
+ return 'flat';
20
+ }
21
+ return 'project_tree';
22
+ }
10
23
  export async function handler(args, extra) {
11
24
  try {
12
25
  const result = await getCustomPerspectiveTasks({
13
26
  perspectiveName: args.perspectiveName,
14
27
  hideCompleted: args.hideCompleted !== false, // Default to true
15
28
  limit: args.limit || 1000,
29
+ displayMode: resolveCustomPerspectiveDisplayMode(args),
16
30
  showHierarchy: args.showHierarchy || false, // Default to false
17
31
  groupByProject: args.groupByProject !== false // Default to true
18
32
  });
@@ -0,0 +1,32 @@
1
+ import assert from 'node:assert/strict';
2
+ import test from 'node:test';
3
+ import { resolveCustomPerspectiveDisplayMode } from './getCustomPerspectiveTasks.js';
4
+ test('resolveCustomPerspectiveDisplayMode respects explicit displayMode', () => {
5
+ const mode = resolveCustomPerspectiveDisplayMode({
6
+ perspectiveName: 'Today',
7
+ displayMode: 'flat',
8
+ showHierarchy: true,
9
+ groupByProject: true,
10
+ });
11
+ assert.equal(mode, 'flat');
12
+ });
13
+ test('resolveCustomPerspectiveDisplayMode maps legacy params to task_tree', () => {
14
+ const mode = resolveCustomPerspectiveDisplayMode({
15
+ perspectiveName: 'Today',
16
+ showHierarchy: true,
17
+ });
18
+ assert.equal(mode, 'task_tree');
19
+ });
20
+ test('resolveCustomPerspectiveDisplayMode maps legacy params to flat when groupByProject is false', () => {
21
+ const mode = resolveCustomPerspectiveDisplayMode({
22
+ perspectiveName: 'Today',
23
+ groupByProject: false,
24
+ });
25
+ assert.equal(mode, 'flat');
26
+ });
27
+ test('resolveCustomPerspectiveDisplayMode defaults to project_tree', () => {
28
+ const mode = resolveCustomPerspectiveDisplayMode({
29
+ perspectiveName: 'Today',
30
+ });
31
+ assert.equal(mode, 'project_tree');
32
+ });
@@ -9,7 +9,9 @@ import { getPerspectiveTasksV2 } from '../primitives/getPerspectiveTasksV2.js';
9
9
  export const schema = z.object({
10
10
  perspectiveName: z.string().describe("้€่ง†ๅ็งฐใ€‚ไฝฟ็”จไฝ ๅœจ OmniFocus ไธญๅˆ›ๅปบ็š„่‡ชๅฎšไน‰้€่ง†ๅ็งฐ๏ผŒๅฆ‚ 'ไปŠๆ—ฅๅทฅไฝœๅฎ‰ๆŽ’'ใ€'ไปŠๆ—ฅๅค็›˜' ็ญ‰"),
11
11
  hideCompleted: z.boolean().optional().default(true).describe("ๆ˜ฏๅฆ้š่—ๅทฒๅฎŒๆˆๅ’Œๅทฒๆ”พๅผƒ็š„ไปปๅŠก๏ผˆ้ป˜่ฎค: true๏ผ‰"),
12
- limit: z.number().optional().default(100).describe("่ฟ”ๅ›žไปปๅŠก็š„ๆœ€ๅคงๆ•ฐ้‡๏ผˆ้ป˜่ฎค: 100๏ผŒ่ฎพไธบ 0 ่กจ็คบๆ— ้™ๅˆถ๏ผ‰")
12
+ limit: z.number().optional().default(100).describe("่ฟ”ๅ›žไปปๅŠก็š„ๆœ€ๅคงๆ•ฐ้‡๏ผˆ้ป˜่ฎค: 100๏ผŒ่ฎพไธบ 0 ่กจ็คบๆ— ้™ๅˆถ๏ผ‰"),
13
+ displayMode: z.enum(['project_tree', 'task_tree', 'flat']).optional().default('project_tree')
14
+ .describe("ๅฑ•็คบๆจกๅผ๏ผšproject_tree๏ผˆๆŒ‰้กน็›ฎ+ๅญไปปๅŠกๆ ‘๏ผ‰๏ผŒtask_tree๏ผˆๅ…จๅฑ€ไปปๅŠกๆ ‘๏ผ‰๏ผŒflat๏ผˆๅนณ้“บๅˆ—่กจ๏ผ‰")
13
15
  });
14
16
  export async function handler(params) {
15
17
  try {
@@ -33,7 +35,8 @@ export async function handler(params) {
33
35
  totalTasks: result.tasks?.length || 0,
34
36
  options: {
35
37
  hideCompleted: params.hideCompleted,
36
- limit: params.limit
38
+ limit: params.limit,
39
+ displayMode: result.displayMode || params.displayMode
37
40
  },
38
41
  metadata: {
39
42
  timestamp: new Date().toISOString(),
@@ -41,6 +44,12 @@ export async function handler(params) {
41
44
  engine: "OmniFocus 4.2+ archivedFilterRules"
42
45
  }
43
46
  };
47
+ if (result.projectTree) {
48
+ response.projectTree = result.projectTree;
49
+ }
50
+ if (result.taskTree) {
51
+ response.taskTree = result.taskTree;
52
+ }
44
53
  // ๅฆ‚ๆžœๆœ‰ไปปๅŠก๏ผŒๆทปๅŠ ๆฑ‡ๆ€ปไฟกๆฏ
45
54
  if (result.tasks && result.tasks.length > 0) {
46
55
  const summary = {
@@ -49,7 +58,10 @@ export async function handler(params) {
49
58
  tasksWithDeferDate: result.tasks.filter(t => t.deferDate).length,
50
59
  tasksWithPlannedDate: result.tasks.filter(t => t.plannedDate).length,
51
60
  projectTasks: result.tasks.filter(t => t.projectName).length,
52
- inboxTasks: result.tasks.filter(t => !t.projectName).length
61
+ inboxTasks: result.tasks.filter(t => !t.projectName).length,
62
+ projectGroupCount: result.summary?.projectGroupCount ?? 0,
63
+ rootTaskCount: result.summary?.rootTaskCount ?? 0,
64
+ nestedTaskCount: result.summary?.nestedTaskCount ?? 0
53
65
  };
54
66
  response.summary = summary;
55
67
  }
@@ -0,0 +1,54 @@
1
+ import { z } from 'zod';
2
+ import { moveTask } from '../primitives/moveTask.js';
3
+ export const schema = z.object({
4
+ id: z.string().optional().describe('The ID of the task to move'),
5
+ name: z.string().optional().describe('The name of the task to move (fallback if ID not provided)'),
6
+ targetProjectId: z.string().optional().describe('Destination project ID'),
7
+ targetProjectName: z.string().optional().describe('Destination project name (errors on duplicate names)'),
8
+ targetParentTaskId: z.string().optional().describe('Destination parent task ID'),
9
+ targetParentTaskName: z.string().optional().describe('Destination parent task name (errors on duplicate names)'),
10
+ targetInbox: z.boolean().optional().describe('Move task to inbox')
11
+ });
12
+ function formatDestination(args) {
13
+ if (args.targetInbox) {
14
+ return 'inbox';
15
+ }
16
+ if (args.targetProjectId || args.targetProjectName) {
17
+ return `project "${args.targetProjectId || args.targetProjectName}"`;
18
+ }
19
+ if (args.targetParentTaskId || args.targetParentTaskName) {
20
+ return `parent task "${args.targetParentTaskId || args.targetParentTaskName}"`;
21
+ }
22
+ return 'destination';
23
+ }
24
+ export async function handler(args, extra) {
25
+ try {
26
+ const result = await moveTask(args);
27
+ if (result.success) {
28
+ return {
29
+ content: [{
30
+ type: 'text',
31
+ text: `โœ… Task "${result.name || args.id || args.name}" moved successfully to ${formatDestination(args)}.`
32
+ }]
33
+ };
34
+ }
35
+ return {
36
+ content: [{
37
+ type: 'text',
38
+ text: `Failed to move task: ${result.error}`
39
+ }],
40
+ isError: true
41
+ };
42
+ }
43
+ catch (err) {
44
+ const error = err;
45
+ console.error(`Tool execution error: ${error.message}`);
46
+ return {
47
+ content: [{
48
+ type: 'text',
49
+ text: `Error moving task: ${error.message}`
50
+ }],
51
+ isError: true
52
+ };
53
+ }
54
+ }
@@ -0,0 +1,27 @@
1
+ import assert from 'node:assert/strict';
2
+ import test from 'node:test';
3
+ import { handler, schema } from './moveTask.js';
4
+ test('move_task schema accepts source and destination fields', () => {
5
+ const parsed = schema.parse({
6
+ id: 'task-1',
7
+ targetProjectId: 'project-1'
8
+ });
9
+ assert.equal(parsed.id, 'task-1');
10
+ assert.equal(parsed.targetProjectId, 'project-1');
11
+ });
12
+ test('move_task schema preserves targetInbox boolean', () => {
13
+ const parsed = schema.parse({
14
+ name: 'My Task',
15
+ targetInbox: true
16
+ });
17
+ assert.equal(parsed.targetInbox, true);
18
+ });
19
+ test('move_task handler returns validation errors for conflicting destinations', async () => {
20
+ const result = await handler({
21
+ id: 'task-1',
22
+ targetProjectId: 'project-1',
23
+ targetInbox: true
24
+ }, {});
25
+ assert.equal(result.isError, true);
26
+ assert.match(result.content[0].text, /Exactly one destination/);
27
+ });
@@ -45,6 +45,16 @@ test('edit_item schema preserves newPlannedDate', () => {
45
45
  });
46
46
  assert.equal(parsed.newPlannedDate, '2026-02-14');
47
47
  });
48
+ test('edit_item schema supports task move destination fields', () => {
49
+ const parsed = editItemSchema.parse({
50
+ itemType: 'task',
51
+ id: 'abc',
52
+ newProjectId: 'project-1',
53
+ moveToInbox: false
54
+ });
55
+ assert.equal(parsed.newProjectId, 'project-1');
56
+ assert.equal(parsed.moveToInbox, false);
57
+ });
48
58
  test('filter_tasks schema supports planned date filters and sorting', () => {
49
59
  const parsed = filterTasksSchema.parse({
50
60
  plannedToday: true,
@@ -1,5 +1,5 @@
1
1
  import { executeAppleScript } from '../../utils/scriptExecution.js';
2
- import { formatDateForAppleScript } from '../../utils/dateFormatter.js';
2
+ import { appleScriptDateCode } from '../../utils/dateFormatter.js';
3
3
  export function buildTagAssignmentScript(tags, targetVar) {
4
4
  if (!tags || tags.length === 0) {
5
5
  return '';
@@ -24,14 +24,15 @@ export function buildTagAssignmentScript(tags, targetVar) {
24
24
  /**
25
25
  * Generate pure AppleScript for task creation
26
26
  */
27
- function generateAppleScript(params) {
27
+ export function generateAppleScript(params) {
28
28
  // Sanitize and prepare parameters for AppleScript
29
29
  const name = params.name.replace(/['"\\]/g, '\\$&'); // Escape quotes and backslashes
30
30
  const note = params.note?.replace(/['"\\]/g, '\\$&') || '';
31
- // Convert ISO dates to AppleScript format
32
- const dueDate = params.dueDate ? formatDateForAppleScript(params.dueDate) : '';
33
- const deferDate = params.deferDate ? formatDateForAppleScript(params.deferDate) : '';
34
- const plannedDate = params.plannedDate ? formatDateForAppleScript(params.plannedDate) : '';
31
+ // Build date variables outside OmniFocus tell block to avoid locale parsing issues.
32
+ const dueDateCode = params.dueDate ? appleScriptDateCode(params.dueDate, 'dueDateValue') : '';
33
+ const deferDateCode = params.deferDate ? appleScriptDateCode(params.deferDate, 'deferDateValue') : '';
34
+ const plannedDateCode = params.plannedDate ? appleScriptDateCode(params.plannedDate, 'plannedDateValue') : '';
35
+ const datePreamble = [dueDateCode, deferDateCode, plannedDateCode].filter(Boolean).join('\n');
35
36
  const flagged = params.flagged === true;
36
37
  const estimatedMinutes = params.estimatedMinutes?.toString() || '';
37
38
  const tags = params.tags || [];
@@ -42,6 +43,7 @@ function generateAppleScript(params) {
42
43
  // Construct AppleScript with error handling
43
44
  let script = `
44
45
  try
46
+ ${datePreamble}
45
47
  tell application "OmniFocus"
46
48
  tell front document
47
49
  -- Determine the container (parent task, project, or inbox)
@@ -76,9 +78,9 @@ function generateAppleScript(params) {
76
78
 
77
79
  -- Set task properties
78
80
  ${note ? `set note of newTask to "${note}"` : ''}
79
- ${dueDate ? `set due date of newTask to date "${dueDate}"` : ''}
80
- ${deferDate ? `set defer date of newTask to date "${deferDate}"` : ''}
81
- ${plannedDate ? `set planned date of newTask to date "${plannedDate}"` : ''}
81
+ ${params.dueDate ? `set due date of newTask to dueDateValue` : ''}
82
+ ${params.deferDate ? `set defer date of newTask to deferDateValue` : ''}
83
+ ${params.plannedDate ? `set planned date of newTask to plannedDateValue` : ''}
82
84
  ${flagged ? `set flagged of newTask to true` : ''}
83
85
  ${estimatedMinutes ? `set estimated minutes of newTask to ${estimatedMinutes}` : ''}
84
86
 
@@ -1,6 +1,6 @@
1
1
  import assert from 'node:assert/strict';
2
2
  import test from 'node:test';
3
- import { buildTagAssignmentScript } from './addOmniFocusTask.js';
3
+ import { buildTagAssignmentScript, generateAppleScript } from './addOmniFocusTask.js';
4
4
  test('buildTagAssignmentScript creates missing tags before assignment', () => {
5
5
  const script = buildTagAssignmentScript(['mcp-test-tag'], 'newTask');
6
6
  assert.match(script, /set theTag to first flattened tag where name = "mcp-test-tag"/);
@@ -8,3 +8,20 @@ test('buildTagAssignmentScript creates missing tags before assignment', () => {
8
8
  assert.match(script, /set theTag to make new tag with properties \{name:"mcp-test-tag"\}/);
9
9
  assert.match(script, /add theTag to tags of newTask/);
10
10
  });
11
+ test('generateAppleScript builds date variables before OmniFocus tell block', () => {
12
+ const script = generateAppleScript({
13
+ name: 'Task with dates',
14
+ dueDate: '2026-02-27',
15
+ deferDate: '2026-02-25',
16
+ plannedDate: '2026-02-24'
17
+ });
18
+ const tellIndex = script.indexOf('tell application "OmniFocus"');
19
+ const preambleIndex = script.indexOf('set dueDateValue to current date');
20
+ assert.ok(preambleIndex > -1 && preambleIndex < tellIndex);
21
+ assert.match(script, /set due date of newTask to dueDateValue/);
22
+ assert.match(script, /set defer date of newTask to deferDateValue/);
23
+ assert.match(script, /set planned date of newTask to plannedDateValue/);
24
+ assert.doesNotMatch(script, /set due date of newTask to date "/);
25
+ assert.doesNotMatch(script, /set defer date of newTask to date "/);
26
+ assert.doesNotMatch(script, /set planned date of newTask to date "/);
27
+ });
@@ -1,16 +1,17 @@
1
1
  import { executeAppleScript } from '../../utils/scriptExecution.js';
2
- import { formatDateForAppleScript } from '../../utils/dateFormatter.js';
2
+ import { appleScriptDateCode } from '../../utils/dateFormatter.js';
3
3
  /**
4
4
  * Generate pure AppleScript for project creation
5
5
  */
6
- function generateAppleScript(params) {
6
+ export function generateAppleScript(params) {
7
7
  // Sanitize and prepare parameters for AppleScript
8
8
  const name = params.name.replace(/['"\\]/g, '\\$&'); // Escape quotes and backslashes
9
9
  const note = params.note?.replace(/['"\\]/g, '\\$&') || '';
10
- // Convert ISO dates to AppleScript format
11
- const dueDate = params.dueDate ? formatDateForAppleScript(params.dueDate) : '';
12
- const deferDate = params.deferDate ? formatDateForAppleScript(params.deferDate) : '';
13
- const plannedDate = params.plannedDate ? formatDateForAppleScript(params.plannedDate) : '';
10
+ // Build date variables outside OmniFocus tell block to avoid locale parsing issues.
11
+ const dueDateCode = params.dueDate ? appleScriptDateCode(params.dueDate, 'dueDateValue') : '';
12
+ const deferDateCode = params.deferDate ? appleScriptDateCode(params.deferDate, 'deferDateValue') : '';
13
+ const plannedDateCode = params.plannedDate ? appleScriptDateCode(params.plannedDate, 'plannedDateValue') : '';
14
+ const datePreamble = [dueDateCode, deferDateCode, plannedDateCode].filter(Boolean).join('\n');
14
15
  const flagged = params.flagged === true;
15
16
  const estimatedMinutes = params.estimatedMinutes?.toString() || '';
16
17
  const tags = params.tags || [];
@@ -37,6 +38,7 @@ function generateAppleScript(params) {
37
38
  // Construct AppleScript with error handling
38
39
  let script = `
39
40
  try
41
+ ${datePreamble}
40
42
  tell application "OmniFocus"
41
43
  tell front document
42
44
  -- Determine the container (root or folder)
@@ -55,9 +57,9 @@ function generateAppleScript(params) {
55
57
 
56
58
  -- Set project properties
57
59
  ${note ? `set note of newProject to "${note}"` : ''}
58
- ${dueDate ? `set due date of newProject to date "${dueDate}"` : ''}
59
- ${deferDate ? `set defer date of newProject to date "${deferDate}"` : ''}
60
- ${plannedDate ? `set planned date of newProject to date "${plannedDate}"` : ''}
60
+ ${params.dueDate ? `set due date of newProject to dueDateValue` : ''}
61
+ ${params.deferDate ? `set defer date of newProject to deferDateValue` : ''}
62
+ ${params.plannedDate ? `set planned date of newProject to plannedDateValue` : ''}
61
63
  ${flagged ? `set flagged of newProject to true` : ''}
62
64
  ${estimatedMinutes ? `set estimated minutes of newProject to ${estimatedMinutes}` : ''}
63
65
  ${`set sequential of newProject to ${sequential}`}