omnifocus-mcp-enhanced 1.7.0 → 1.9.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 +68 -1
- package/dist/server.js +21 -1
- package/dist/tools/definitions/addFolder.js +40 -0
- package/dist/tools/definitions/appendToNote.js +41 -0
- package/dist/tools/definitions/countTasks.js +41 -0
- package/dist/tools/definitions/duplicateTask.js +42 -0
- package/dist/tools/definitions/editFolder.js +39 -0
- package/dist/tools/definitions/getFolder.js +53 -0
- package/dist/tools/definitions/listFolders.js +18 -0
- package/dist/tools/definitions/removeFolder.js +56 -0
- package/dist/tools/primitives/addFolder.js +87 -0
- package/dist/tools/primitives/addFolder.test.js +27 -0
- package/dist/tools/primitives/appendToNote.js +122 -0
- package/dist/tools/primitives/appendToNote.test.js +45 -0
- package/dist/tools/primitives/countTasks.js +34 -0
- package/dist/tools/primitives/duplicateTask.js +27 -0
- package/dist/tools/primitives/duplicateTask.test.js +85 -0
- package/dist/tools/primitives/editFolder.js +196 -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/utils/omnifocusScripts/duplicateTask.js +76 -0
- package/dist/utils/omnifocusScripts/getFolder.js +107 -0
- package/dist/utils/omnifocusScripts/listFolders.js +62 -0
- package/package.json +1 -1
- package/src/server.ts +69 -1
- package/src/tools/definitions/addFolder.ts +45 -0
- package/src/tools/definitions/appendToNote.ts +46 -0
- package/src/tools/definitions/countTasks.ts +45 -0
- package/src/tools/definitions/duplicateTask.ts +47 -0
- package/src/tools/definitions/editFolder.ts +44 -0
- package/src/tools/definitions/getFolder.ts +59 -0
- package/src/tools/definitions/listFolders.ts +21 -0
- package/src/tools/definitions/removeFolder.ts +60 -0
- package/src/tools/primitives/addFolder.test.ts +35 -0
- package/src/tools/primitives/addFolder.ts +101 -0
- package/src/tools/primitives/appendToNote.test.ts +54 -0
- package/src/tools/primitives/appendToNote.ts +150 -0
- package/src/tools/primitives/countTasks.ts +53 -0
- package/src/tools/primitives/duplicateTask.test.ts +104 -0
- package/src/tools/primitives/duplicateTask.ts +51 -0
- package/src/tools/primitives/editFolder.ts +228 -0
- 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/utils/omnifocusScripts/duplicateTask.js +76 -0
- package/src/utils/omnifocusScripts/getFolder.js +107 -0
- package/src/utils/omnifocusScripts/listFolders.js +62 -0
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.9.0** - Added 3 productivity tools: `append_to_note` (append text to a task/project note without overwriting), `count_tasks` (fast "how many" aggregate queries with a status breakdown, using the same filters as `filter_tasks`), and `duplicate_task` (clone a task with or without its subtasks, optionally renamed).
|
|
48
|
+
- **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.
|
|
47
49
|
- **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.
|
|
48
50
|
- **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.
|
|
49
51
|
- **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.
|
|
@@ -65,6 +67,7 @@ Want to see where the project is heading next? See the [roadmap](docs/roadmap/20
|
|
|
65
67
|
- **🎯 Batch Operations** - Add/remove multiple tasks efficiently
|
|
66
68
|
- **📊 Smart Querying** - Find tasks by ID, name, or complex criteria
|
|
67
69
|
- **🔄 Full CRUD Operations** - Create, read, update, delete tasks and projects
|
|
70
|
+
- **📁 Folder Management** - Full CRUD for folders with nested hierarchy, move/rename, and content inspection
|
|
68
71
|
- **📅 Time Management** - Due, defer, planned dates, estimates, and scheduling
|
|
69
72
|
- **🏷️ Advanced Tagging** - Tag-based filtering with exact/partial matching
|
|
70
73
|
- **🚫 Mutually Exclusive Tags** - Automatically respects exclusive tag groups when applying tags
|
|
@@ -512,8 +515,20 @@ read_task_attachment {
|
|
|
512
515
|
16. **list_custom_perspectives** - 🌟 **NEW**: List all custom perspectives with details
|
|
513
516
|
17. **get_custom_perspective_tasks** - 🌟 **NEW**: Access custom perspective with hierarchical display
|
|
514
517
|
|
|
518
|
+
### 📁 Folder Management Tools (NEW)
|
|
519
|
+
18. **add_folder** - 🆕 **NEW**: Create a folder, optionally nested under a parent folder
|
|
520
|
+
19. **edit_folder** - 🆕 **NEW**: Rename a folder or move it under a different parent (empty string moves to root)
|
|
521
|
+
20. **remove_folder** - 🆕 **NEW**: Delete a folder (⚠️ also deletes all contained projects and tasks)
|
|
522
|
+
21. **list_folders** - 🆕 **NEW**: List all folders with IDs, parents, status, and project counts
|
|
523
|
+
22. **get_folder** - 🆕 **NEW**: Get a single folder with its child projects and subfolders
|
|
524
|
+
|
|
525
|
+
### ⚡ Productivity Tools (NEW)
|
|
526
|
+
23. **append_to_note** - 🆕 **NEW**: Append text to a task/project note without overwriting
|
|
527
|
+
24. **count_tasks** - 🆕 **NEW**: Fast "how many" queries with a status breakdown (same filters as filter_tasks)
|
|
528
|
+
25. **duplicate_task** - 🆕 **NEW**: Clone a task with/without subtasks, optionally renamed
|
|
529
|
+
|
|
515
530
|
### 📊 Analytics & Tracking
|
|
516
|
-
|
|
531
|
+
26. **get_today_completed_tasks** - View today's completed tasks
|
|
517
532
|
|
|
518
533
|
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)
|
|
519
534
|
|
|
@@ -623,6 +638,58 @@ get_custom_perspective_tasks {
|
|
|
623
638
|
}
|
|
624
639
|
```
|
|
625
640
|
|
|
641
|
+
### 📁 Folder Management
|
|
642
|
+
|
|
643
|
+
```bash
|
|
644
|
+
# List all folders with project counts
|
|
645
|
+
list_folders {"includeDropped": false}
|
|
646
|
+
|
|
647
|
+
# Create a top-level folder
|
|
648
|
+
add_folder {"name": "Work"}
|
|
649
|
+
|
|
650
|
+
# Create a nested folder
|
|
651
|
+
add_folder {"name": "Clients", "parentFolderName": "Work"}
|
|
652
|
+
|
|
653
|
+
# Inspect a folder's projects and subfolders
|
|
654
|
+
get_folder {"name": "Work"}
|
|
655
|
+
|
|
656
|
+
# Rename or move a folder (empty string moves to root)
|
|
657
|
+
edit_folder {"name": "Clients", "newName": "Key Clients"}
|
|
658
|
+
edit_folder {"name": "Key Clients", "newParentFolderName": ""}
|
|
659
|
+
|
|
660
|
+
# Delete a folder (⚠️ also deletes all contained projects and tasks)
|
|
661
|
+
remove_folder {"name": "Old Archive"}
|
|
662
|
+
```
|
|
663
|
+
|
|
664
|
+
### ⚡ Productivity Tools
|
|
665
|
+
|
|
666
|
+
```bash
|
|
667
|
+
# Append a progress note without overwriting the existing note
|
|
668
|
+
append_to_note {
|
|
669
|
+
"itemType": "task",
|
|
670
|
+
"name": "Write report",
|
|
671
|
+
"text": "Drafted section 1 today"
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
# Fast count: how many flagged tasks are still actionable?
|
|
675
|
+
count_tasks {
|
|
676
|
+
"flagged": true,
|
|
677
|
+
"taskStatus": ["Available", "Next", "DueSoon", "Overdue"]
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
# How many tasks remain in a project (by status breakdown)
|
|
681
|
+
count_tasks {"projectFilter": "Website Redesign"}
|
|
682
|
+
|
|
683
|
+
# Duplicate a task template with its subtasks
|
|
684
|
+
duplicate_task {
|
|
685
|
+
"name": "Weekly Review Checklist",
|
|
686
|
+
"newName": "Weekly Review - 2026-03-02"
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
# Duplicate without subtasks
|
|
690
|
+
duplicate_task {"taskId": "abc123", "includeSubtasks": false}
|
|
691
|
+
```
|
|
692
|
+
|
|
626
693
|
## 🔧 Configuration
|
|
627
694
|
|
|
628
695
|
### Claude Code
|
package/dist/server.js
CHANGED
|
@@ -25,10 +25,20 @@ import * as filterTasksTool from './tools/definitions/filterTasks.js';
|
|
|
25
25
|
// Import custom perspective tools
|
|
26
26
|
import * as listCustomPerspectivesTool from './tools/definitions/listCustomPerspectives.js';
|
|
27
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';
|
|
34
|
+
// Import productivity tools
|
|
35
|
+
import * as appendToNoteTool from './tools/definitions/appendToNote.js';
|
|
36
|
+
import * as countTasksTool from './tools/definitions/countTasks.js';
|
|
37
|
+
import * as duplicateTaskTool from './tools/definitions/duplicateTask.js';
|
|
28
38
|
// Create an MCP server
|
|
29
39
|
const server = new McpServer({
|
|
30
40
|
name: "OmniFocus MCP",
|
|
31
|
-
version: "1.
|
|
41
|
+
version: "1.9.0"
|
|
32
42
|
});
|
|
33
43
|
// Register tools
|
|
34
44
|
server.tool("dump_database", "Gets the current state of your OmniFocus database", dumpDatabaseTool.schema.shape, dumpDatabaseTool.handler);
|
|
@@ -54,6 +64,16 @@ server.tool("filter_tasks", "Advanced task filtering with unlimited perspective
|
|
|
54
64
|
// Custom perspective tools
|
|
55
65
|
server.tool("list_custom_perspectives", "List all custom perspectives defined in OmniFocus", listCustomPerspectivesTool.schema.shape, listCustomPerspectivesTool.handler);
|
|
56
66
|
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);
|
|
67
|
+
// Folder management tools
|
|
68
|
+
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);
|
|
69
|
+
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);
|
|
70
|
+
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);
|
|
71
|
+
server.tool("list_folders", "List all OmniFocus folders with IDs, parent relationships, status, and project counts without loading tasks.", listFoldersTool.schema.shape, listFoldersTool.handler);
|
|
72
|
+
server.tool("get_folder", "Get a single OmniFocus folder by ID or name, including its child projects and subfolders.", getFolderTool.schema.shape, getFolderTool.handler);
|
|
73
|
+
// Productivity tools
|
|
74
|
+
server.tool("append_to_note", "Append text to a task or project note without overwriting the existing note. Useful for logging progress or adding context.", appendToNoteTool.schema.shape, appendToNoteTool.handler);
|
|
75
|
+
server.tool("count_tasks", "Count tasks matching filters without returning the full list. Fast 'how many' queries that return a total plus a breakdown by status. Uses the same filters as filter_tasks.", countTasksTool.schema.shape, countTasksTool.handler);
|
|
76
|
+
server.tool("duplicate_task", "Duplicate an existing task, optionally with its subtasks, and optionally with a new name. Useful for template-based workflows.", duplicateTaskTool.schema.shape, duplicateTaskTool.handler);
|
|
57
77
|
// Start the MCP server
|
|
58
78
|
const transport = new StdioServerTransport();
|
|
59
79
|
// 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
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { appendToNote } from '../primitives/appendToNote.js';
|
|
3
|
+
export const schema = z.object({
|
|
4
|
+
id: z.string().optional().describe('The ID of the task or project'),
|
|
5
|
+
name: z.string().optional().describe('The name of the task or project (as fallback if ID not provided)'),
|
|
6
|
+
itemType: z.enum(['task', 'project']).describe("Type of item whose note to append to ('task' or 'project')"),
|
|
7
|
+
text: z.string().describe('The text to append to the existing note'),
|
|
8
|
+
separator: z.string().optional().describe('Separator inserted between the existing note and the new text (default: a newline). Pass an empty string to append with no separator.')
|
|
9
|
+
});
|
|
10
|
+
export async function handler(args, _extra) {
|
|
11
|
+
try {
|
|
12
|
+
if (!args.id && !args.name) {
|
|
13
|
+
return {
|
|
14
|
+
content: [{ type: 'text', text: 'Either id or name must be provided to append to a note.' }],
|
|
15
|
+
isError: true
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
const result = await appendToNote(args);
|
|
19
|
+
if (result.success) {
|
|
20
|
+
const itemTypeLabel = args.itemType === 'task' ? 'Task' : 'Project';
|
|
21
|
+
return {
|
|
22
|
+
content: [{
|
|
23
|
+
type: 'text',
|
|
24
|
+
text: `✅ Appended text to ${itemTypeLabel} "${result.name}" note.\n\nid: ${result.id}`
|
|
25
|
+
}]
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
return {
|
|
29
|
+
content: [{ type: 'text', text: `Failed to append to note: ${result.error}` }],
|
|
30
|
+
isError: true
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
catch (err) {
|
|
34
|
+
const error = err;
|
|
35
|
+
console.error(`Tool execution error: ${error.message}`);
|
|
36
|
+
return {
|
|
37
|
+
content: [{ type: 'text', text: `Error appending to note: ${error.message}` }],
|
|
38
|
+
isError: true
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { countTasks } from '../primitives/countTasks.js';
|
|
3
|
+
export const schema = z.object({
|
|
4
|
+
taskStatus: z.array(z.string()).optional().describe('Filter by task status: Available, Next, Blocked, DueSoon, Overdue, Completed, Dropped'),
|
|
5
|
+
perspective: z.enum(['inbox', 'flagged', 'all']).optional().describe("Scope: 'inbox', 'flagged', or 'all' (default: all)"),
|
|
6
|
+
projectFilter: z.string().optional().describe('Only count tasks in projects whose name contains this text'),
|
|
7
|
+
tagFilter: z.union([z.string(), z.array(z.string())]).optional().describe('Only count tasks with these tags'),
|
|
8
|
+
exactTagMatch: z.boolean().optional().describe('Require exact tag name match (default: false)'),
|
|
9
|
+
flagged: z.boolean().optional().describe('Only count flagged (true) or unflagged (false) tasks'),
|
|
10
|
+
searchText: z.string().optional().describe('Only count tasks whose name or note contains this text'),
|
|
11
|
+
dueToday: z.boolean().optional().describe('Only count tasks due today'),
|
|
12
|
+
dueThisWeek: z.boolean().optional().describe('Only count tasks due this week'),
|
|
13
|
+
overdue: z.boolean().optional().describe('Only count overdue tasks'),
|
|
14
|
+
completedToday: z.boolean().optional().describe('Only count tasks completed today'),
|
|
15
|
+
completedThisWeek: z.boolean().optional().describe('Only count tasks completed this week'),
|
|
16
|
+
plannedToday: z.boolean().optional().describe('Only count tasks planned for today'),
|
|
17
|
+
plannedThisWeek: z.boolean().optional().describe('Only count tasks planned for this week')
|
|
18
|
+
});
|
|
19
|
+
export async function handler(args, _extra) {
|
|
20
|
+
try {
|
|
21
|
+
const result = await countTasks(args);
|
|
22
|
+
const statusEntries = Object.entries(result.byStatus).sort((a, b) => b[1] - a[1]);
|
|
23
|
+
const breakdown = statusEntries.length > 0
|
|
24
|
+
? statusEntries.map(([status, count]) => `- ${status}: ${count}`).join('\n')
|
|
25
|
+
: '- (no matching tasks)';
|
|
26
|
+
return {
|
|
27
|
+
content: [{
|
|
28
|
+
type: 'text',
|
|
29
|
+
text: `# Task Count\n\n**Total: ${result.total}**\n\nBy status:\n${breakdown}`
|
|
30
|
+
}]
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
catch (err) {
|
|
34
|
+
const error = err;
|
|
35
|
+
console.error(`Tool execution error: ${error.message}`);
|
|
36
|
+
return {
|
|
37
|
+
content: [{ type: 'text', text: `Error counting tasks: ${error.message}` }],
|
|
38
|
+
isError: true
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { duplicateTask } from '../primitives/duplicateTask.js';
|
|
3
|
+
export const schema = z.object({
|
|
4
|
+
taskId: z.string().optional().describe('The ID of the task to duplicate'),
|
|
5
|
+
taskName: z.string().optional().describe('The name of the task to duplicate (as fallback if ID not provided)'),
|
|
6
|
+
newName: z.string().optional().describe('Optional new name for the duplicated task (keeps the original name if omitted)'),
|
|
7
|
+
includeSubtasks: z.boolean().optional().describe('Whether to include the task\'s subtasks in the copy (default: true)')
|
|
8
|
+
});
|
|
9
|
+
export async function handler(args, _extra) {
|
|
10
|
+
try {
|
|
11
|
+
if (!args.taskId && !args.taskName) {
|
|
12
|
+
return {
|
|
13
|
+
content: [{ type: 'text', text: 'Either taskId or taskName must be provided to duplicate a task.' }],
|
|
14
|
+
isError: true
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
const result = await duplicateTask(args);
|
|
18
|
+
if (result.success) {
|
|
19
|
+
const subtaskText = (result.childrenCount && result.childrenCount > 0)
|
|
20
|
+
? ` with ${result.childrenCount} subtask(s)`
|
|
21
|
+
: '';
|
|
22
|
+
return {
|
|
23
|
+
content: [{
|
|
24
|
+
type: 'text',
|
|
25
|
+
text: `✅ Duplicated task as "${result.name}"${subtaskText}.\n\nid: ${result.newTaskId}`
|
|
26
|
+
}]
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
return {
|
|
30
|
+
content: [{ type: 'text', text: `Failed to duplicate task: ${result.error}` }],
|
|
31
|
+
isError: true
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
catch (err) {
|
|
35
|
+
const error = err;
|
|
36
|
+
console.error(`Tool execution error: ${error.message}`);
|
|
37
|
+
return {
|
|
38
|
+
content: [{ type: 'text', text: `Error duplicating task: ${error.message}` }],
|
|
39
|
+
isError: true
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
}
|
|
@@ -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
|
+
}
|
|
@@ -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
|
+
}
|
|
@@ -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,87 @@
|
|
|
1
|
+
import { executeAppleScript } from '../../utils/scriptExecution.js';
|
|
2
|
+
import { buildAppleScriptJsonHelpers } from '../../utils/appleScriptJson.js';
|
|
3
|
+
import { escapeAppleScriptString } from '../../utils/appleScriptString.js';
|
|
4
|
+
/**
|
|
5
|
+
* Generate pure AppleScript for folder creation
|
|
6
|
+
*/
|
|
7
|
+
export function generateAppleScript(params) {
|
|
8
|
+
const name = escapeAppleScriptString(params.name);
|
|
9
|
+
const parentFolderName = params.parentFolderName ? escapeAppleScriptString(params.parentFolderName) : '';
|
|
10
|
+
const jsonHelpers = buildAppleScriptJsonHelpers();
|
|
11
|
+
const script = `
|
|
12
|
+
${jsonHelpers}
|
|
13
|
+
try
|
|
14
|
+
tell application "OmniFocus"
|
|
15
|
+
tell front document
|
|
16
|
+
-- Determine the container (root or parent folder)
|
|
17
|
+
if "${parentFolderName}" is "" then
|
|
18
|
+
-- Create folder at the root level
|
|
19
|
+
set newFolder to make new folder with properties {name:"${name}"}
|
|
20
|
+
else
|
|
21
|
+
-- Resolve parent folder with duplicate protection
|
|
22
|
+
set parentMatches to (flattened folders where name = "${parentFolderName}")
|
|
23
|
+
set parentMatchCount to count of parentMatches
|
|
24
|
+
|
|
25
|
+
if parentMatchCount = 0 then
|
|
26
|
+
return "{\\\"success\\\":false,\\\"error\\\":\\"" & my jsonEscape("Parent folder not found: ${parentFolderName}") & "\\\"}"
|
|
27
|
+
else if parentMatchCount > 1 then
|
|
28
|
+
return "{\\\"success\\\":false,\\\"error\\\":\\"" & my jsonEscape("Ambiguous parent folder name: ${parentFolderName}. Multiple matches found; please rename or use a unique folder.") & "\\\"}"
|
|
29
|
+
end if
|
|
30
|
+
|
|
31
|
+
set parentFolder to item 1 of parentMatches
|
|
32
|
+
set newFolder to make new folder with properties {name:"${name}"} at end of folders of parentFolder
|
|
33
|
+
end if
|
|
34
|
+
|
|
35
|
+
-- Get the folder ID and name
|
|
36
|
+
set folderId to id of newFolder as string
|
|
37
|
+
set folderNameValue to name of newFolder
|
|
38
|
+
|
|
39
|
+
-- Return success with folder ID
|
|
40
|
+
return "{\\\"success\\\":true,\\\"folderId\\\":\\"" & my jsonEscape(folderId) & "\\",\\\"name\\\":\\"" & my jsonEscape(folderNameValue) & "\\\"}"
|
|
41
|
+
end tell
|
|
42
|
+
end tell
|
|
43
|
+
on error errorMessage
|
|
44
|
+
return "{\\\"success\\\":false,\\\"error\\\":\\"" & my jsonEscape(errorMessage) & "\\\"}"
|
|
45
|
+
end try
|
|
46
|
+
`;
|
|
47
|
+
return script;
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Add a folder to OmniFocus
|
|
51
|
+
*/
|
|
52
|
+
export async function addFolder(params) {
|
|
53
|
+
try {
|
|
54
|
+
const script = generateAppleScript(params);
|
|
55
|
+
console.error('Executing AppleScript for folder creation...');
|
|
56
|
+
const stdout = await executeAppleScript(script);
|
|
57
|
+
console.error('AppleScript stdout:', stdout);
|
|
58
|
+
try {
|
|
59
|
+
const result = JSON.parse(stdout);
|
|
60
|
+
if (!result.success) {
|
|
61
|
+
return {
|
|
62
|
+
success: false,
|
|
63
|
+
error: result.error
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
return {
|
|
67
|
+
success: true,
|
|
68
|
+
folderId: result.folderId,
|
|
69
|
+
name: result.name
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
catch (parseError) {
|
|
73
|
+
console.error('Error parsing AppleScript result:', parseError);
|
|
74
|
+
return {
|
|
75
|
+
success: false,
|
|
76
|
+
error: `Failed to parse result: ${stdout}`
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
catch (error) {
|
|
81
|
+
console.error('Error in addFolder:', error);
|
|
82
|
+
return {
|
|
83
|
+
success: false,
|
|
84
|
+
error: error?.message || 'Unknown error in addFolder'
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import assert from 'node:assert/strict';
|
|
2
|
+
import test from 'node:test';
|
|
3
|
+
import { generateAppleScript } from './addFolder.js';
|
|
4
|
+
test('addFolder creates folder at root level when no parent specified', () => {
|
|
5
|
+
const script = generateAppleScript({ name: 'Work' });
|
|
6
|
+
// Root branch is selected at AppleScript runtime via the empty parent name check.
|
|
7
|
+
assert.match(script, /if "" is "" then/);
|
|
8
|
+
assert.match(script, /make new folder with properties \{name:"Work"\}/);
|
|
9
|
+
assert.match(script, /my jsonEscape\(folderId\)/);
|
|
10
|
+
});
|
|
11
|
+
test('addFolder nests under a parent folder with duplicate protection', () => {
|
|
12
|
+
const script = generateAppleScript({ name: 'Sub', parentFolderName: 'Parent' });
|
|
13
|
+
assert.match(script, /flattened folders where name = "Parent"/);
|
|
14
|
+
assert.match(script, /Ambiguous parent folder name: Parent/);
|
|
15
|
+
assert.match(script, /make new folder with properties \{name:"Sub"\} at end of folders of parentFolder/);
|
|
16
|
+
});
|
|
17
|
+
test('addFolder keeps apostrophes and doubles backslashes in the name', () => {
|
|
18
|
+
const script = generateAppleScript({ name: "Client's \\ Files" });
|
|
19
|
+
assert.match(script, /name:"Client's \\\\ Files"/);
|
|
20
|
+
assert.doesNotMatch(script, /\\'/);
|
|
21
|
+
});
|
|
22
|
+
test('addFolder escapes JSON response values through AppleScript helper', () => {
|
|
23
|
+
const script = generateAppleScript({ name: 'Work' });
|
|
24
|
+
assert.match(script, /on jsonEscape\(inputText\)/);
|
|
25
|
+
assert.match(script, /my jsonEscape\(folderId\)/);
|
|
26
|
+
assert.match(script, /my jsonEscape\(folderNameValue\)/);
|
|
27
|
+
});
|