omnifocus-mcp-enhanced 1.6.8 → 1.6.10

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 (60) hide show
  1. package/README.md +127 -9
  2. package/README.zh.md +127 -9
  3. package/dist/server.js +3 -1
  4. package/dist/tools/definitions/batchAddItems.js +3 -3
  5. package/dist/tools/definitions/getTaskById.js +34 -25
  6. package/dist/tools/definitions/getTaskById.test.js +32 -0
  7. package/dist/tools/definitions/plannedDateSchemas.test.js +7 -0
  8. package/dist/tools/definitions/readTaskAttachment.js +73 -0
  9. package/dist/tools/definitions/readTaskAttachment.test.js +32 -0
  10. package/dist/tools/dumpDatabase.js +3 -2
  11. package/dist/tools/primitives/addOmniFocusTask.js +16 -11
  12. package/dist/tools/primitives/addOmniFocusTask.test.js +18 -0
  13. package/dist/tools/primitives/addProject.js +12 -7
  14. package/dist/tools/primitives/addProject.test.js +18 -0
  15. package/dist/tools/primitives/batchAddItems.js +21 -1
  16. package/dist/tools/primitives/batchAddItems.test.js +17 -0
  17. package/dist/tools/primitives/editItem.js +26 -22
  18. package/dist/tools/primitives/editItem.test.js +32 -0
  19. package/dist/tools/primitives/getTaskById.js +30 -159
  20. package/dist/tools/primitives/readTaskAttachment.js +91 -0
  21. package/dist/tools/primitives/removeItem.js +8 -4
  22. package/dist/tools/primitives/taskAttachments.js +130 -0
  23. package/dist/tools/primitives/taskAttachments.test.js +39 -0
  24. package/dist/utils/appleScriptJson.js +27 -0
  25. package/dist/utils/appleScriptString.js +11 -0
  26. package/dist/utils/appleScriptString.test.js +7 -0
  27. package/dist/utils/omnifocusScripts/getTaskById.js +94 -0
  28. package/dist/utils/omnifocusScripts/omnifocusDump.js +30 -2
  29. package/dist/utils/omnifocusScripts/readTaskAttachment.js +100 -0
  30. package/docs/roadmap/2026-02-25-batch-move-tasks-plan.md +28 -0
  31. package/docs/roadmap/2026-02-25-batch-move-tasks-plan.zh.md +28 -0
  32. package/package.json +3 -3
  33. package/src/server.ts +9 -1
  34. package/src/tools/definitions/batchAddItems.ts +3 -3
  35. package/src/tools/definitions/getTaskById.test.ts +34 -0
  36. package/src/tools/definitions/getTaskById.ts +44 -34
  37. package/src/tools/definitions/plannedDateSchemas.test.ts +9 -0
  38. package/src/tools/definitions/readTaskAttachment.test.ts +36 -0
  39. package/src/tools/definitions/readTaskAttachment.ts +89 -0
  40. package/src/tools/dumpDatabase.ts +5 -3
  41. package/src/tools/primitives/addOmniFocusTask.test.ts +22 -0
  42. package/src/tools/primitives/addOmniFocusTask.ts +16 -11
  43. package/src/tools/primitives/addProject.test.ts +22 -0
  44. package/src/tools/primitives/addProject.ts +12 -7
  45. package/src/tools/primitives/batchAddItems.test.ts +22 -0
  46. package/src/tools/primitives/batchAddItems.ts +27 -2
  47. package/src/tools/primitives/editItem.test.ts +38 -0
  48. package/src/tools/primitives/editItem.ts +26 -22
  49. package/src/tools/primitives/getTaskById.ts +59 -164
  50. package/src/tools/primitives/readTaskAttachment.ts +140 -0
  51. package/src/tools/primitives/removeItem.ts +9 -5
  52. package/src/tools/primitives/taskAttachments.test.ts +52 -0
  53. package/src/tools/primitives/taskAttachments.ts +195 -0
  54. package/src/types.ts +13 -2
  55. package/src/utils/appleScriptJson.ts +27 -0
  56. package/src/utils/appleScriptString.test.ts +9 -0
  57. package/src/utils/appleScriptString.ts +11 -0
  58. package/src/utils/omnifocusScripts/getTaskById.js +94 -0
  59. package/src/utils/omnifocusScripts/omnifocusDump.js +30 -2
  60. package/src/utils/omnifocusScripts/readTaskAttachment.js +100 -0
