omnifocus-mcp-enhanced 1.6.8 → 1.6.9

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 (30) hide show
  1. package/README.md +31 -9
  2. package/README.zh.md +31 -9
  3. package/dist/server.js +3 -1
  4. package/dist/tools/definitions/getTaskById.js +34 -25
  5. package/dist/tools/definitions/getTaskById.test.js +32 -0
  6. package/dist/tools/definitions/readTaskAttachment.js +73 -0
  7. package/dist/tools/definitions/readTaskAttachment.test.js +32 -0
  8. package/dist/tools/dumpDatabase.js +3 -2
  9. package/dist/tools/primitives/getTaskById.js +30 -159
  10. package/dist/tools/primitives/readTaskAttachment.js +91 -0
  11. package/dist/tools/primitives/taskAttachments.js +130 -0
  12. package/dist/tools/primitives/taskAttachments.test.js +39 -0
  13. package/dist/utils/omnifocusScripts/getTaskById.js +94 -0
  14. package/dist/utils/omnifocusScripts/omnifocusDump.js +30 -2
  15. package/dist/utils/omnifocusScripts/readTaskAttachment.js +100 -0
  16. package/package.json +1 -1
  17. package/src/server.ts +9 -1
  18. package/src/tools/definitions/getTaskById.test.ts +34 -0
  19. package/src/tools/definitions/getTaskById.ts +44 -34
  20. package/src/tools/definitions/readTaskAttachment.test.ts +36 -0
  21. package/src/tools/definitions/readTaskAttachment.ts +89 -0
  22. package/src/tools/dumpDatabase.ts +5 -3
  23. package/src/tools/primitives/getTaskById.ts +59 -164
  24. package/src/tools/primitives/readTaskAttachment.ts +140 -0
  25. package/src/tools/primitives/taskAttachments.test.ts +52 -0
  26. package/src/tools/primitives/taskAttachments.ts +195 -0
  27. package/src/types.ts +13 -2
  28. package/src/utils/omnifocusScripts/getTaskById.js +94 -0
  29. package/src/utils/omnifocusScripts/omnifocusDump.js +30 -2
  30. package/src/utils/omnifocusScripts/readTaskAttachment.js +100 -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.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
21
  - **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
22
  - **v1.6.6** - Added full Planned Date support (create/edit/read/filter/sort/export), including `plannedDate`/`newPlannedDate` and updated task displays.
22
23
 
@@ -38,6 +39,7 @@ Enhanced Model Context Protocol (MCP) server for OmniFocus featuring **native cu
38
39
  - **📅 Time Management** - Due, defer, planned dates, estimates, and scheduling
39
40
  - **🏷️ Advanced Tagging** - Tag-based filtering with exact/partial matching
40
41
  - **🤖 AI Integration** - Seamless Claude AI integration for intelligent workflows
42
+ - **🖼️ Attachment-Aware Reads** - Surface note attachments and linked files before deciding whether AI should inspect them
41
43
 
42
44
  ## 📦 Installation
43
45
 
@@ -215,6 +217,25 @@ Efficiently manage multiple tasks:
215
217
  }
216
218
  ```
217
219
 
220
+ ### 6. 🖼️ Attachment Inspection
221
+
222
+ Discover images and linked files on a task first, then read only the attachment you need:
223
+
224
+ ```bash
225
+ # List task details plus attachment metadata
226
+ get_task_by_id {
227
+ "taskId": "abc123"
228
+ }
229
+
230
+ # Open an attachment returned by get_task_by_id
231
+ read_task_attachment {
232
+ "taskId": "abc123",
233
+ "attachmentId": "embedded-1"
234
+ }
235
+ ```
236
+
237
+ `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.
238
+
218
239
  ## 🛠️ Complete Tool Reference
219
240
 
220
241
  ### 📊 Database & Task Management
@@ -226,21 +247,22 @@ Efficiently manage multiple tasks:
226
247
  6. **move_task** - Move an existing task to project/parent task/inbox
227
248
  7. **batch_add_items** - Bulk add (enhanced with subtask support)
228
249
  8. **batch_remove_items** - Bulk remove
229
- 9. **get_task_by_id** - Query task information
250
+ 9. **get_task_by_id** - Query task information, including attachment metadata
251
+ 10. **read_task_attachment** - Read an attachment reported by `get_task_by_id`
230
252
 
231
253
  ### 🔍 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
