omnifocus-mcp-enhanced 1.6.11 → 1.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +36 -1
- package/README.zh.md +3 -0
- package/dist/omnifocustypes.js +11 -0
- package/dist/server.js +15 -1
- package/dist/tools/definitions/addFolder.js +40 -0
- package/dist/tools/definitions/addOmniFocusTask.js +5 -1
- package/dist/tools/definitions/addProject.js +5 -1
- package/dist/tools/definitions/editFolder.js +39 -0
- package/dist/tools/definitions/editItem.js +5 -1
- package/dist/tools/definitions/getFolder.js +53 -0
- package/dist/tools/definitions/listFolders.js +18 -0
- package/dist/tools/definitions/plannedDateSchemas.test.js +25 -0
- package/dist/tools/definitions/removeFolder.js +56 -0
- package/dist/tools/definitions/setRepetitionRule.js +88 -0
- package/dist/tools/definitions/setRepetitionRule.test.js +42 -0
- package/dist/tools/primitives/addFolder.js +87 -0
- package/dist/tools/primitives/addFolder.test.js +27 -0
- package/dist/tools/primitives/addOmniFocusTask.js +22 -4
- package/dist/tools/primitives/addProject.js +22 -4
- package/dist/tools/primitives/applyTagsExclusive.js +37 -0
- package/dist/tools/primitives/applyTagsExclusive.test.js +19 -0
- package/dist/tools/primitives/editFolder.js +196 -0
- package/dist/tools/primitives/editItem.js +35 -3
- package/dist/tools/primitives/editItem.test.js +22 -0
- package/dist/tools/primitives/getFolder.js +27 -0
- package/dist/tools/primitives/listFolders.js +32 -0
- package/dist/tools/primitives/listFolders.test.js +59 -0
- package/dist/tools/primitives/removeFolder.js +117 -0
- package/dist/tools/primitives/setRepetitionRule.js +85 -0
- package/dist/tools/primitives/setRepetitionRule.test.js +65 -0
- package/dist/utils/omnifocusScripts/applyTagsExclusive.js +106 -0
- package/dist/utils/omnifocusScripts/getFolder.js +107 -0
- package/dist/utils/omnifocusScripts/listFolders.js +62 -0
- package/dist/utils/omnifocusScripts/setRepetitionRule.js +117 -0
- package/dist/utils/scriptExecution.js +7 -3
- package/package.json +1 -1
- package/src/omnifocustypes.ts +12 -0
- package/src/server.ts +51 -1
- package/src/tools/definitions/addFolder.ts +45 -0
- package/src/tools/definitions/addOmniFocusTask.ts +6 -1
- package/src/tools/definitions/addProject.ts +6 -1
- package/src/tools/definitions/editFolder.ts +44 -0
- package/src/tools/definitions/editItem.ts +6 -1
- package/src/tools/definitions/getFolder.ts +59 -0
- package/src/tools/definitions/listFolders.ts +21 -0
- package/src/tools/definitions/plannedDateSchemas.test.ts +31 -0
- package/src/tools/definitions/removeFolder.ts +60 -0
- package/src/tools/definitions/setRepetitionRule.test.ts +53 -0
- package/src/tools/definitions/setRepetitionRule.ts +93 -0
- package/src/tools/primitives/addFolder.test.ts +35 -0
- package/src/tools/primitives/addFolder.ts +101 -0
- package/src/tools/primitives/addOmniFocusTask.ts +28 -6
- package/src/tools/primitives/addProject.ts +28 -6
- package/src/tools/primitives/applyTagsExclusive.test.ts +23 -0
- package/src/tools/primitives/applyTagsExclusive.ts +56 -0
- package/src/tools/primitives/editFolder.ts +228 -0
- package/src/tools/primitives/editItem.test.ts +26 -0
- package/src/tools/primitives/editItem.ts +42 -3
- package/src/tools/primitives/getFolder.ts +60 -0
- package/src/tools/primitives/listFolders.test.ts +82 -0
- package/src/tools/primitives/listFolders.ts +55 -0
- package/src/tools/primitives/removeFolder.ts +142 -0
- package/src/tools/primitives/setRepetitionRule.test.ts +76 -0
- package/src/tools/primitives/setRepetitionRule.ts +132 -0
- package/src/types.ts +11 -0
- package/src/utils/omnifocusScripts/applyTagsExclusive.js +106 -0
- package/src/utils/omnifocusScripts/getFolder.js +107 -0
- package/src/utils/omnifocusScripts/listFolders.js +62 -0
- package/src/utils/omnifocusScripts/setRepetitionRule.js +117 -0
- package/src/utils/scriptExecution.ts +9 -8
package/README.md
CHANGED
|
@@ -44,6 +44,8 @@ Want to see where the project is heading next? See the [roadmap](docs/roadmap/20
|
|
|
44
44
|
|
|
45
45
|
## 🆕 Latest Release
|
|
46
46
|
|
|
47
|
+
- **v1.8.0** - Added full **Folder Management** support: `add_folder`, `edit_folder`, `remove_folder`, `list_folders`, and `get_folder`. Create nested folder hierarchies, rename/move folders (with cycle protection), and inspect folder contents (child projects + subfolders). Note: removing a folder permanently deletes all projects and tasks it contains.
|
|
48
|
+
- **v1.7.0** - Added OmniFocus 4.7+ repeat rule support via `set_repetition_rule` (ICS rule strings, schedule type, anchor date, catch-up, end date, repetition count), mutually exclusive tag support via `exclusiveTags` on add/edit tools, and improved planned-date editing tests.
|
|
47
49
|
- **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.
|
|
48
50
|
- **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.
|
|
49
51
|
- **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.
|
|
@@ -64,8 +66,11 @@ Want to see where the project is heading next? See the [roadmap](docs/roadmap/20
|
|
|
64
66
|
- **🎯 Batch Operations** - Add/remove multiple tasks efficiently
|
|
65
67
|
- **📊 Smart Querying** - Find tasks by ID, name, or complex criteria
|
|
66
68
|
- **🔄 Full CRUD Operations** - Create, read, update, delete tasks and projects
|
|
69
|
+
- **📁 Folder Management** - Full CRUD for folders with nested hierarchy, move/rename, and content inspection
|
|
67
70
|
- **📅 Time Management** - Due, defer, planned dates, estimates, and scheduling
|
|
68
71
|
- **🏷️ Advanced Tagging** - Tag-based filtering with exact/partial matching
|
|
72
|
+
- **🚫 Mutually Exclusive Tags** - Automatically respects exclusive tag groups when applying tags
|
|
73
|
+
- **🔁 Repeat Rules** - Full OmniFocus 4.7+ repetition support (ICS rules, schedule type, anchor date, catch-up, end date, count)
|
|
69
74
|
- **🤖 AI Integration** - Seamless Claude AI integration for intelligent workflows
|
|
70
75
|
- **🖼️ Attachment-Aware Reads** - Surface note attachments and linked files before deciding whether AI should inspect them
|
|
71
76
|
|
|
@@ -509,8 +514,15 @@ read_task_attachment {
|
|
|
509
514
|
16. **list_custom_perspectives** - 🌟 **NEW**: List all custom perspectives with details
|
|
510
515
|
17. **get_custom_perspective_tasks** - 🌟 **NEW**: Access custom perspective with hierarchical display
|
|
511
516
|
|
|
517
|
+
### 📁 Folder Management Tools (NEW)
|
|
518
|
+
18. **add_folder** - 🆕 **NEW**: Create a folder, optionally nested under a parent folder
|
|
519
|
+
19. **edit_folder** - 🆕 **NEW**: Rename a folder or move it under a different parent (empty string moves to root)
|
|
520
|
+
20. **remove_folder** - 🆕 **NEW**: Delete a folder (⚠️ also deletes all contained projects and tasks)
|
|
521
|
+
21. **list_folders** - 🆕 **NEW**: List all folders with IDs, parents, status, and project counts
|
|
522
|
+
22. **get_folder** - 🆕 **NEW**: Get a single folder with its child projects and subfolders
|
|
523
|
+
|
|
512
524
|
### 📊 Analytics & Tracking
|
|
513
|
-
|
|
525
|
+
23. **get_today_completed_tasks** - View today's completed tasks
|
|
514
526
|
|
|
515
527
|
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)
|
|
516
528
|
|
|
@@ -620,6 +632,29 @@ get_custom_perspective_tasks {
|
|
|
620
632
|
}
|
|
621
633
|
```
|
|
622
634
|
|
|
635
|
+
### 📁 Folder Management
|
|
636
|
+
|
|
637
|
+
```bash
|
|
638
|
+
# List all folders with project counts
|
|
639
|
+
list_folders {"includeDropped": false}
|
|
640
|
+
|
|
641
|
+
# Create a top-level folder
|
|
642
|
+
add_folder {"name": "Work"}
|
|
643
|
+
|
|
644
|
+
# Create a nested folder
|
|
645
|
+
add_folder {"name": "Clients", "parentFolderName": "Work"}
|
|
646
|
+
|
|
647
|
+
# Inspect a folder's projects and subfolders
|
|
648
|
+
get_folder {"name": "Work"}
|
|
649
|
+
|
|
650
|
+
# Rename or move a folder (empty string moves to root)
|
|
651
|
+
edit_folder {"name": "Clients", "newName": "Key Clients"}
|
|
652
|
+
edit_folder {"name": "Key Clients", "newParentFolderName": ""}
|
|
653
|
+
|
|
654
|
+
# Delete a folder (⚠️ also deletes all contained projects and tasks)
|
|
655
|
+
remove_folder {"name": "Old Archive"}
|
|
656
|
+
```
|
|
657
|
+
|
|
623
658
|
## 🔧 Configuration
|
|
624
659
|
|
|
625
660
|
### Claude Code
|
package/README.zh.md
CHANGED
|
@@ -40,6 +40,7 @@ OmniFocus 本身已经很强了,但它大多数时候仍然是一个需要你
|
|
|
40
40
|
|
|
41
41
|
## 🆕 最新版本
|
|
42
42
|
|
|
43
|
+
- **v1.7.0** - 新增 OmniFocus 4.7+ 重复规则支持(`set_repetition_rule`:ICS 规则、重复方式、锚定日期、自动追平、结束日期、重复次数),新增互斥标签支持(`exclusiveTags`),并补全计划日期编辑测试。
|
|
43
44
|
- **v1.6.10** - 修复 `edit_item` 无法完成 Inbox 任务的问题,修复 AppleScript 对单引号/反斜杠等特殊字符的处理,修复特殊字符导致的 JSON 返回解析失败,并补充 `batch_add_items` / `mcporter` 的可直接运行示例与说明。
|
|
44
45
|
- **v1.6.9** - 新增任务附件支持:`get_task_by_id` 会返回附件元信息,`dump_database` 导出附件/链接元信息,并新增 `read_task_attachment`,可在支持时直接把图片附件作为 MCP 图片内容返回。
|
|
45
46
|
- **v1.6.6** - 新增 Planned Date(计划日期)全链路支持:创建/编辑/读取/过滤/排序/导出,包含 `plannedDate` / `newPlannedDate`。
|
|
@@ -61,6 +62,8 @@ OmniFocus 本身已经很强了,但它大多数时候仍然是一个需要你
|
|
|
61
62
|
- **🔄 完整 CRUD 操作** - 创建、读取、更新、删除任务和项目
|
|
62
63
|
- **📅 时间管理** - 截止日期、推迟日期、计划日期、估时和计划
|
|
63
64
|
- **🏷️ 高级标签** - 基于标签的精确/模糊匹配过滤
|
|
65
|
+
- **🚫 互斥标签** - 应用标签时自动遵守互斥标签组规则
|
|
66
|
+
- **🔁 重复规则** - 完整支持 OmniFocus 4.7+ 重复任务(ICS 规则、重复方式、锚定日期、自动追平、结束日期、重复次数)
|
|
64
67
|
- **🤖 AI 集成** - 与 Claude AI 无缝集成,实现智能工作流
|
|
65
68
|
- **🖼️ 附件感知读取** - 先暴露备注附件和链接文件的元信息,再决定是否让 AI 继续查看附件内容
|
|
66
69
|
|
package/dist/omnifocustypes.js
CHANGED
|
@@ -18,6 +18,17 @@ export var Task;
|
|
|
18
18
|
RepetitionMethod[RepetitionMethod["Fixed"] = 2] = "Fixed";
|
|
19
19
|
RepetitionMethod[RepetitionMethod["None"] = 3] = "None";
|
|
20
20
|
})(RepetitionMethod = Task.RepetitionMethod || (Task.RepetitionMethod = {}));
|
|
21
|
+
let RepetitionScheduleType;
|
|
22
|
+
(function (RepetitionScheduleType) {
|
|
23
|
+
RepetitionScheduleType[RepetitionScheduleType["Regularly"] = 0] = "Regularly";
|
|
24
|
+
RepetitionScheduleType[RepetitionScheduleType["FromCompletion"] = 1] = "FromCompletion";
|
|
25
|
+
})(RepetitionScheduleType = Task.RepetitionScheduleType || (Task.RepetitionScheduleType = {}));
|
|
26
|
+
let AnchorDateKey;
|
|
27
|
+
(function (AnchorDateKey) {
|
|
28
|
+
AnchorDateKey[AnchorDateKey["DeferDate"] = 0] = "DeferDate";
|
|
29
|
+
AnchorDateKey[AnchorDateKey["DueDate"] = 1] = "DueDate";
|
|
30
|
+
AnchorDateKey[AnchorDateKey["PlannedDate"] = 2] = "PlannedDate";
|
|
31
|
+
})(AnchorDateKey = Task.AnchorDateKey || (Task.AnchorDateKey = {}));
|
|
21
32
|
})(Task || (Task = {}));
|
|
22
33
|
export var Project;
|
|
23
34
|
(function (Project) {
|
package/dist/server.js
CHANGED
|
@@ -13,6 +13,7 @@ import * as batchRemoveItemsTool from './tools/definitions/batchRemoveItems.js';
|
|
|
13
13
|
import * as getTaskByIdTool from './tools/definitions/getTaskById.js';
|
|
14
14
|
import * as readTaskAttachmentTool from './tools/definitions/readTaskAttachment.js';
|
|
15
15
|
import * as getTodayCompletedTasksTool from './tools/definitions/getTodayCompletedTasks.js';
|
|
16
|
+
import * as setRepetitionRuleTool from './tools/definitions/setRepetitionRule.js';
|
|
16
17
|
// Import perspective tools
|
|
17
18
|
import * as getInboxTasksTool from './tools/definitions/getInboxTasks.js';
|
|
18
19
|
import * as getFlaggedTasksTool from './tools/definitions/getFlaggedTasks.js';
|
|
@@ -24,10 +25,16 @@ import * as filterTasksTool from './tools/definitions/filterTasks.js';
|
|
|
24
25
|
// Import custom perspective tools
|
|
25
26
|
import * as listCustomPerspectivesTool from './tools/definitions/listCustomPerspectives.js';
|
|
26
27
|
import * as getCustomPerspectiveTasksTool from './tools/definitions/getCustomPerspectiveTasks.js';
|
|
28
|
+
// Import folder management tools
|
|
29
|
+
import * as addFolderTool from './tools/definitions/addFolder.js';
|
|
30
|
+
import * as editFolderTool from './tools/definitions/editFolder.js';
|
|
31
|
+
import * as removeFolderTool from './tools/definitions/removeFolder.js';
|
|
32
|
+
import * as listFoldersTool from './tools/definitions/listFolders.js';
|
|
33
|
+
import * as getFolderTool from './tools/definitions/getFolder.js';
|
|
27
34
|
// Create an MCP server
|
|
28
35
|
const server = new McpServer({
|
|
29
36
|
name: "OmniFocus MCP",
|
|
30
|
-
version: "1.
|
|
37
|
+
version: "1.8.0"
|
|
31
38
|
});
|
|
32
39
|
// Register tools
|
|
33
40
|
server.tool("dump_database", "Gets the current state of your OmniFocus database", dumpDatabaseTool.schema.shape, dumpDatabaseTool.handler);
|
|
@@ -41,6 +48,7 @@ server.tool("batch_remove_items", "Remove multiple tasks or projects from OmniFo
|
|
|
41
48
|
server.tool("get_task_by_id", "Get information about a specific task by ID or name", getTaskByIdTool.schema.shape, getTaskByIdTool.handler);
|
|
42
49
|
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);
|
|
43
50
|
server.tool("get_today_completed_tasks", "Get tasks completed today - view today's accomplishments", getTodayCompletedTasksTool.schema.shape, getTodayCompletedTasksTool.handler);
|
|
51
|
+
server.tool("set_repetition_rule", "Set, update, or clear the repeat rule on a task. Supports ICS rule strings, schedule type, anchor date, catch-up, end date, and repetition count.", setRepetitionRuleTool.schema.shape, setRepetitionRuleTool.handler);
|
|
44
52
|
// Register perspective tools
|
|
45
53
|
server.tool("get_inbox_tasks", "Get tasks from OmniFocus inbox perspective", getInboxTasksTool.schema.shape, getInboxTasksTool.handler);
|
|
46
54
|
server.tool("get_flagged_tasks", "Get flagged tasks from OmniFocus with optional project filtering", getFlaggedTasksTool.schema.shape, getFlaggedTasksTool.handler);
|
|
@@ -52,6 +60,12 @@ server.tool("filter_tasks", "Advanced task filtering with unlimited perspective
|
|
|
52
60
|
// Custom perspective tools
|
|
53
61
|
server.tool("list_custom_perspectives", "List all custom perspectives defined in OmniFocus", listCustomPerspectivesTool.schema.shape, listCustomPerspectivesTool.handler);
|
|
54
62
|
server.tool("get_custom_perspective_tasks", "Get tasks from a specific OmniFocus custom perspective by name. Use this when user refers to perspective names like '今日工作安排', '今日复盘', '本周项目' etc. - these are custom views created in OmniFocus, NOT tags. Supports hierarchical tree display of task relationships.", getCustomPerspectiveTasksTool.schema.shape, getCustomPerspectiveTasksTool.handler);
|
|
63
|
+
// Folder management tools
|
|
64
|
+
server.tool("add_folder", "Create a new folder in OmniFocus, optionally nested under a parent folder. Folders organize projects into a hierarchy.", addFolderTool.schema.shape, addFolderTool.handler);
|
|
65
|
+
server.tool("edit_folder", "Rename a folder or move it under a different parent folder (use an empty string for newParentFolderName to move it to the root level).", editFolderTool.schema.shape, editFolderTool.handler);
|
|
66
|
+
server.tool("remove_folder", "Remove a folder from OmniFocus. WARNING: this also permanently deletes all projects and tasks contained in the folder.", removeFolderTool.schema.shape, removeFolderTool.handler);
|
|
67
|
+
server.tool("list_folders", "List all OmniFocus folders with IDs, parent relationships, status, and project counts without loading tasks.", listFoldersTool.schema.shape, listFoldersTool.handler);
|
|
68
|
+
server.tool("get_folder", "Get a single OmniFocus folder by ID or name, including its child projects and subfolders.", getFolderTool.schema.shape, getFolderTool.handler);
|
|
55
69
|
// Start the MCP server
|
|
56
70
|
const transport = new StdioServerTransport();
|
|
57
71
|
// Use await with server.connect to ensure proper connection
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { addFolder } from '../primitives/addFolder.js';
|
|
3
|
+
export const schema = z.object({
|
|
4
|
+
name: z.string().describe('The name of the folder'),
|
|
5
|
+
parentFolderName: z.string().optional().describe('The name of the parent folder to nest this folder under (created at the root level if not specified)')
|
|
6
|
+
});
|
|
7
|
+
export async function handler(args, _extra) {
|
|
8
|
+
try {
|
|
9
|
+
const result = await addFolder(args);
|
|
10
|
+
if (result.success) {
|
|
11
|
+
const locationText = args.parentFolderName
|
|
12
|
+
? `inside folder "${args.parentFolderName}"`
|
|
13
|
+
: 'at the root level';
|
|
14
|
+
return {
|
|
15
|
+
content: [{
|
|
16
|
+
type: 'text',
|
|
17
|
+
text: `✅ Folder "${args.name}" created successfully ${locationText}.\n\nid: ${result.folderId}`
|
|
18
|
+
}]
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
return {
|
|
22
|
+
content: [{
|
|
23
|
+
type: 'text',
|
|
24
|
+
text: `Failed to create folder: ${result.error}`
|
|
25
|
+
}],
|
|
26
|
+
isError: true
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
catch (err) {
|
|
30
|
+
const error = err;
|
|
31
|
+
console.error(`Tool execution error: ${error.message}`);
|
|
32
|
+
return {
|
|
33
|
+
content: [{
|
|
34
|
+
type: 'text',
|
|
35
|
+
text: `Error creating folder: ${error.message}`
|
|
36
|
+
}],
|
|
37
|
+
isError: true
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
}
|
|
@@ -9,6 +9,7 @@ export const schema = z.object({
|
|
|
9
9
|
flagged: z.boolean().optional().describe("Whether the task is flagged or not"),
|
|
10
10
|
estimatedMinutes: z.number().optional().describe("Estimated time to complete the task, in minutes"),
|
|
11
11
|
tags: z.array(z.string()).optional().describe("Tags to assign to the task"),
|
|
12
|
+
exclusiveTags: z.boolean().optional().describe("Respect mutually exclusive tag groups when applying tags (default: true). When a tag belongs to an exclusive group, sibling tags from that group are removed."),
|
|
12
13
|
projectName: z.string().optional().describe("The name of the project to add the task to (will add to inbox if not specified)"),
|
|
13
14
|
parentTaskId: z.string().optional().describe("The ID of the parent task to create this task as a subtask"),
|
|
14
15
|
parentTaskName: z.string().optional().describe("The name of the parent task to create this task as a subtask (alternative to parentTaskId)")
|
|
@@ -39,10 +40,13 @@ export async function handler(args, extra) {
|
|
|
39
40
|
let plannedDateText = args.plannedDate
|
|
40
41
|
? ` planned for ${new Date(args.plannedDate).toLocaleDateString()}`
|
|
41
42
|
: "";
|
|
43
|
+
let exclusivityText = result.removedSiblings && result.removedSiblings.length > 0
|
|
44
|
+
? `\nRemoved mutually exclusive tags: ${result.removedSiblings.join(', ')}`
|
|
45
|
+
: "";
|
|
42
46
|
return {
|
|
43
47
|
content: [{
|
|
44
48
|
type: "text",
|
|
45
|
-
text: `✅ Task "${args.name}" created successfully ${locationText}${dueDateText}${plannedDateText}${tagText}.\n\nid: ${result.taskId}`
|
|
49
|
+
text: `✅ Task "${args.name}" created successfully ${locationText}${dueDateText}${plannedDateText}${tagText}.\n\nid: ${result.taskId}${exclusivityText}`
|
|
46
50
|
}]
|
|
47
51
|
};
|
|
48
52
|
}
|
|
@@ -9,6 +9,7 @@ export const schema = z.object({
|
|
|
9
9
|
flagged: z.boolean().optional().describe("Whether the project is flagged or not"),
|
|
10
10
|
estimatedMinutes: z.number().optional().describe("Estimated time to complete the project, in minutes"),
|
|
11
11
|
tags: z.array(z.string()).optional().describe("Tags to assign to the project"),
|
|
12
|
+
exclusiveTags: z.boolean().optional().describe("Respect mutually exclusive tag groups when applying tags (default: true). When a tag belongs to an exclusive group, sibling tags from that group are removed."),
|
|
12
13
|
folderName: z.string().optional().describe("The name of the folder to add the project to (will add to root if not specified)"),
|
|
13
14
|
sequential: z.boolean().optional().describe("Whether tasks in the project should be sequential (default: false)")
|
|
14
15
|
});
|
|
@@ -33,10 +34,13 @@ export async function handler(args, extra) {
|
|
|
33
34
|
let sequentialText = args.sequential
|
|
34
35
|
? " (sequential)"
|
|
35
36
|
: " (parallel)";
|
|
37
|
+
let exclusivityText = result.removedSiblings && result.removedSiblings.length > 0
|
|
38
|
+
? `\nRemoved mutually exclusive tags: ${result.removedSiblings.join(', ')}`
|
|
39
|
+
: "";
|
|
36
40
|
return {
|
|
37
41
|
content: [{
|
|
38
42
|
type: "text",
|
|
39
|
-
text: `✅ Project "${args.name}" created successfully ${locationText}${dueDateText}${plannedDateText}${tagText}${sequentialText}.\n\nid: ${result.projectId}`
|
|
43
|
+
text: `✅ Project "${args.name}" created successfully ${locationText}${dueDateText}${plannedDateText}${tagText}${sequentialText}.\n\nid: ${result.projectId}${exclusivityText}`
|
|
40
44
|
}]
|
|
41
45
|
};
|
|
42
46
|
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { editFolder } from '../primitives/editFolder.js';
|
|
3
|
+
export const schema = z.object({
|
|
4
|
+
id: z.string().optional().describe('The ID of the folder to edit'),
|
|
5
|
+
name: z.string().optional().describe('The name of the folder to edit (as fallback if ID not provided)'),
|
|
6
|
+
newName: z.string().optional().describe('New name for the folder'),
|
|
7
|
+
newParentFolderName: z.string().optional().describe('Move the folder under this parent folder. Use an empty string "" to move it to the root level.')
|
|
8
|
+
});
|
|
9
|
+
export async function handler(args, _extra) {
|
|
10
|
+
try {
|
|
11
|
+
if (!args.id && !args.name) {
|
|
12
|
+
return {
|
|
13
|
+
content: [{ type: 'text', text: 'Either id or name must be provided to edit a folder.' }],
|
|
14
|
+
isError: true
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
const result = await editFolder(args);
|
|
18
|
+
if (result.success) {
|
|
19
|
+
return {
|
|
20
|
+
content: [{
|
|
21
|
+
type: 'text',
|
|
22
|
+
text: `✅ Folder "${result.name}" updated successfully.\nChanged: ${result.changedProperties || 'nothing'}\n\nid: ${result.id}`
|
|
23
|
+
}]
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
return {
|
|
27
|
+
content: [{ type: 'text', text: `Failed to edit folder: ${result.error}` }],
|
|
28
|
+
isError: true
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
catch (err) {
|
|
32
|
+
const error = err;
|
|
33
|
+
console.error(`Tool execution error: ${error.message}`);
|
|
34
|
+
return {
|
|
35
|
+
content: [{ type: 'text', text: `Error editing folder: ${error.message}` }],
|
|
36
|
+
isError: true
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
}
|
|
@@ -17,6 +17,7 @@ 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
|
+
exclusiveTags: z.boolean().optional().describe("Respect mutually exclusive tag groups when adding/replacing tags (default: true). When a tag belongs to an exclusive group, sibling tags from that group are removed."),
|
|
20
21
|
newProjectId: z.string().optional().describe("For tasks: move task to this project ID"),
|
|
21
22
|
newProjectName: z.string().optional().describe("For tasks: move task to this project name (errors on duplicate names)"),
|
|
22
23
|
newParentTaskId: z.string().optional().describe("For tasks: move task under this parent task ID"),
|
|
@@ -48,10 +49,13 @@ export async function handler(args, extra) {
|
|
|
48
49
|
if (result.changedProperties) {
|
|
49
50
|
changedText = ` (${result.changedProperties})`;
|
|
50
51
|
}
|
|
52
|
+
let exclusivityText = result.removedSiblings && result.removedSiblings.length > 0
|
|
53
|
+
? `\nRemoved mutually exclusive tags: ${result.removedSiblings.join(', ')}`
|
|
54
|
+
: "";
|
|
51
55
|
return {
|
|
52
56
|
content: [{
|
|
53
57
|
type: "text",
|
|
54
|
-
text: `✅ ${itemTypeLabel} "${result.name}" updated successfully${changedText}
|
|
58
|
+
text: `✅ ${itemTypeLabel} "${result.name}" updated successfully${changedText}.${exclusivityText}`
|
|
55
59
|
}]
|
|
56
60
|
};
|
|
57
61
|
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { getFolder } from '../primitives/getFolder.js';
|
|
3
|
+
export const schema = z.object({
|
|
4
|
+
id: z.string().optional().describe('The ID of the folder to get'),
|
|
5
|
+
name: z.string().optional().describe('The name of the folder to get (as fallback if ID not provided)')
|
|
6
|
+
});
|
|
7
|
+
export async function handler(args, _extra) {
|
|
8
|
+
try {
|
|
9
|
+
if (!args.id && !args.name) {
|
|
10
|
+
return {
|
|
11
|
+
content: [{ type: 'text', text: 'Either id or name must be provided to get a folder.' }],
|
|
12
|
+
isError: true
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
const folder = await getFolder({ id: args.id, name: args.name });
|
|
16
|
+
const lines = [];
|
|
17
|
+
lines.push(`# Folder: ${folder.name}`);
|
|
18
|
+
lines.push('');
|
|
19
|
+
lines.push(`- id: ${folder.id}`);
|
|
20
|
+
lines.push(`- status: ${folder.status}`);
|
|
21
|
+
if (folder.parentFolderID) {
|
|
22
|
+
lines.push(`- parent folder id: ${folder.parentFolderID}`);
|
|
23
|
+
}
|
|
24
|
+
lines.push('');
|
|
25
|
+
lines.push(`## Subfolders (${folder.subfolders.length})`);
|
|
26
|
+
if (folder.subfolders.length === 0) {
|
|
27
|
+
lines.push('None');
|
|
28
|
+
}
|
|
29
|
+
else {
|
|
30
|
+
for (const sub of folder.subfolders) {
|
|
31
|
+
lines.push(`- ${sub.name} [${sub.status}] (id:${sub.id})`);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
lines.push('');
|
|
35
|
+
lines.push(`## Projects (${folder.projects.length})`);
|
|
36
|
+
if (folder.projects.length === 0) {
|
|
37
|
+
lines.push('None');
|
|
38
|
+
}
|
|
39
|
+
else {
|
|
40
|
+
for (const project of folder.projects) {
|
|
41
|
+
lines.push(`- ${project.name} [${project.status}] (id:${project.id}, remaining:${project.remainingTaskCount})`);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
return { content: [{ type: 'text', text: lines.join('\n') }] };
|
|
45
|
+
}
|
|
46
|
+
catch (error) {
|
|
47
|
+
const message = error instanceof Error ? error.message : 'Unknown error occurred';
|
|
48
|
+
return {
|
|
49
|
+
content: [{ type: 'text', text: `Error getting folder: ${message}` }],
|
|
50
|
+
isError: true
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { listFolders } from '../primitives/listFolders.js';
|
|
3
|
+
export const schema = z.object({
|
|
4
|
+
includeDropped: z.boolean().optional().describe('Include dropped folders (default: true)')
|
|
5
|
+
});
|
|
6
|
+
export async function handler(args, _extra) {
|
|
7
|
+
try {
|
|
8
|
+
const result = await listFolders(args.includeDropped !== false);
|
|
9
|
+
return { content: [{ type: 'text', text: result }] };
|
|
10
|
+
}
|
|
11
|
+
catch (error) {
|
|
12
|
+
const message = error instanceof Error ? error.message : 'Unknown error occurred';
|
|
13
|
+
return {
|
|
14
|
+
content: [{ type: 'text', text: `Error listing folders: ${message}` }],
|
|
15
|
+
isError: true
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
}
|
|
@@ -74,3 +74,28 @@ test('filter_tasks schema supports planned date filters and sorting', () => {
|
|
|
74
74
|
assert.equal(parsed.plannedBefore, '2026-02-20');
|
|
75
75
|
assert.equal(parsed.sortBy, 'plannedDate');
|
|
76
76
|
});
|
|
77
|
+
test('add_omnifocus_task schema preserves exclusiveTags', () => {
|
|
78
|
+
const parsed = addTaskSchema.parse({
|
|
79
|
+
name: 'Task with exclusive tags',
|
|
80
|
+
tags: ['High', 'Work'],
|
|
81
|
+
exclusiveTags: false
|
|
82
|
+
});
|
|
83
|
+
assert.equal(parsed.exclusiveTags, false);
|
|
84
|
+
});
|
|
85
|
+
test('add_project schema preserves exclusiveTags', () => {
|
|
86
|
+
const parsed = addProjectSchema.parse({
|
|
87
|
+
name: 'Project with exclusive tags',
|
|
88
|
+
tags: ['High'],
|
|
89
|
+
exclusiveTags: true
|
|
90
|
+
});
|
|
91
|
+
assert.equal(parsed.exclusiveTags, true);
|
|
92
|
+
});
|
|
93
|
+
test('edit_item schema preserves exclusiveTags', () => {
|
|
94
|
+
const parsed = editItemSchema.parse({
|
|
95
|
+
itemType: 'task',
|
|
96
|
+
id: 'abc',
|
|
97
|
+
addTags: ['High'],
|
|
98
|
+
exclusiveTags: true
|
|
99
|
+
});
|
|
100
|
+
assert.equal(parsed.exclusiveTags, true);
|
|
101
|
+
});
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { removeFolder } from '../primitives/removeFolder.js';
|
|
3
|
+
export const schema = z.object({
|
|
4
|
+
id: z.string().optional().describe('The ID of the folder to remove'),
|
|
5
|
+
name: z.string().optional().describe('The name of the folder to remove (as fallback if ID not provided)')
|
|
6
|
+
});
|
|
7
|
+
export async function handler(args, _extra) {
|
|
8
|
+
try {
|
|
9
|
+
if (!args.id && !args.name) {
|
|
10
|
+
return {
|
|
11
|
+
content: [{ type: 'text', text: 'Either id or name must be provided to remove a folder.' }],
|
|
12
|
+
isError: true
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
const result = await removeFolder(args);
|
|
16
|
+
if (result.success) {
|
|
17
|
+
const projectCount = result.deletedProjectCount ?? 0;
|
|
18
|
+
const taskCount = result.deletedTaskCount ?? 0;
|
|
19
|
+
const cascadeWarning = (projectCount > 0 || taskCount > 0)
|
|
20
|
+
? `\n⚠️ This also permanently deleted ${projectCount} contained project(s) and ${taskCount} task(s).`
|
|
21
|
+
: '';
|
|
22
|
+
return {
|
|
23
|
+
content: [{
|
|
24
|
+
type: 'text',
|
|
25
|
+
text: `✅ Folder "${result.name}" removed successfully.${cascadeWarning}`
|
|
26
|
+
}]
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
let errorMsg = 'Failed to remove folder';
|
|
30
|
+
if (result.error) {
|
|
31
|
+
if (result.error.includes('Folder not found')) {
|
|
32
|
+
errorMsg = 'Folder not found';
|
|
33
|
+
if (args.id)
|
|
34
|
+
errorMsg += ` with ID "${args.id}"`;
|
|
35
|
+
if (args.name)
|
|
36
|
+
errorMsg += `${args.id ? ' or' : ' with'} name "${args.name}"`;
|
|
37
|
+
errorMsg += '.';
|
|
38
|
+
}
|
|
39
|
+
else {
|
|
40
|
+
errorMsg += `: ${result.error}`;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
return {
|
|
44
|
+
content: [{ type: 'text', text: errorMsg }],
|
|
45
|
+
isError: true
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
catch (err) {
|
|
49
|
+
const error = err;
|
|
50
|
+
console.error(`Tool execution error: ${error.message}`);
|
|
51
|
+
return {
|
|
52
|
+
content: [{ type: 'text', text: `Error removing folder: ${error.message}` }],
|
|
53
|
+
isError: true
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { setRepetitionRule } from '../primitives/setRepetitionRule.js';
|
|
3
|
+
export const schema = z.object({
|
|
4
|
+
taskId: z.string().describe('The ID of the task to modify'),
|
|
5
|
+
ruleString: z
|
|
6
|
+
.string()
|
|
7
|
+
.optional()
|
|
8
|
+
.describe('ICS recurrence rule string, e.g. FREQ=WEEKLY;INTERVAL=2. Defaults to FREQ=WEEKLY.'),
|
|
9
|
+
scheduleType: z
|
|
10
|
+
.enum(['Regularly', 'FromCompletion'])
|
|
11
|
+
.optional()
|
|
12
|
+
.describe('How the next occurrence is scheduled. Regularly repeats from assigned dates; FromCompletion repeats after completion.'),
|
|
13
|
+
anchorDateKey: z
|
|
14
|
+
.enum(['DueDate', 'DeferDate', 'PlannedDate'])
|
|
15
|
+
.optional()
|
|
16
|
+
.describe('Which date property is advanced when the task repeats.'),
|
|
17
|
+
catchUpAutomatically: z
|
|
18
|
+
.boolean()
|
|
19
|
+
.optional()
|
|
20
|
+
.describe('When true, missed occurrences are skipped and the next future occurrence is created.'),
|
|
21
|
+
endDate: z
|
|
22
|
+
.string()
|
|
23
|
+
.optional()
|
|
24
|
+
.describe('ISO date string for when the repetition ends. Encoded into the rule as UNTIL=.'),
|
|
25
|
+
count: z
|
|
26
|
+
.number()
|
|
27
|
+
.optional()
|
|
28
|
+
.describe('Number of repetitions after which the rule ends. Encoded into the rule as COUNT=.'),
|
|
29
|
+
clear: z
|
|
30
|
+
.boolean()
|
|
31
|
+
.optional()
|
|
32
|
+
.describe('Set to true to remove the repetition rule from the task.'),
|
|
33
|
+
});
|
|
34
|
+
export async function handler(args, extra) {
|
|
35
|
+
try {
|
|
36
|
+
const result = await setRepetitionRule({
|
|
37
|
+
taskId: args.taskId,
|
|
38
|
+
ruleString: args.ruleString,
|
|
39
|
+
scheduleType: args.scheduleType,
|
|
40
|
+
anchorDateKey: args.anchorDateKey,
|
|
41
|
+
catchUpAutomatically: args.catchUpAutomatically,
|
|
42
|
+
endDate: args.endDate,
|
|
43
|
+
count: args.count,
|
|
44
|
+
clear: args.clear,
|
|
45
|
+
});
|
|
46
|
+
if (!result.success) {
|
|
47
|
+
return {
|
|
48
|
+
content: [
|
|
49
|
+
{
|
|
50
|
+
type: 'text',
|
|
51
|
+
text: `Failed to set repetition rule: ${result.error || 'Unknown error'}`,
|
|
52
|
+
},
|
|
53
|
+
],
|
|
54
|
+
isError: true,
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
if (result.cleared) {
|
|
58
|
+
return {
|
|
59
|
+
content: [
|
|
60
|
+
{
|
|
61
|
+
type: 'text',
|
|
62
|
+
text: 'Repetition rule cleared successfully.',
|
|
63
|
+
},
|
|
64
|
+
],
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
return {
|
|
68
|
+
content: [
|
|
69
|
+
{
|
|
70
|
+
type: 'text',
|
|
71
|
+
text: `Repetition rule set successfully.\nRule: ${result.ruleString}\nSchedule: ${result.scheduleType}\nAnchor: ${result.anchorDateKey}\nCatch up automatically: ${result.catchUpAutomatically}`,
|
|
72
|
+
},
|
|
73
|
+
],
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
catch (err) {
|
|
77
|
+
const errorMessage = err instanceof Error ? err.message : 'Unknown error occurred';
|
|
78
|
+
return {
|
|
79
|
+
content: [
|
|
80
|
+
{
|
|
81
|
+
type: 'text',
|
|
82
|
+
text: `Error setting repetition rule: ${errorMessage}`,
|
|
83
|
+
},
|
|
84
|
+
],
|
|
85
|
+
isError: true,
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import assert from 'node:assert/strict';
|
|
2
|
+
import test from 'node:test';
|
|
3
|
+
import { schema } from './setRepetitionRule.js';
|
|
4
|
+
test('set_repetition_rule schema requires taskId', () => {
|
|
5
|
+
assert.throws(() => schema.parse({}));
|
|
6
|
+
});
|
|
7
|
+
test('set_repetition_rule schema accepts minimal valid input', () => {
|
|
8
|
+
const parsed = schema.parse({ taskId: 'abc123' });
|
|
9
|
+
assert.equal(parsed.taskId, 'abc123');
|
|
10
|
+
});
|
|
11
|
+
test('set_repetition_rule schema accepts full repetition config', () => {
|
|
12
|
+
const parsed = schema.parse({
|
|
13
|
+
taskId: 'abc123',
|
|
14
|
+
ruleString: 'FREQ=WEEKLY;INTERVAL=2',
|
|
15
|
+
scheduleType: 'FromCompletion',
|
|
16
|
+
anchorDateKey: 'PlannedDate',
|
|
17
|
+
catchUpAutomatically: true,
|
|
18
|
+
endDate: '2026-12-31T23:59:59.000Z',
|
|
19
|
+
count: 10,
|
|
20
|
+
});
|
|
21
|
+
assert.equal(parsed.ruleString, 'FREQ=WEEKLY;INTERVAL=2');
|
|
22
|
+
assert.equal(parsed.scheduleType, 'FromCompletion');
|
|
23
|
+
assert.equal(parsed.anchorDateKey, 'PlannedDate');
|
|
24
|
+
assert.equal(parsed.catchUpAutomatically, true);
|
|
25
|
+
assert.equal(parsed.count, 10);
|
|
26
|
+
});
|
|
27
|
+
test('set_repetition_rule schema rejects invalid scheduleType', () => {
|
|
28
|
+
assert.throws(() => schema.parse({
|
|
29
|
+
taskId: 'abc123',
|
|
30
|
+
scheduleType: 'Sometimes',
|
|
31
|
+
}));
|
|
32
|
+
});
|
|
33
|
+
test('set_repetition_rule schema rejects invalid anchorDateKey', () => {
|
|
34
|
+
assert.throws(() => schema.parse({
|
|
35
|
+
taskId: 'abc123',
|
|
36
|
+
anchorDateKey: 'StartDate',
|
|
37
|
+
}));
|
|
38
|
+
});
|
|
39
|
+
test('set_repetition_rule schema accepts clear flag', () => {
|
|
40
|
+
const parsed = schema.parse({ taskId: 'abc123', clear: true });
|
|
41
|
+
assert.equal(parsed.clear, true);
|
|
42
|
+
});
|