package/README.md CHANGED
@@ -17,6 +17,8 @@ Enhanced Model Context Protocol (MCP) server for OmniFocus featuring **native cu
17
17
 
18
18
  ## 🆕 Latest Release
19
19
 
20
+ - **v1.6.10** - Fixed Inbox task completion via `edit_item`, fixed AppleScript special-character handling for apostrophes/backslashes, fixed JSON result escaping for special characters, and clarified `batch_add_items` / `mcporter` usage with working examples.
21
+ - **v1.6.9** - Added task attachment support: `get_task_by_id` now lists attachment metadata, `dump_database` exports attachment/link metadata, and new `read_task_attachment` returns image attachments as MCP image content when possible.
20
22
  - **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.
21
23
  - **v1.6.6** - Added full Planned Date support (create/edit/read/filter/sort/export), including `plannedDate`/`newPlannedDate` and updated task displays.
22
24
 
@@ -38,6 +40,7 @@ Enhanced Model Context Protocol (MCP) server for OmniFocus featuring **native cu
38
40
  - **📅 Time Management** - Due, defer, planned dates, estimates, and scheduling
39
41
  - **🏷️ Advanced Tagging** - Tag-based filtering with exact/partial matching
40
42
  - **🤖 AI Integration** - Seamless Claude AI integration for intelligent workflows
43
+ - **🖼️ Attachment-Aware Reads** - Surface note attachments and linked files before deciding whether AI should inspect them
41
44
 
42
45
  ## 📦 Installation
43
46
 
@@ -215,6 +218,120 @@ Efficiently manage multiple tasks:
215
218
  }