254
+ 11. **get_inbox_tasks** - Inbox perspective
255
+ 12. **get_flagged_tasks** - Flagged perspective
256
+ 13. **get_forecast_tasks** - Forecast perspective (due/deferred/planned task data included)
257
+ 14. **get_tasks_by_tag** - Tag-based filtering
258
+ 15. **filter_tasks** - Ultimate filtering with unlimited combinations
237
259
 
238
260
  ### 🌟 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
261
+ 16. **list_custom_perspectives** - 🌟 **NEW**: List all custom perspectives with details
262
+ 17. **get_custom_perspective_tasks** - 🌟 **NEW**: Access custom perspective with hierarchical display
241
263
 
242
264
  ### 📊 Analytics & Tracking
243
- 17. **get_today_completed_tasks** - View today's completed tasks
265
+ 18. **get_today_completed_tasks** - View today's completed tasks
244
266
 
245
267
  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
268
 
package/README.zh.md CHANGED
@@ -13,6 +13,7 @@
13
13
 
14
14
  ## 🆕 最新版本
15
15
 
16
+ - **v1.6.9** - 新增任务附件支持:`get_task_by_id` 会返回附件元信息,`dump_database` 导出附件/链接元信息,并新增 `read_task_attachment`,可在支持时直接把图片附件作为 MCP 图片内容返回。
16
17
  - **v1.6.6** - 新增 Planned Date(计划日期)全链路支持:创建/编辑/读取/过滤/排序/导出,包含 `plannedDate` / `newPlannedDate`。
17
18
 
18
19
  ## ✨ 核心特性
@@ -33,6 +34,7 @@
33
34
  - **📅 时间管理** - 截止日期、推迟日期、计划日期、估时和计划
34
35
  - **🏷️ 高级标签** - 基于标签的精确/模糊匹配过滤
35
36
  - **🤖 AI 集成** - 与 Claude AI 无缝集成,实现智能工作流
37
+ - **🖼️ 附件感知读取** - 先暴露备注附件和链接文件的元信息,再决定是否让 AI 继续查看附件内容
36
38
 
37
39
  ## 📦 安装
38
40
 
@@ -210,6 +212,25 @@ get_custom_perspective_tasks {
210
212
  }
