omnifocus-mcp-enhanced 1.6.7 → 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.
- package/README.md +75 -12
- package/README.zh.md +58 -12
- package/dist/server.js +5 -1
- package/dist/tools/definitions/editItem.js +5 -0
- package/dist/tools/definitions/getTaskById.js +34 -25
- package/dist/tools/definitions/getTaskById.test.js +32 -0
- package/dist/tools/definitions/moveTask.js +54 -0
- package/dist/tools/definitions/moveTask.test.js +27 -0
- package/dist/tools/definitions/plannedDateSchemas.test.js +10 -0
- package/dist/tools/definitions/readTaskAttachment.js +73 -0
- package/dist/tools/definitions/readTaskAttachment.test.js +32 -0
- package/dist/tools/dumpDatabase.js +3 -2
- package/dist/tools/primitives/addOmniFocusTask.js +11 -9
- package/dist/tools/primitives/addOmniFocusTask.test.js +18 -1
- package/dist/tools/primitives/addProject.js +11 -9
- package/dist/tools/primitives/addProject.test.js +20 -0
- package/dist/tools/primitives/editItem.js +209 -42
- package/dist/tools/primitives/editItem.test.js +82 -0
- package/dist/tools/primitives/getTaskById.js +30 -159
- package/dist/tools/primitives/moveTask.js +55 -0
- package/dist/tools/primitives/moveTask.test.js +29 -0
- package/dist/tools/primitives/readTaskAttachment.js +91 -0
- package/dist/tools/primitives/taskAttachments.js +130 -0
- package/dist/tools/primitives/taskAttachments.test.js +39 -0
- package/dist/utils/dateFormatter.js +32 -0
- package/dist/utils/dateFormatter.test.js +15 -1
- package/dist/utils/omnifocusScripts/getTaskById.js +94 -0
- package/dist/utils/omnifocusScripts/omnifocusDump.js +30 -2
- package/dist/utils/omnifocusScripts/readTaskAttachment.js +100 -0
- package/docs/roadmap/2026-02-25-batch-move-tasks-plan.md +55 -0
- package/docs/roadmap/2026-02-25-batch-move-tasks-plan.zh.md +55 -0
- package/package.json +1 -1
- package/src/server.ts +17 -1
- package/src/tools/definitions/editItem.ts +5 -0
- package/src/tools/definitions/getTaskById.test.ts +34 -0
- package/src/tools/definitions/getTaskById.ts +44 -34
- package/src/tools/definitions/moveTask.test.ts +33 -0
- package/src/tools/definitions/moveTask.ts +63 -0
- package/src/tools/definitions/plannedDateSchemas.test.ts +12 -0
- package/src/tools/definitions/readTaskAttachment.test.ts +36 -0
- package/src/tools/definitions/readTaskAttachment.ts +89 -0
- package/src/tools/dumpDatabase.ts +5 -3
- package/src/tools/primitives/addOmniFocusTask.test.ts +22 -1
- package/src/tools/primitives/addOmniFocusTask.ts +11 -9
- package/src/tools/primitives/addProject.test.ts +24 -0
- package/src/tools/primitives/addProject.ts +11 -9
- package/src/tools/primitives/editItem.test.ts +99 -0
- package/src/tools/primitives/editItem.ts +228 -42
- package/src/tools/primitives/getTaskById.ts +59 -164
- package/src/tools/primitives/moveTask.test.ts +35 -0
- package/src/tools/primitives/moveTask.ts +80 -0
- package/src/tools/primitives/readTaskAttachment.ts +140 -0
- package/src/tools/primitives/taskAttachments.test.ts +52 -0
- package/src/tools/primitives/taskAttachments.ts +195 -0
- package/src/types.ts +13 -2
- package/src/utils/dateFormatter.test.ts +18 -1
- package/src/utils/dateFormatter.ts +38 -0
- package/src/utils/omnifocusScripts/getTaskById.js +94 -0
- package/src/utils/omnifocusScripts/omnifocusDump.js +30 -2
- 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.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.
|
|
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.
|
|
20
22
|
- **v1.6.6** - Added full Planned Date support (create/edit/read/filter/sort/export), including `plannedDate`/`newPlannedDate` and updated task displays.
|
|
21
23
|
|
|
22
24
|
## ✨ Key Features
|
|
@@ -37,6 +39,7 @@ Enhanced Model Context Protocol (MCP) server for OmniFocus featuring **native cu
|
|
|
37
39
|
- **📅 Time Management** - Due, defer, planned dates, estimates, and scheduling
|
|
38
40
|
- **🏷️ Advanced Tagging** - Tag-based filtering with exact/partial matching
|
|
39
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
|
|
40
43
|
|
|
41
44
|
## 📦 Installation
|
|
42
45
|
|
|
@@ -214,6 +217,25 @@ Efficiently manage multiple tasks:
|
|
|
214
217
|
}
|
|
215
218
|
```
|
|
216
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
|
+
|
|
217
239
|
## 🛠️ Complete Tool Reference
|
|
218
240
|
|
|
219
241
|
### 📊 Database & Task Management
|
|
@@ -221,24 +243,28 @@ Efficiently manage multiple tasks:
|
|
|
221
243
|
2. **add_omnifocus_task** - Create tasks (enhanced with subtask support)
|
|
222
244
|
3. **add_project** - Create projects
|
|
223
245
|
4. **remove_item** - Delete tasks or projects
|
|
224
|
-
5. **edit_item** - Edit tasks or projects
|
|
225
|
-
6. **
|
|
226
|
-
7. **
|
|
227
|
-
8. **
|
|
246
|
+
5. **edit_item** - Edit tasks or projects (now supports task moves: project/parent/inbox)
|
|
247
|
+
6. **move_task** - Move an existing task to project/parent task/inbox
|
|
248
|
+
7. **batch_add_items** - Bulk add (enhanced with subtask support)
|
|
249
|
+
8. **batch_remove_items** - Bulk remove
|
|
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`
|
|
228
252
|
|
|
229
253
|
### 🔍 Built-in Perspective Tools
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
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
|
|
235
259
|
|
|
236
260
|
### 🌟 Custom Perspective Tools (NEW)
|
|
237
|
-
|
|
238
|
-
|
|
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
|
|
239
263
|
|
|
240
264
|
### 📊 Analytics & Tracking
|
|
241
|
-
|
|
265
|
+
18. **get_today_completed_tasks** - View today's completed tasks
|
|
266
|
+
|
|
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)
|
|
242
268
|
|
|
243
269
|
## 🚀 Quick Start Examples
|
|
244
270
|
|
|
@@ -272,6 +298,43 @@ add_omnifocus_task {
|
|
|
272
298
|
}
|
|
273
299
|
```
|
|
274
300
|
|
|
301
|
+
### Task Move Operations
|
|
302
|
+
```bash
|
|
303
|
+
# Move task to a project
|
|
304
|
+
move_task {
|
|
305
|
+
"id": "task-id-123",
|
|
306
|
+
"targetProjectName": "Planning"
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
# Move task under another task
|
|
310
|
+
move_task {
|
|
311
|
+
"id": "task-id-123",
|
|
312
|
+
"targetParentTaskId": "parent-task-id-456"
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
# Move task back to inbox
|
|
316
|
+
move_task {
|
|
317
|
+
"id": "task-id-123",
|
|
318
|
+
"targetInbox": true
|
|
319
|
+
}
|
|
320
|
+
```
|
|
321
|
+
|
|
322
|
+
Task move safety rules:
|
|
323
|
+
- Name lookups fail fast on duplicates and ask you to use IDs.
|
|
324
|
+
- Destination must be exactly one type: project OR parent task OR inbox.
|
|
325
|
+
- Moving a task into itself/its descendants is blocked to prevent cycles.
|
|
326
|
+
|
|
327
|
+
You can also move with `edit_item` and combine move + field updates:
|
|
328
|
+
```bash
|
|
329
|
+
edit_item {
|
|
330
|
+
"itemType": "task",
|
|
331
|
+
"id": "task-id-123",
|
|
332
|
+
"newProjectName": "Planning",
|
|
333
|
+
"newName": "Review tmux workflow",
|
|
334
|
+
"newFlagged": true
|
|
335
|
+
}
|
|
336
|
+
```
|
|
337
|
+
|
|
275
338
|
### Smart Task Discovery
|
|
276
339
|
```bash
|
|
277
340
|
# Find high-priority work
|
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
|
### 📊 数据库与任务管理
|
|
@@ -217,24 +238,28 @@ get_custom_perspective_tasks {
|
|
|
217
238
|
2. **add_omnifocus_task** - 创建任务(增强子任务支持)
|
|
218
239
|
3. **add_project** - 创建项目
|
|
219
240
|
4. **remove_item** - 删除任务或项目
|
|
220
|
-
5. **edit_item** -
|
|
221
|
-
6. **
|
|
222
|
-
7. **
|
|
223
|
-
8. **
|
|
241
|
+
5. **edit_item** - 编辑任务或项目(现已支持任务转移:项目/父任务/Inbox)
|
|
242
|
+
6. **move_task** - 将已有任务转移到项目/父任务/Inbox
|
|
243
|
+
7. **batch_add_items** - 批量添加(增强子任务支持)
|
|
244
|
+
8. **batch_remove_items** - 批量删除
|
|
245
|
+
9. **get_task_by_id** - 查询任务信息,并返回附件元信息
|
|
246
|
+
10. **read_task_attachment** - 读取 `get_task_by_id` 返回的任务附件
|
|
224
247
|
|
|
225
248
|
### 🔍 内置透视工具
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
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** - 无限组合的终极过滤
|
|
231
254
|
|
|
232
255
|
### 🌟 自定义透视工具(新功能)
|
|
233
|
-
|
|
234
|
-
|
|
256
|
+
16. **list_custom_perspectives** - 🌟 **新功能**:列出所有自定义透视及详情
|
|
257
|
+
17. **get_custom_perspective_tasks** - 🌟 **新功能**:访问自定义透视,支持层级显示
|
|
235
258
|
|
|
236
259
|
### 📊 分析与跟踪
|
|
237
|
-
|
|
260
|
+
18. **get_today_completed_tasks** - 查看今日完成的任务
|
|
261
|
+
|
|
262
|
+
批量转移功能后续计划(Roadmap):[docs/roadmap/2026-02-25-batch-move-tasks-plan.zh.md](docs/roadmap/2026-02-25-batch-move-tasks-plan.zh.md)
|
|
238
263
|
|
|
239
264
|
## 🚀 快速开始示例
|
|
240
265
|
|
|
@@ -268,6 +293,27 @@ add_omnifocus_task {
|
|
|
268
293
|
}
|
|
269
294
|
```
|
|
270
295
|
|
|
296
|
+
### 任务转移操作
|
|
297
|
+
```bash
|
|
298
|
+
# 转移到项目
|
|
299
|
+
move_task {
|
|
300
|
+
"id": "task-id-123",
|
|
301
|
+
"targetProjectName": "规划"
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
# 转移到父任务下
|
|
305
|
+
move_task {
|
|
306
|
+
"id": "task-id-123",
|
|
307
|
+
"targetParentTaskId": "parent-task-id-456"
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
# 转移回 Inbox
|
|
311
|
+
move_task {
|
|
312
|
+
"id": "task-id-123",
|
|
313
|
+
"targetInbox": true
|
|
314
|
+
}
|
|
315
|
+
```
|
|
316
|
+
|
|
271
317
|
### 智能任务发现
|
|
272
318
|
```bash
|
|
273
319
|
# 找到高优先级工作
|
package/dist/server.js
CHANGED
|
@@ -7,9 +7,11 @@ 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';
|
|
14
|
+
import * as readTaskAttachmentTool from './tools/definitions/readTaskAttachment.js';
|
|
13
15
|
import * as getTodayCompletedTasksTool from './tools/definitions/getTodayCompletedTasks.js';
|
|
14
16
|
// Import perspective tools
|
|
15
17
|
import * as getInboxTasksTool from './tools/definitions/getInboxTasks.js';
|
|
@@ -24,7 +26,7 @@ import * as getCustomPerspectiveTasksTool from './tools/definitions/getCustomPer
|
|
|
24
26
|
// Create an MCP server
|
|
25
27
|
const server = new McpServer({
|
|
26
28
|
name: "OmniFocus MCP",
|
|
27
|
-
version: "1.6.
|
|
29
|
+
version: "1.6.9"
|
|
28
30
|
});
|
|
29
31
|
// Register tools
|
|
30
32
|
server.tool("dump_database", "Gets the current state of your OmniFocus database", dumpDatabaseTool.schema.shape, dumpDatabaseTool.handler);
|
|
@@ -32,9 +34,11 @@ server.tool("add_omnifocus_task", "Add a new task to OmniFocus", addOmniFocusTas
|
|
|
32
34
|
server.tool("add_project", "Add a new project to OmniFocus", addProjectTool.schema.shape, addProjectTool.handler);
|
|
33
35
|
server.tool("remove_item", "Remove a task or project from OmniFocus", removeItemTool.schema.shape, removeItemTool.handler);
|
|
34
36
|
server.tool("edit_item", "Edit a task or project in OmniFocus", editItemTool.schema.shape, editItemTool.handler);
|
|
37
|
+
server.tool("move_task", "Move an existing task to a project, parent task, or inbox", moveTaskTool.schema.shape, moveTaskTool.handler);
|
|
35
38
|
server.tool("batch_add_items", "Add multiple tasks or projects to OmniFocus in a single operation", batchAddItemsTool.schema.shape, batchAddItemsTool.handler);
|
|
36
39
|
server.tool("batch_remove_items", "Remove multiple tasks or projects from OmniFocus in a single operation", batchRemoveItemsTool.schema.shape, batchRemoveItemsTool.handler);
|
|
37
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);
|
|
38
42
|
server.tool("get_today_completed_tasks", "Get tasks completed today - view today's accomplishments", getTodayCompletedTasksTool.schema.shape, getTodayCompletedTasksTool.handler);
|
|
39
43
|
// Register perspective tools
|
|
40
44
|
server.tool("get_inbox_tasks", "Get tasks from OmniFocus inbox perspective", getInboxTasksTool.schema.shape, getInboxTasksTool.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"),
|
|
@@ -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:
|
|
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,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,
|
|
@@ -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:
|
|
62
|
-
linkedFileURLs: [],
|
|
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
|
};
|