216
219
  ```
217
220
 
221
+ CLI tip for `mcporter`:
222
+
223
+ ```bash
224
+ # Prefer explicit JSON args for complex arrays / nested objects
225
+ mcporter call omnifocus.batch_add_items --args '{
226
+ "items": [
227
+ {
228
+ "type": "task",
229
+ "name": "Website Technical SEO",
230
+ "projectName": "SEO Project"
231
+ }
232
+ ]
233
+ }'
234
+ ```
235
+
236
+ If you pass a subtask with `parentTaskId` or `parentTaskName`, do not also pass `projectName`. Subtasks inherit the project from their parent task.
237
+
238
+ Working `mcporter` examples:
239
+
240
+ ```bash
241
+ # 1) Batch-create top-level tasks in a project
242
+ mcporter call omnifocus.batch_add_items --args '{
243
+ "items": [
244
+ {
245
+ "type": "task",
246
+ "name": "Parent: Category A",
247
+ "projectName": "OmniFocus MCP Batch Test"
248
+ },
249
+ {
250
+ "type": "task",
251
+ "name": "Parent: Category B",
252
+ "projectName": "OmniFocus MCP Batch Test"
253
+ }
254
+ ]
255
+ }'
256
+ ```
257
+
258
+ ```bash
259
+ # 2) Create parent + child in one batch
260
+ mcporter call omnifocus.batch_add_items --args '{
261
+ "items": [
262
+ {
263
+ "type": "task",
264
+ "name": "Parent: Category A",
265
+ "projectName": "OmniFocus MCP Batch Test"
266
+ },
267
+ {
268
+ "type": "task",
269
+ "name": "Child: A1",
270
+ "parentTaskName": "Parent: Category A"
271
+ }
272
+ ]
273
+ }'
274
+ ```
275
+
276
+ ```bash
277
+ # 3) Safer two-step flow when adding many subtasks to existing parents
278
+ mcporter call omnifocus.batch_add_items --args '{
279
+ "items": [
280
+ {
281
+ "type": "task",
282
+ "name": "Child: A1",
283
+ "parentTaskName": "Parent: Category A"
284
+ },
285
+ {
286
+ "type": "task",
287
+ "name": "Child: A2",
288
+ "parentTaskName": "Parent: Category A"
289
+ },
290
+ {
291
+ "type": "task",
292
+ "name": "Child: B1",
293
+ "parentTaskName": "Parent: Category B"
294
+ }
295
+ ]
296
+ }'
297
+ ```
298
+
299
+ This will fail, by design:
300
+
301
+ ```bash
302
+ mcporter call omnifocus.batch_add_items --args '{
303
+ "items": [
304
+ {
305
+ "type": "task",
306
+ "name": "Child: A1",
307
+ "projectName": "OmniFocus MCP Batch Test",
308
+ "parentTaskName": "Parent: Category A"
309
+ }
310
+ ]
311
+ }'
312
+ ```
313
+
314
+ Because a subtask must inherit its project from the parent task.
315
+
316
+ ### 6. 🖼️ Attachment Inspection
317
+
318
+ Discover images and linked files on a task first, then read only the attachment you need:
319
+
320
+ ```bash
321
+ # List task details plus attachment metadata
322
+ get_task_by_id {
323
+ "taskId": "abc123"
324
+ }
325
+
326
+ # Open an attachment returned by get_task_by_id
327
+ read_task_attachment {
328
+ "taskId": "abc123",
329
+ "attachmentId": "embedded-1"
330
+ }
331
+ ```
332
+
333
+ `get_task_by_id` now reports attachment IDs, names, MIME guesses, source (`embedded` vs `linked`), and sizes when available. `read_task_attachment` returns images as MCP image content when possible, so AI clients can inspect the image directly instead of parsing base64 from plain text.
334
+
218
335
  ## 🛠️ Complete Tool Reference
219
336
 
220
337
  ### 📊 Database & Task Management
@@ -226,21 +343,22 @@ Efficiently manage multiple tasks:
226
343
  6. **move_task** - Move an existing task to project/parent task/inbox
227
344
  7. **batch_add_items** - Bulk add (enhanced with subtask support)
228
345
  8. **batch_remove_items** - Bulk remove
229
- 9. **get_task_by_id** - Query task information
346
+ 9. **get_task_by_id** - Query task information, including attachment metadata
347
+ 10. **read_task_attachment** - Read an attachment reported by `get_task_by_id`
230
348
 
231
349
  ### 🔍 Built-in Perspective Tools
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
350
+ 11. **get_inbox_tasks** - Inbox perspective
351
+ 12. **get_flagged_tasks** - Flagged perspective
352
+ 13. **get_forecast_tasks** - Forecast perspective (due/deferred/planned task data included)
353
+ 14. **get_tasks_by_tag** - Tag-based filtering
354
+ 15. **filter_tasks** - Ultimate filtering with unlimited combinations
237
355
 
238
356
  ### 🌟 Custom Perspective Tools (NEW)
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
357
+ 16. **list_custom_perspectives** - 🌟 **NEW**: List all custom perspectives with details
358
+ 17. **get_custom_perspective_tasks** - 🌟 **NEW**: Access custom perspective with hierarchical display
241
359
 
242
360
  ### 📊 Analytics & Tracking
243
- 17. **get_today_completed_tasks** - View today's completed tasks
361
+ 18. **get_today_completed_tasks** - View today's completed tasks
244
362
 
245
363
  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)
246
364
 
package/README.zh.md CHANGED
@@ -13,6 +13,8 @@
13
13
 
14
14
  ## 🆕 最新版本
15
15
 
16
+ - **v1.6.10** - 修复 `edit_item` 无法完成 Inbox 任务的问题,修复 AppleScript 对单引号/反斜杠等特殊字符的处理,修复特殊字符导致的 JSON 返回解析失败,并补充 `batch_add_items` / `mcporter` 的可直接运行示例与说明。
17
+ - **v1.6.9** - 新增任务附件支持:`get_task_by_id` 会返回附件元信息,`dump_database` 导出附件/链接元信息,并新增 `read_task_attachment`,可在支持时直接把图片附件作为 MCP 图片内容返回。
16
18
  - **v1.6.6** - 新增 Planned Date(计划日期)全链路支持:创建/编辑/读取/过滤/排序/导出,包含 `plannedDate` / `newPlannedDate`。
17
19
 
18
20
  ## ✨ 核心特性
@@ -33,6 +35,7 @@
33
35
  - **📅 时间管理** - 截止日期、推迟日期、计划日期、估时和计划
34
36
  - **🏷️ 高级标签** - 基于标签的精确/模糊匹配过滤
35
37
  - **🤖 AI 集成** - 与 Claude AI 无缝集成,实现智能工作流
38
+ - **🖼️ 附件感知读取** - 先暴露备注附件和链接文件的元信息,再决定是否让 AI 继续查看附件内容
36
39
 
37
40
  ## 📦 安装
38
41
 
@@ -210,6 +213,120 @@ get_custom_perspective_tasks {
210
213
  }
211
214
  ```