211
213
  ```
212
214
 
215
+ ### 6. 🖼️ 附件查看
216
+
217
+ 先读取任务和附件元信息,再按需打开具体附件:
218
+
219
+ ```bash
220
+ # 读取任务详情和附件元信息
221
+ get_task_by_id {
222
+ "taskId": "abc123"
223
+ }
224
+
225
+ # 打开 get_task_by_id 返回的某个附件
226
+ read_task_attachment {
227
+ "taskId": "abc123",
228
+ "attachmentId": "embedded-1"
229
+ }
230
+ ```
231
+
232
+ `get_task_by_id` 现在会返回附件 ID、名称、推断出的 MIME 类型、来源(`embedded` 或 `linked`)以及可用时的大小。`read_task_attachment` 会尽量把图片作为 MCP 图片内容直接返回,这样 AI 客户端可以直接查看图片,而不是只能读一段 base64 文本。
233
+
213
234
  ## 🛠️ 完整工具参考
214
235
 
215
236
  ### 📊 数据库与任务管理
@@ -221,21 +242,22 @@ get_custom_perspective_tasks {
221
242
  6. **move_task** - 将已有任务转移到项目/父任务/Inbox
222
243
  7. **batch_add_items** - 批量添加(增强子任务支持)
223
244
  8. **batch_remove_items** - 批量删除
224
- 9. **get_task_by_id** - 查询任务信息
245
+ 9. **get_task_by_id** - 查询任务信息,并返回附件元信息
246
+ 10. **read_task_attachment** - 读取 `get_task_by_id` 返回的任务附件
225
247
 
226
248
  ### 🔍 内置透视工具
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** - 无限组合的终极过滤
249
+ 11. **get_inbox_tasks** - 收件箱透视
250
+ 12. **get_flagged_tasks** - 已标记透视
251
+ 13. **get_forecast_tasks** - 预测透视(包含截止/推迟/计划日期任务数据)
252
+ 14. **get_tasks_by_tag** - 基于标签的过滤
253
+ 15. **filter_tasks** - 无限组合的终极过滤
232
254
 
233
255
  ### 🌟 自定义透视工具(新功能)
234
- 15. **list_custom_perspectives** - 🌟 **新功能**:列出所有自定义透视及详情
235
- 16. **get_custom_perspective_tasks** - 🌟 **新功能**:访问自定义透视,支持层级显示
256
+ 16. **list_custom_perspectives** - 🌟 **新功能**:列出所有自定义透视及详情
257
+ 17. **get_custom_perspective_tasks** - 🌟 **新功能**:访问自定义透视,支持层级显示
236
258
 
237
259
  ### 📊 分析与跟踪
238
- 17. **get_today_completed_tasks** - 查看今日完成的任务
260
+ 18. **get_today_completed_tasks** - 查看今日完成的任务
239
261
 
240
262
  批量转移功能后续计划(Roadmap):[docs/roadmap/2026-02-25-batch-move-tasks-plan.zh.md](docs/roadmap/2026-02-25-batch-move-tasks-plan.zh.md)
241
263
 
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);
@@ -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
+ });
@@ -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
  };
@@ -1,108 +1,5 @@
1
- import { executeAppleScript } from '../../utils/scriptExecution.js';
2
- /**
3
- * Generate AppleScript to get task information by ID or name
4
- */
5
- function generateGetTaskScript(params) {
6
- const taskId = params.taskId?.replace(/['"\\]/g, '\\$&') || '';
7
- const taskName = params.taskName?.replace(/['"\\]/g, '\\$&') || '';
8
- let script = `
9
- try
10
- tell application "OmniFocus"
11
- tell front document
12
- -- Find task by ID or name
13
- if "${taskId}" is not "" then
14
- set theTask to first flattened task where id = "${taskId}"
15
- else if "${taskName}" is not "" then
16
- set theTask to first flattened task where name = "${taskName}"
17
- else
18
- return "{\\\"success\\\":false,\\\"error\\\":\\\"Either taskId or taskName must be provided\\\"}"
19
- end if
20
-
21
- -- Get task information
22
- set taskId to id of theTask as string
23
- set taskName to name of theTask
24
- set taskNote to note of theTask
25
- set taskChildren to tasks of theTask
26
- set childrenCount to count of taskChildren
27
- set hasChildren to (childrenCount > 0)
28
-
29
- -- Get parent information
30
- set parentId to ""
31
- set parentName to ""
32
- try
33
- set parentTask to container of theTask
34
- if class of parentTask is task then
35
- set parentId to id of parentTask as string
36
- set parentName to name of parentTask
37
- end if
38
- end try
39
-
40
- -- Get project information
41
- set projectId to ""
42
- set projectName to ""
43
- try
44
- set containingProject to containing project of theTask
45
- if containingProject is not missing value then
46
- set projectId to id of containingProject as string
47
- set projectName to name of containingProject
48
- end if
49
- end try
50
-
51
- -- Get tags
52
- set taskTags to tags of theTask
53
- set tagNames to ""
54
- if (count of taskTags) > 0 then
55
- set tagList to {}
56
- repeat with taskTag in taskTags
57
- set end of tagList to "\\"" & (name of taskTag) & "\\""
58
- end repeat
59
- set AppleScript's text item delimiters to ","
60
- set tagNames to tagList as string
61
- set AppleScript's text item delimiters to ""
62
- end if
63
-
64
- -- Get other properties
65
- set taskFlagged to flagged of theTask
66
- set taskCompleted to completed of theTask
67
- set taskDueDate to ""
68
- set taskDeferDate to ""
69
- set taskPlannedDate to ""
70
- set taskEstimatedMinutes to ""
71
-
72
- try
73
- if due date of theTask is not missing value then
74
- set taskDueDate to (due date of theTask) as string
75
- end if
76
- end try
77
-
78
- try
79
- if defer date of theTask is not missing value then
80
- set taskDeferDate to (defer date of theTask) as string
81
- end if
82
- end try
83
-
84
- try
85
- if planned date of theTask is not missing value then
86
- set taskPlannedDate to (planned date of theTask) as string
87
- end if
88
- end try
89
-
90
- try
91
- if estimated minutes of theTask is not missing value then
92
- set taskEstimatedMinutes to (estimated minutes of theTask) as string
93
- end if
94
- end try
95
-
96
- -- Return simple pipe-delimited result to avoid JSON escaping issues
97
- return "SUCCESS|" & taskId & "|" & taskName & "|" & taskNote & "|" & parentId & "|" & parentName & "|" & projectId & "|" & projectName & "|" & hasChildren & "|" & childrenCount & "|" & tagNames & "|" & taskDueDate & "|" & taskDeferDate & "|" & taskPlannedDate & "|" & taskFlagged & "|" & taskCompleted & "|" & taskEstimatedMinutes
98
- end tell
99
- end tell
100
- on error errorMessage
101
- return "{\\\"success\\\":false,\\\"error\\\":\\"" & errorMessage & "\\"}"
102
- end try
103
- `;
104
- return script;
105
- }
1
+ import { executeOmniFocusScript } from '../../utils/scriptExecution.js';
2
+ import { normalizeTaskAttachments } from './taskAttachments.js';
106
3
  /**
107
4
  * Get task information by ID or name from OmniFocus
108
5
  */
@@ -115,64 +12,38 @@ export async function getTaskById(params) {
115
12
  error: "Either taskId or taskName must be provided"
116
13
  };
117
14
  }
118
- // Generate AppleScript
119
- const script = generateGetTaskScript(params);
120
- console.error("Generated getTaskById AppleScript:");
121
- console.error(script);
122
- console.error("Executing getTaskById AppleScript...");
123
- // Execute AppleScript using temp file (avoids shell escaping issues)
124
- const stdout = await executeAppleScript(script);
125
- console.error("AppleScript stdout:", stdout);
126
- // Parse the result
127
- try {
128
- if (stdout.startsWith('SUCCESS|')) {
129
- // Parse pipe-delimited format
130
- const parts = stdout.substring(8).split('|'); // Remove "SUCCESS|" prefix
131
- const [id, name, note, parentId, parentName, projectId, projectName, hasChildrenStr, childrenCountStr, tagNamesStr, dueDate, deferDate, plannedDate, flaggedStr, completedStr, estimatedMinutesStr] = parts;
132
- // Parse tags from comma-separated quoted strings
133
- let tags = [];
134
- if (tagNamesStr && tagNamesStr.trim() !== '') {
135
- tags = tagNamesStr.split(',').map(tag => tag.replace(/^"(.*)"$/, '$1'));
136
- }
137
- const taskInfo = {
138
- id,
139
- name,
140
- note,
141
- parentId: parentId || undefined,
142
- parentName: parentName || undefined,
143
- projectId: projectId || undefined,
144
- projectName: projectName || undefined,
145
- hasChildren: hasChildrenStr === 'true',
146
- childrenCount: parseInt(childrenCountStr) || 0,
147
- tags,
148
- dueDate: dueDate || undefined,
149
- deferDate: deferDate || undefined,
150
- plannedDate: plannedDate || undefined,
151
- flagged: flaggedStr === 'true',
152
- completed: completedStr === 'true',
153
- estimatedMinutes: estimatedMinutesStr ? parseInt(estimatedMinutesStr) : undefined
154
- };
155
- return {
156
- success: true,
157
- task: taskInfo
158
- };
159
- }
160
- else {
161
- // Try JSON parsing for error messages
162
- const result = JSON.parse(stdout);
163
- return {
164
- success: false,
165
- error: result.error
166
- };
167
- }
168
- }
169
- catch (parseError) {
170
- console.error("Error parsing AppleScript result:", parseError);
15
+ const result = await executeOmniFocusScript('@getTaskById.js', params);
16
+ if (!result.success || !result.task) {
171
17
  return {
172
18
  success: false,
173
- error: `Failed to parse result: ${stdout}`
19
+ error: result.error || 'Failed to retrieve task'
174
20
  };
175
21
  }
22
+ const rawTask = result.task;
23
+ const taskInfo = {
24
+ id: rawTask.id,
25
+ name: rawTask.name,
26
+ note: rawTask.note || '',
27
+ parentId: rawTask.parentId || undefined,
28
+ parentName: rawTask.parentName || undefined,
29
+ projectId: rawTask.projectId || undefined,
30
+ projectName: rawTask.projectName || undefined,
31
+ hasChildren: Boolean(rawTask.hasChildren),
32
+ childrenCount: rawTask.childrenCount || 0,
33
+ tags: rawTask.tags || [],
34
+ dueDate: rawTask.dueDate || undefined,
35
+ deferDate: rawTask.deferDate || undefined,
36
+ plannedDate: rawTask.plannedDate || undefined,
37
+ flagged: Boolean(rawTask.flagged),
38
+ completed: Boolean(rawTask.completed),
39
+ estimatedMinutes: rawTask.estimatedMinutes ?? undefined,
40
+ attachments: normalizeTaskAttachments(rawTask.attachments, rawTask.linkedFileURLs),
41
+ linkedFileURLs: rawTask.linkedFileURLs || []
42
+ };
43
+ return {
44
+ success: true,
45
+ task: taskInfo
46
+ };
176
47
  }
177
48
  catch (error) {
178
49
  console.error("Error in getTaskById:", error);