212
215
 
216
+ `mcporter` 调用提示:
217
+
218
+ ```bash
219
+ # 复杂数组 / 嵌套对象,建议明确使用 --args JSON
220
+ mcporter call omnifocus.batch_add_items --args '{
221
+ "items": [
222
+ {
223
+ "type": "task",
224
+ "name": "网站技术 SEO",
225
+ "projectName": "SEO 项目"
226
+ }
227
+ ]
228
+ }'
229
+ ```
230
+
231
+ 如果某条子任务传了 `parentTaskId` 或 `parentTaskName`,就不要再传 `projectName`。子任务会自动继承父任务所在项目。
232
+
233
+ 可直接运行的 `mcporter` 示例:
234
+
235
+ ```bash
236
+ # 1)批量创建项目下的顶层任务
237
+ mcporter call omnifocus.batch_add_items --args '{
238
+ "items": [
239
+ {
240
+ "type": "task",
241
+ "name": "父任务:分类A",
242
+ "projectName": "OmniFocus MCP 批量测试"
243
+ },
244
+ {
245
+ "type": "task",
246
+ "name": "父任务:分类B",
247
+ "projectName": "OmniFocus MCP 批量测试"
248
+ }
249
+ ]
250
+ }'
251
+ ```
252
+
253
+ ```bash
254
+ # 2)单次批量里同时创建父任务和子任务
255
+ mcporter call omnifocus.batch_add_items --args '{
256
+ "items": [
257
+ {
258
+ "type": "task",
259
+ "name": "父任务:分类A",
260
+ "projectName": "OmniFocus MCP 批量测试"
261
+ },
262
+ {
263
+ "type": "task",
264
+ "name": "子任务:A1",
265
+ "parentTaskName": "父任务:分类A"
266
+ }
267
+ ]
268
+ }'
269
+ ```
270
+
271
+ ```bash
272
+ # 3)更稳妥的两步法:父任务已存在时,再批量创建多个子任务
273
+ mcporter call omnifocus.batch_add_items --args '{
274
+ "items": [
275
+ {
276
+ "type": "task",
277
+ "name": "子任务:A1",
278
+ "parentTaskName": "父任务:分类A"
279
+ },
280
+ {
281
+ "type": "task",
282
+ "name": "子任务:A2",
283
+ "parentTaskName": "父任务:分类A"
284
+ },
285
+ {
286
+ "type": "task",
287
+ "name": "子任务:B1",
288
+ "parentTaskName": "父任务:分类B"
289
+ }
290
+ ]
291
+ }'
292
+ ```
293
+
294
+ 下面这种写法会失败,这属于预期行为:
295
+
296
+ ```bash
297
+ mcporter call omnifocus.batch_add_items --args '{
298
+ "items": [
299
+ {
300
+ "type": "task",
301
+ "name": "子任务:A1",
302
+ "projectName": "OmniFocus MCP 批量测试",
303
+ "parentTaskName": "父任务:分类A"
304
+ }
305
+ ]
306
+ }'
307
+ ```
308
+
309
+ 因为子任务必须继承父任务所在项目,不能再单独传 `projectName`。
310
+
311
+ ### 6. 🖼️ 附件查看
312
+
313
+ 先读取任务和附件元信息,再按需打开具体附件:
314
+
315
+ ```bash
316
+ # 读取任务详情和附件元信息
317
+ get_task_by_id {
318
+ "taskId": "abc123"
319
+ }
320
+
321
+ # 打开 get_task_by_id 返回的某个附件
322
+ read_task_attachment {
323
+ "taskId": "abc123",
324
+ "attachmentId": "embedded-1"
325
+ }
326
+ ```
327
+
328
+ `get_task_by_id` 现在会返回附件 ID、名称、推断出的 MIME 类型、来源(`embedded` 或 `linked`)以及可用时的大小。`read_task_attachment` 会尽量把图片作为 MCP 图片内容直接返回,这样 AI 客户端可以直接查看图片,而不是只能读一段 base64 文本。
329
+
213
330
  ## 🛠️ 完整工具参考
214
331
 
215
332
  ### 📊 数据库与任务管理
@@ -221,21 +338,22 @@ get_custom_perspective_tasks {
221
338
  6. **move_task** - 将已有任务转移到项目/父任务/Inbox
222
339
  7. **batch_add_items** - 批量添加(增强子任务支持)
223
340
  8. **batch_remove_items** - 批量删除
224
- 9. **get_task_by_id** - 查询任务信息
341
+ 9. **get_task_by_id** - 查询任务信息,并返回附件元信息
342
+ 10. **read_task_attachment** - 读取 `get_task_by_id` 返回的任务附件
225
343
 
226
344
  ### 🔍 内置透视工具
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** - 无限组合的终极过滤
345
+ 11. **get_inbox_tasks** - 收件箱透视
346
+ 12. **get_flagged_tasks** - 已标记透视
347
+ 13. **get_forecast_tasks** - 预测透视(包含截止/推迟/计划日期任务数据)
348
+ 14. **get_tasks_by_tag** - 基于标签的过滤
349
+ 15. **filter_tasks** - 无限组合的终极过滤
232
350
 
233
351
  ### 🌟 自定义透视工具(新功能)
234
- 15. **list_custom_perspectives** - 🌟 **新功能**:列出所有自定义透视及详情
235
- 16. **get_custom_perspective_tasks** - 🌟 **新功能**:访问自定义透视,支持层级显示
352
+ 16. **list_custom_perspectives** - 🌟 **新功能**:列出所有自定义透视及详情
353
+ 17. **get_custom_perspective_tasks** - 🌟 **新功能**:访问自定义透视,支持层级显示
236
354
 
237
355
  ### 📊 分析与跟踪
238
- 17. **get_today_completed_tasks** - 查看今日完成的任务
356
+ 18. **get_today_completed_tasks** - 查看今日完成的任务
239
357
 
240
358
  批量转移功能后续计划(Roadmap):[docs/roadmap/2026-02-25-batch-move-tasks-plan.zh.md](docs/roadmap/2026-02-25-batch-move-tasks-plan.zh.md)
241
359
 
package/dist/server.js CHANGED
@@ -11,6 +11,7 @@ import * as moveTaskTool from './tools/definitions/moveTask.js';
11
11
  import * as batchAddItemsTool from './tools/definitions/batchAddItems.js';
12
12
  import * as batchRemoveItemsTool from './tools/definitions/batchRemoveItems.js';
13
13
  import * as getTaskByIdTool from './tools/definitions/getTaskById.js';
14
+ import * as readTaskAttachmentTool from './tools/definitions/readTaskAttachment.js';
14
15
  import * as getTodayCompletedTasksTool from './tools/definitions/getTodayCompletedTasks.js';
15
16
  // Import perspective tools
16
17
  import * as getInboxTasksTool from './tools/definitions/getInboxTasks.js';
@@ -25,7 +26,7 @@ import * as getCustomPerspectiveTasksTool from './tools/definitions/getCustomPer
25
26
  // Create an MCP server
26
27
  const server = new McpServer({
27
28
  name: "OmniFocus MCP",
28
- version: "1.6.6"
29
+ version: "1.6.9"
29
30
  });
30
31
  // Register tools
31
32
  server.tool("dump_database", "Gets the current state of your OmniFocus database", dumpDatabaseTool.schema.shape, dumpDatabaseTool.handler);
@@ -37,6 +38,7 @@ server.tool("move_task", "Move an existing task to a project, parent task, or in
37
38
  server.tool("batch_add_items", "Add multiple tasks or projects to OmniFocus in a single operation", batchAddItemsTool.schema.shape, batchAddItemsTool.handler);
38
39
  server.tool("batch_remove_items", "Remove multiple tasks or projects from OmniFocus in a single operation", batchRemoveItemsTool.schema.shape, batchRemoveItemsTool.handler);
39
40
  server.tool("get_task_by_id", "Get information about a specific task by ID or name", getTaskByIdTool.schema.shape, getTaskByIdTool.handler);
41
+ server.tool("read_task_attachment", "Read a task attachment reported by get_task_by_id. Images are returned as MCP image content when possible.", readTaskAttachmentTool.schema.shape, readTaskAttachmentTool.handler);
40
42
  server.tool("get_today_completed_tasks", "Get tasks completed today - view today's accomplishments", getTodayCompletedTasksTool.schema.shape, getTodayCompletedTasksTool.handler);
41
43
  // Register perspective tools
42
44
  server.tool("get_inbox_tasks", "Get tasks from OmniFocus inbox perspective", getInboxTasksTool.schema.shape, getInboxTasksTool.handler);
@@ -12,9 +12,9 @@ export const schema = z.object({
12
12
  estimatedMinutes: z.number().optional().describe("Estimated time to complete the item, in minutes"),
13
13
  tags: z.array(z.string()).optional().describe("Tags to assign to the item"),
14
14
  // Task-specific properties
15
- projectName: z.string().optional().describe("For tasks: The name of the project to add the task to"),
16
- parentTaskId: z.string().optional().describe("For tasks: The ID of the parent task to create this task as a subtask"),
17
- parentTaskName: z.string().optional().describe("For tasks: The name of the parent task to create this task as a subtask"),
15
+ projectName: z.string().optional().describe("For tasks: The project name for top-level tasks. Omit this when parentTaskId or parentTaskName is set."),
16
+ parentTaskId: z.string().optional().describe("For tasks: The parent task ID for subtasks. When this is set, do not also provide projectName."),
17
+ parentTaskName: z.string().optional().describe("For tasks: The parent task name for subtasks. Subtasks inherit project from their parent, so do not also provide projectName."),
18
18
  // Project-specific properties
19
19
  folderName: z.string().optional().describe("For projects: The name of the folder to add the project to"),
20
20
  sequential: z.boolean().optional().describe("For projects: Whether tasks in the project should be sequential")
@@ -1,9 +1,42 @@
1
1
  import { z } from 'zod';
2
2
  import { getTaskById } from '../primitives/getTaskById.js';
3
+ import { formatAttachmentSize } from '../primitives/taskAttachments.js';
3
4
  export const schema = z.object({
4
5
  taskId: z.string().optional().describe("The ID of the task to retrieve"),
5
6
  taskName: z.string().optional().describe("The name of the task to retrieve (alternative to taskId)")
6
7
  });
8
+ export function formatTaskInfo(task) {
9
+ let infoText = `📋 **Task Information**\n`;
10
+ infoText += `• **Name**: ${task.name}\n`;
11
+ infoText += `• **ID**: ${task.id}\n`;
12
+ if (task.note) {
13
+ infoText += `• **Note**: ${task.note}\n`;
14
+ }
15
+ if (task.parentId && task.parentName) {
16
+ infoText += `• **Parent Task**: ${task.parentName} (${task.parentId})\n`;
17
+ }
18
+ if (task.projectId && task.projectName) {
19
+ infoText += `• **Project**: ${task.projectName} (${task.projectId})\n`;
20
+ }
21
+ if (task.dueDate) {
22
+ infoText += `• **Due**: ${new Date(task.dueDate).toLocaleString()}\n`;
23
+ }
24
+ if (task.deferDate) {
25
+ infoText += `• **Defer**: ${new Date(task.deferDate).toLocaleString()}\n`;
26
+ }
27
+ if (task.plannedDate) {
28
+ infoText += `• **Planned**: ${new Date(task.plannedDate).toLocaleString()}\n`;
29
+ }
30
+ infoText += `• **Has Children**: ${task.hasChildren ? `Yes (${task.childrenCount} subtasks)` : 'No'}\n`;
31
+ infoText += `• **Attachments**: ${task.attachments.length}\n`;
32
+ if (task.attachments.length > 0) {
33
+ task.attachments.forEach(attachment => {
34
+ infoText += ` - ${attachment.id}: ${attachment.name} [${attachment.kind}, ${attachment.mimeType || 'unknown'}, ${attachment.source}, ${formatAttachmentSize(attachment.sizeBytes)}]\n`;
35
+ });
36
+ infoText += `• Use read_task_attachment with an attachment ID or name when you need to inspect the file.\n`;
37
+ }
38
+ return infoText;
39
+ }
7
40
  export async function handler(args, extra) {
8
41
  try {
9
42
  // Validate that either taskId or taskName is provided
@@ -19,34 +52,10 @@ export async function handler(args, extra) {
19
52
  // Call the getTaskById function
20
53
  const result = await getTaskById(args);
21
54
  if (result.success && result.task) {
22
- const task = result.task;
23
- // Format task information for display
24
- let infoText = `📋 **Task Information**\n`;
25
- infoText += `• **Name**: ${task.name}\n`;
26
- infoText += `• **ID**: ${task.id}\n`;
27
- if (task.note) {
28
- infoText += `• **Note**: ${task.note}\n`;
29
- }
30
- if (task.parentId && task.parentName) {
31
- infoText += `• **Parent Task**: ${task.parentName} (${task.parentId})\n`;
32
- }
33
- if (task.projectId && task.projectName) {
34
- infoText += `• **Project**: ${task.projectName} (${task.projectId})\n`;
35
- }
36
- if (task.dueDate) {
37
- infoText += `• **Due**: ${new Date(task.dueDate).toLocaleString()}\n`;
38
- }
39
- if (task.deferDate) {
40
- infoText += `• **Defer**: ${new Date(task.deferDate).toLocaleString()}\n`;
41
- }
42
- if (task.plannedDate) {
43
- infoText += `• **Planned**: ${new Date(task.plannedDate).toLocaleString()}\n`;
44
- }
45
- infoText += `• **Has Children**: ${task.hasChildren ? `Yes (${task.childrenCount} subtasks)` : 'No'}\n`;
46
55
  return {
47
56
  content: [{
48
57
  type: "text",
49
- text: infoText
58
+ text: formatTaskInfo(result.task)
50
59
  }]
51
60
  };
52
61
  }
@@ -0,0 +1,32 @@
1
+ import assert from 'node:assert/strict';
2
+ import test from 'node:test';
3
+ import { formatTaskInfo } from './getTaskById.js';
4
+ test('formatTaskInfo includes attachment metadata for follow-up reads', () => {
5
+ const output = formatTaskInfo({
6
+ id: 'task-1',
7
+ name: 'Review screenshots',
8
+ note: 'Latest UI mocks are attached',
9
+ hasChildren: false,
10
+ childrenCount: 0,
11
+ tags: ['design'],
12
+ flagged: false,
13
+ completed: false,
14
+ linkedFileURLs: [],
15
+ attachments: [
16
+ {
17
+ id: 'embedded-1',
18
+ name: 'ui-mock.png',
19
+ kind: 'image',
20
+ mimeType: 'image/png',
21
+ sizeBytes: 4096,
22
+ source: 'embedded',
23
+ isImage: true
24
+ }
25
+ ]
26
+ });
27
+ assert.match(output, /Attachments\*\*: 1/);
28
+ assert.match(output, /embedded-1/);
29
+ assert.match(output, /ui-mock\.png/);
30
+ assert.match(output, /image\/png/);
31
+ assert.match(output, /Use read_task_attachment/);
32
+ });
@@ -37,6 +37,13 @@ test('batch_add_items schema preserves plannedDate for task and project', () =>
37
37
  assert.equal(parsed.items[0].plannedDate, '2026-02-14');
38
38
  assert.equal(parsed.items[1].plannedDate, '2026-02-15');
39
39
  });
40
+ test('batch_add_items schema documents subtask project inheritance', () => {
41
+ const itemSchema = batchAddItemsSchema.shape.items._def.type;
42
+ const projectNameDescription = itemSchema.shape.projectName.description;
43
+ const parentTaskNameDescription = itemSchema.shape.parentTaskName.description;
44
+ assert.match(projectNameDescription, /omit this when parentTaskId or parentTaskName is set/i);
45
+ assert.match(parentTaskNameDescription, /subtasks inherit project from their parent/i);
46
+ });
40
47
  test('edit_item schema preserves newPlannedDate', () => {
41
48
  const parsed = editItemSchema.parse({
42
49
  itemType: 'task',
@@ -0,0 +1,73 @@
1
+ import { z } from 'zod';
2
+ import { readTaskAttachment, validateReadTaskAttachmentParams } from '../primitives/readTaskAttachment.js';
3
+ import { formatAttachmentSize } from '../primitives/taskAttachments.js';
4
+ export const schema = z.object({
5
+ taskId: z.string().optional().describe('The ID of the task that owns the attachment'),
6
+ taskName: z.string().optional().describe('The name of the task that owns the attachment'),
7
+ attachmentId: z.string().optional().describe('The attachment ID reported by get_task_by_id'),
8
+ attachmentName: z.string().optional().describe('The attachment name reported by get_task_by_id')
9
+ });
10
+ export function buildAttachmentContentResponse(result) {
11
+ const attachment = result.attachment;
12
+ const summary = `📎 **Attachment**\n` +
13
+ `• **Name**: ${attachment.name}\n` +
14
+ `• **ID**: ${attachment.id}\n` +
15
+ `• **Source**: ${attachment.source}\n` +
16
+ `• **Type**: ${attachment.kind}\n` +
17
+ `• **MIME**: ${attachment.mimeType || 'unknown'}\n` +
18
+ `• **Size**: ${formatAttachmentSize(attachment.sizeBytes)}\n`;
19
+ const content = [
20
+ {
21
+ type: 'text',
22
+ text: summary
23
+ }
24
+ ];
25
+ if (attachment.isImage && attachment.mimeType && result.content.base64) {
26
+ content.push({
27
+ type: 'image',
28
+ data: result.content.base64,
29
+ mimeType: attachment.mimeType
30
+ });
31
+ return { content };
32
+ }
33
+ if (result.content.text) {
34
+ content.push({
35
+ type: 'text',
36
+ text: result.content.text
37
+ });
38
+ return { content };
39
+ }
40
+ if (result.content.base64) {
41
+ content.push({
42
+ type: 'text',
43
+ text: `Base64 content:\n${result.content.base64}`
44
+ });
45
+ }
46
+ return { content };
47
+ }
48
+ export async function handler(args, extra) {
49
+ const validation = validateReadTaskAttachmentParams(args);
50
+ if (!validation.valid) {
51
+ return {
52
+ content: [{
53
+ type: 'text',
54
+ text: `Error: ${validation.error}`
55
+ }],
56
+ isError: true
57
+ };
58
+ }
59
+ const result = await readTaskAttachment(args);
60
+ if (!result.success || !result.attachment || !result.content) {
61
+ return {
62
+ content: [{
63
+ type: 'text',
64
+ text: `Failed to read attachment: ${result.error}`
65
+ }],
66
+ isError: true
67
+ };
68
+ }
69
+ return buildAttachmentContentResponse({
70
+ attachment: result.attachment,
71
+ content: result.content
72
+ });
73
+ }
@@ -0,0 +1,32 @@
1
+ import assert from 'node:assert/strict';
2
+ import test from 'node:test';
3
+ import { buildAttachmentContentResponse, schema } from './readTaskAttachment.js';
4
+ test('read_task_attachment schema preserves task and attachment selectors', () => {
5
+ const parsed = schema.parse({
6
+ taskId: 'task-1',
7
+ attachmentId: 'embedded-1'
8
+ });
9
+ assert.equal(parsed.taskId, 'task-1');
10
+ assert.equal(parsed.attachmentId, 'embedded-1');
11
+ });
12
+ test('buildAttachmentContentResponse returns image content blocks for image attachments', () => {
13
+ const response = buildAttachmentContentResponse({
14
+ attachment: {
15
+ id: 'embedded-1',
16
+ name: 'annotated.png',
17
+ kind: 'image',
18
+ mimeType: 'image/png',
19
+ sizeBytes: 12,
20
+ source: 'embedded',
21
+ isImage: true
22
+ },
23
+ content: {
24
+ base64: 'QUJDRA=='
25
+ }
26
+ });
27
+ assert.equal(response.content[0].type, 'text');
28
+ assert.match(response.content[0].text, /annotated\.png/);
29
+ assert.equal(response.content[1].type, 'image');
30
+ assert.equal(response.content[1].mimeType, 'image/png');
31
+ assert.equal(response.content[1].data, 'QUJDRA==');
32
+ });
@@ -1,4 +1,5 @@
1
1
  import { executeOmniFocusScript } from '../utils/scriptExecution.js';
2
+ import { normalizeTaskAttachments } from './primitives/taskAttachments.js';
2
3
  // Main function to dump the database
3
4
  export async function dumpDatabase() {
4
5
  try {
@@ -58,8 +59,8 @@ export async function dumpDatabase() {
58
59
  isRepeating: false, // Not available in the new format
59
60
  repetitionMethod: null, // Not available in the new format
60
61
  repetitionRule: null, // Not available in the new format
61
- attachments: [], // Default empty array
62
- linkedFileURLs: [], // Default empty array
62
+ attachments: normalizeTaskAttachments(task.attachments, task.linkedFileURLs),
63
+ linkedFileURLs: task.linkedFileURLs || [],
63
64
  notifications: [], // Default empty array
64
65
  shouldUseFloatingTimeZone: false // Default value
65
66
  };