omnifocus-mcp-enhanced 1.1.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/.claude/settings.local.json +18 -0
- package/CLAUDE.md +280 -0
- package/README.md +168 -0
- package/assets/omnifocus-mcp-logo.png +0 -0
- package/cli.cjs +9 -0
- package/dist/omnifocustypes.js +48 -0
- package/dist/server.js +42 -0
- package/dist/tools/definitions/addOmniFocusTask.js +67 -0
- package/dist/tools/definitions/addProject.js +61 -0
- package/dist/tools/definitions/addSubtask.js +66 -0
- package/dist/tools/definitions/batchAddItems.js +75 -0
- package/dist/tools/definitions/batchRemoveItems.js +74 -0
- package/dist/tools/definitions/dumpDatabase.js +249 -0
- package/dist/tools/definitions/editItem.js +88 -0
- package/dist/tools/definitions/getTaskById.js +66 -0
- package/dist/tools/definitions/removeItem.js +80 -0
- package/dist/tools/dumpDatabase.js +121 -0
- package/dist/tools/primitives/addOmniFocusTask.js +157 -0
- package/dist/tools/primitives/addProject.js +113 -0
- package/dist/tools/primitives/addSubtask.js +37 -0
- package/dist/tools/primitives/batchAddItems.js +87 -0
- package/dist/tools/primitives/batchRemoveItems.js +44 -0
- package/dist/tools/primitives/editItem.js +328 -0
- package/dist/tools/primitives/getTaskById.js +115 -0
- package/dist/tools/primitives/removeItem.js +124 -0
- package/dist/types.js +1 -0
- package/dist/utils/omnifocusScripts/omnifocusDump.js +223 -0
- package/dist/utils/scriptExecution.js +113 -0
- package/package.json +51 -0
- package/publish.sh +148 -0
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { addSubtask } from '../primitives/addSubtask.js';
|
|
3
|
+
export const schema = z.object({
|
|
4
|
+
name: z.string().describe("The name of the subtask"),
|
|
5
|
+
parentTaskId: z.string().optional().describe("The ID of the parent task"),
|
|
6
|
+
parentTaskName: z.string().optional().describe("The name of the parent task (alternative to parentTaskId)"),
|
|
7
|
+
note: z.string().optional().describe("Additional notes for the subtask"),
|
|
8
|
+
dueDate: z.string().optional().describe("The due date of the subtask in ISO format (YYYY-MM-DD or full ISO date)"),
|
|
9
|
+
deferDate: z.string().optional().describe("The defer date of the subtask in ISO format (YYYY-MM-DD or full ISO date)"),
|
|
10
|
+
flagged: z.boolean().optional().describe("Whether the subtask is flagged or not"),
|
|
11
|
+
estimatedMinutes: z.number().optional().describe("Estimated time to complete the subtask, in minutes"),
|
|
12
|
+
tags: z.array(z.string()).optional().describe("Tags to assign to the subtask")
|
|
13
|
+
});
|
|
14
|
+
export async function handler(args, extra) {
|
|
15
|
+
try {
|
|
16
|
+
// Validate that either parentTaskId or parentTaskName is provided
|
|
17
|
+
if (!args.parentTaskId && !args.parentTaskName) {
|
|
18
|
+
return {
|
|
19
|
+
content: [{
|
|
20
|
+
type: "text",
|
|
21
|
+
text: "Error: Either parentTaskId or parentTaskName must be provided for subtask creation."
|
|
22
|
+
}],
|
|
23
|
+
isError: true
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
// Call the addSubtask function
|
|
27
|
+
const result = await addSubtask(args);
|
|
28
|
+
if (result.success) {
|
|
29
|
+
// Subtask was added successfully
|
|
30
|
+
const parentRef = args.parentTaskId || args.parentTaskName;
|
|
31
|
+
let tagText = args.tags && args.tags.length > 0
|
|
32
|
+
? ` with tags: ${args.tags.join(', ')}`
|
|
33
|
+
: "";
|
|
34
|
+
let dueDateText = args.dueDate
|
|
35
|
+
? ` due on ${new Date(args.dueDate).toLocaleDateString()}`
|
|
36
|
+
: "";
|
|
37
|
+
return {
|
|
38
|
+
content: [{
|
|
39
|
+
type: "text",
|
|
40
|
+
text: `✅ Subtask "${args.name}" created successfully under parent task "${parentRef}"${dueDateText}${tagText}.`
|
|
41
|
+
}]
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
else {
|
|
45
|
+
// Subtask creation failed
|
|
46
|
+
return {
|
|
47
|
+
content: [{
|
|
48
|
+
type: "text",
|
|
49
|
+
text: `Failed to create subtask: ${result.error}`
|
|
50
|
+
}],
|
|
51
|
+
isError: true
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
catch (err) {
|
|
56
|
+
const error = err;
|
|
57
|
+
console.error(`Tool execution error: ${error.message}`);
|
|
58
|
+
return {
|
|
59
|
+
content: [{
|
|
60
|
+
type: "text",
|
|
61
|
+
text: `Error creating subtask: ${error.message}`
|
|
62
|
+
}],
|
|
63
|
+
isError: true
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { batchAddItems } from '../primitives/batchAddItems.js';
|
|
3
|
+
export const schema = z.object({
|
|
4
|
+
items: z.array(z.object({
|
|
5
|
+
type: z.enum(['task', 'project']).describe("Type of item to add ('task' or 'project')"),
|
|
6
|
+
name: z.string().describe("The name of the item"),
|
|
7
|
+
note: z.string().optional().describe("Additional notes for the item"),
|
|
8
|
+
dueDate: z.string().optional().describe("The due date in ISO format (YYYY-MM-DD or full ISO date)"),
|
|
9
|
+
deferDate: z.string().optional().describe("The defer date in ISO format (YYYY-MM-DD or full ISO date)"),
|
|
10
|
+
flagged: z.boolean().optional().describe("Whether the item is flagged or not"),
|
|
11
|
+
estimatedMinutes: z.number().optional().describe("Estimated time to complete the item, in minutes"),
|
|
12
|
+
tags: z.array(z.string()).optional().describe("Tags to assign to the item"),
|
|
13
|
+
// Task-specific properties
|
|
14
|
+
projectName: z.string().optional().describe("For tasks: The name of the project to add the task to"),
|
|
15
|
+
parentTaskId: z.string().optional().describe("For tasks: The ID of the parent task to create this task as a subtask"),
|
|
16
|
+
parentTaskName: z.string().optional().describe("For tasks: The name of the parent task to create this task as a subtask"),
|
|
17
|
+
// Project-specific properties
|
|
18
|
+
folderName: z.string().optional().describe("For projects: The name of the folder to add the project to"),
|
|
19
|
+
sequential: z.boolean().optional().describe("For projects: Whether tasks in the project should be sequential")
|
|
20
|
+
})).describe("Array of items (tasks or projects) to add")
|
|
21
|
+
});
|
|
22
|
+
export async function handler(args, extra) {
|
|
23
|
+
try {
|
|
24
|
+
// Call the batchAddItems function
|
|
25
|
+
const result = await batchAddItems(args.items);
|
|
26
|
+
if (result.success) {
|
|
27
|
+
const successCount = result.results.filter(r => r.success).length;
|
|
28
|
+
const failureCount = result.results.filter(r => !r.success).length;
|
|
29
|
+
let message = `✅ Successfully added ${successCount} items.`;
|
|
30
|
+
if (failureCount > 0) {
|
|
31
|
+
message += ` ⚠️ Failed to add ${failureCount} items.`;
|
|
32
|
+
}
|
|
33
|
+
// Include details about added items
|
|
34
|
+
const details = result.results.map((item, index) => {
|
|
35
|
+
if (item.success) {
|
|
36
|
+
const itemType = args.items[index].type;
|
|
37
|
+
const itemName = args.items[index].name;
|
|
38
|
+
return `- ✅ ${itemType}: "${itemName}"`;
|
|
39
|
+
}
|
|
40
|
+
else {
|
|
41
|
+
const itemType = args.items[index].type;
|
|
42
|
+
const itemName = args.items[index].name;
|
|
43
|
+
return `- ❌ ${itemType}: "${itemName}" - Error: ${item.error}`;
|
|
44
|
+
}
|
|
45
|
+
}).join('\n');
|
|
46
|
+
return {
|
|
47
|
+
content: [{
|
|
48
|
+
type: "text",
|
|
49
|
+
text: `${message}\n\n${details}`
|
|
50
|
+
}]
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
else {
|
|
54
|
+
// Batch operation failed completely
|
|
55
|
+
return {
|
|
56
|
+
content: [{
|
|
57
|
+
type: "text",
|
|
58
|
+
text: `Failed to process batch operation: ${result.error}`
|
|
59
|
+
}],
|
|
60
|
+
isError: true
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
catch (err) {
|
|
65
|
+
const error = err;
|
|
66
|
+
console.error(`Tool execution error: ${error.message}`);
|
|
67
|
+
return {
|
|
68
|
+
content: [{
|
|
69
|
+
type: "text",
|
|
70
|
+
text: `Error processing batch operation: ${error.message}`
|
|
71
|
+
}],
|
|
72
|
+
isError: true
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { batchRemoveItems } from '../primitives/batchRemoveItems.js';
|
|
3
|
+
export const schema = z.object({
|
|
4
|
+
items: z.array(z.object({
|
|
5
|
+
id: z.string().optional().describe("The ID of the task or project to remove"),
|
|
6
|
+
name: z.string().optional().describe("The name of the task or project to remove (as fallback if ID not provided)"),
|
|
7
|
+
itemType: z.enum(['task', 'project']).describe("Type of item to remove ('task' or 'project')")
|
|
8
|
+
})).describe("Array of items (tasks or projects) to remove")
|
|
9
|
+
});
|
|
10
|
+
export async function handler(args, extra) {
|
|
11
|
+
try {
|
|
12
|
+
// Validate that each item has at least an ID or name
|
|
13
|
+
for (const item of args.items) {
|
|
14
|
+
if (!item.id && !item.name) {
|
|
15
|
+
return {
|
|
16
|
+
content: [{
|
|
17
|
+
type: "text",
|
|
18
|
+
text: "Each item must have either id or name provided to remove it."
|
|
19
|
+
}],
|
|
20
|
+
isError: true
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
// Call the batchRemoveItems function
|
|
25
|
+
const result = await batchRemoveItems(args.items);
|
|
26
|
+
if (result.success) {
|
|
27
|
+
const successCount = result.results.filter(r => r.success).length;
|
|
28
|
+
const failureCount = result.results.filter(r => !r.success).length;
|
|
29
|
+
let message = `✅ Successfully removed ${successCount} items.`;
|
|
30
|
+
if (failureCount > 0) {
|
|
31
|
+
message += ` ⚠️ Failed to remove ${failureCount} items.`;
|
|
32
|
+
}
|
|
33
|
+
// Include details about removed items
|
|
34
|
+
const details = result.results.map((item, index) => {
|
|
35
|
+
if (item.success) {
|
|
36
|
+
const itemType = args.items[index].itemType;
|
|
37
|
+
return `- ✅ ${itemType}: "${item.name}"`;
|
|
38
|
+
}
|
|
39
|
+
else {
|
|
40
|
+
const itemType = args.items[index].itemType;
|
|
41
|
+
const identifier = args.items[index].id || args.items[index].name;
|
|
42
|
+
return `- ❌ ${itemType}: ${identifier} - Error: ${item.error}`;
|
|
43
|
+
}
|
|
44
|
+
}).join('\n');
|
|
45
|
+
return {
|
|
46
|
+
content: [{
|
|
47
|
+
type: "text",
|
|
48
|
+
text: `${message}\n\n${details}`
|
|
49
|
+
}]
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
else {
|
|
53
|
+
// Batch operation failed completely
|
|
54
|
+
return {
|
|
55
|
+
content: [{
|
|
56
|
+
type: "text",
|
|
57
|
+
text: `Failed to process batch removal: ${result.error}`
|
|
58
|
+
}],
|
|
59
|
+
isError: true
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
catch (err) {
|
|
64
|
+
const error = err;
|
|
65
|
+
console.error(`Tool execution error: ${error.message}`);
|
|
66
|
+
return {
|
|
67
|
+
content: [{
|
|
68
|
+
type: "text",
|
|
69
|
+
text: `Error processing batch removal: ${error.message}`
|
|
70
|
+
}],
|
|
71
|
+
isError: true
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
}
|
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { dumpDatabase } from '../dumpDatabase.js';
|
|
3
|
+
export const schema = z.object({
|
|
4
|
+
hideCompleted: z.boolean().optional().describe("Set to false to show completed and dropped tasks (default: true)"),
|
|
5
|
+
hideRecurringDuplicates: z.boolean().optional().describe("Set to true to hide duplicate instances of recurring tasks (default: true)")
|
|
6
|
+
});
|
|
7
|
+
export async function handler(args, extra) {
|
|
8
|
+
try {
|
|
9
|
+
// Get raw database
|
|
10
|
+
const database = await dumpDatabase();
|
|
11
|
+
// Format as compact report
|
|
12
|
+
const formattedReport = formatCompactReport(database, {
|
|
13
|
+
hideCompleted: args.hideCompleted !== false, // Default to true
|
|
14
|
+
hideRecurringDuplicates: args.hideRecurringDuplicates !== false // Default to true
|
|
15
|
+
});
|
|
16
|
+
return {
|
|
17
|
+
content: [{
|
|
18
|
+
type: "text",
|
|
19
|
+
text: formattedReport
|
|
20
|
+
}]
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
catch (err) {
|
|
24
|
+
return {
|
|
25
|
+
content: [{
|
|
26
|
+
type: "text",
|
|
27
|
+
text: `Error generating report. Please ensure OmniFocus is running and try again.`
|
|
28
|
+
}],
|
|
29
|
+
isError: true
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
// Function to format date in compact format (M/D)
|
|
34
|
+
function formatCompactDate(isoDate) {
|
|
35
|
+
if (!isoDate)
|
|
36
|
+
return '';
|
|
37
|
+
const date = new Date(isoDate);
|
|
38
|
+
return `${date.getMonth() + 1}/${date.getDate()}`;
|
|
39
|
+
}
|
|
40
|
+
// Function to format the database in the compact report format
|
|
41
|
+
function formatCompactReport(database, options) {
|
|
42
|
+
const { hideCompleted, hideRecurringDuplicates } = options;
|
|
43
|
+
// Get current date for the header
|
|
44
|
+
const today = new Date();
|
|
45
|
+
const dateStr = today.toISOString().split('T')[0];
|
|
46
|
+
let output = `# OMNIFOCUS [${dateStr}]\n\n`;
|
|
47
|
+
// Add legend
|
|
48
|
+
output += `FORMAT LEGEND:
|
|
49
|
+
F: Folder | P: Project | •: Task | 🚩: Flagged
|
|
50
|
+
Dates: [M/D] | Duration: (30m) or (2h) | Tags: <tag1,tag2>
|
|
51
|
+
Status: #next #avail #block #due #over #compl #drop\n\n`;
|
|
52
|
+
// Map of folder IDs to folder objects for quick lookup
|
|
53
|
+
const folderMap = new Map();
|
|
54
|
+
Object.values(database.folders).forEach((folder) => {
|
|
55
|
+
folderMap.set(folder.id, folder);
|
|
56
|
+
});
|
|
57
|
+
// Get all tag names to compute minimum unique prefixes
|
|
58
|
+
const allTagNames = Object.values(database.tags).map((tag) => tag.name);
|
|
59
|
+
const tagPrefixMap = computeMinimumUniquePrefixes(allTagNames);
|
|
60
|
+
// Function to get folder hierarchy path
|
|
61
|
+
function getFolderPath(folderId) {
|
|
62
|
+
const path = [];
|
|
63
|
+
let currentId = folderId;
|
|
64
|
+
while (currentId) {
|
|
65
|
+
const folder = folderMap.get(currentId);
|
|
66
|
+
if (!folder)
|
|
67
|
+
break;
|
|
68
|
+
path.unshift(folder.name);
|
|
69
|
+
currentId = folder.parentFolderID;
|
|
70
|
+
}
|
|
71
|
+
return path;
|
|
72
|
+
}
|
|
73
|
+
// Get root folders (no parent)
|
|
74
|
+
const rootFolders = Object.values(database.folders).filter((folder) => !folder.parentFolderID);
|
|
75
|
+
// Process folders recursively
|
|
76
|
+
function processFolder(folder, level) {
|
|
77
|
+
const indent = ' '.repeat(level);
|
|
78
|
+
let folderOutput = `${indent}F: ${folder.name}\n`;
|
|
79
|
+
// Process subfolders
|
|
80
|
+
if (folder.subfolders && folder.subfolders.length > 0) {
|
|
81
|
+
for (const subfolderId of folder.subfolders) {
|
|
82
|
+
const subfolder = database.folders[subfolderId];
|
|
83
|
+
if (subfolder) {
|
|
84
|
+
folderOutput += `${processFolder(subfolder, level + 1)}`;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
// Process projects in this folder
|
|
89
|
+
if (folder.projects && folder.projects.length > 0) {
|
|
90
|
+
for (const projectId of folder.projects) {
|
|
91
|
+
const project = database.projects[projectId];
|
|
92
|
+
if (project) {
|
|
93
|
+
folderOutput += processProject(project, level + 1);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
return folderOutput;
|
|
98
|
+
}
|
|
99
|
+
// Process a project
|
|
100
|
+
function processProject(project, level) {
|
|
101
|
+
const indent = ' '.repeat(level);
|
|
102
|
+
// Skip if it's completed or dropped and we're hiding completed items
|
|
103
|
+
if (hideCompleted && (project.status === 'Done' || project.status === 'Dropped')) {
|
|
104
|
+
return '';
|
|
105
|
+
}
|
|
106
|
+
// Format project status info
|
|
107
|
+
let statusInfo = '';
|
|
108
|
+
if (project.status === 'OnHold') {
|
|
109
|
+
statusInfo = ' [OnHold]';
|
|
110
|
+
}
|
|
111
|
+
else if (project.status === 'Dropped') {
|
|
112
|
+
statusInfo = ' [Dropped]';
|
|
113
|
+
}
|
|
114
|
+
// Add due date if present
|
|
115
|
+
if (project.dueDate) {
|
|
116
|
+
const dueDateStr = formatCompactDate(project.dueDate);
|
|
117
|
+
statusInfo += statusInfo ? ` [DUE:${dueDateStr}]` : ` [DUE:${dueDateStr}]`;
|
|
118
|
+
}
|
|
119
|
+
// Add flag if present
|
|
120
|
+
const flaggedSymbol = project.flagged ? ' 🚩' : '';
|
|
121
|
+
let projectOutput = `${indent}P: ${project.name}${flaggedSymbol}${statusInfo}\n`;
|
|
122
|
+
// Process tasks in this project
|
|
123
|
+
const projectTasks = database.tasks.filter((task) => task.projectId === project.id && !task.parentId);
|
|
124
|
+
if (projectTasks.length > 0) {
|
|
125
|
+
for (const task of projectTasks) {
|
|
126
|
+
projectOutput += processTask(task, level + 1);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
return projectOutput;
|
|
130
|
+
}
|
|
131
|
+
// Process a task
|
|
132
|
+
function processTask(task, level) {
|
|
133
|
+
const indent = ' '.repeat(level);
|
|
134
|
+
// Skip if it's completed or dropped and we're hiding completed items
|
|
135
|
+
if (hideCompleted && (task.completed || task.taskStatus === 'Completed' || task.taskStatus === 'Dropped')) {
|
|
136
|
+
return '';
|
|
137
|
+
}
|
|
138
|
+
// Flag symbol
|
|
139
|
+
const flagSymbol = task.flagged ? '🚩 ' : '';
|
|
140
|
+
// Format dates
|
|
141
|
+
let dateInfo = '';
|
|
142
|
+
if (task.dueDate) {
|
|
143
|
+
const dueDateStr = formatCompactDate(task.dueDate);
|
|
144
|
+
dateInfo += ` [DUE:${dueDateStr}]`;
|
|
145
|
+
}
|
|
146
|
+
if (task.deferDate) {
|
|
147
|
+
const deferDateStr = formatCompactDate(task.deferDate);
|
|
148
|
+
dateInfo += ` [defer:${deferDateStr}]`;
|
|
149
|
+
}
|
|
150
|
+
// Format duration
|
|
151
|
+
let durationStr = '';
|
|
152
|
+
if (task.estimatedMinutes) {
|
|
153
|
+
// Convert to hours if >= 60 minutes
|
|
154
|
+
if (task.estimatedMinutes >= 60) {
|
|
155
|
+
const hours = Math.floor(task.estimatedMinutes / 60);
|
|
156
|
+
durationStr = ` (${hours}h)`;
|
|
157
|
+
}
|
|
158
|
+
else {
|
|
159
|
+
durationStr = ` (${task.estimatedMinutes}m)`;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
// Format tags
|
|
163
|
+
let tagsStr = '';
|
|
164
|
+
if (task.tagNames && task.tagNames.length > 0) {
|
|
165
|
+
// Use minimum unique prefixes for tag names
|
|
166
|
+
const abbreviatedTags = task.tagNames.map((tag) => {
|
|
167
|
+
return tagPrefixMap.get(tag) || tag;
|
|
168
|
+
});
|
|
169
|
+
tagsStr = ` <${abbreviatedTags.join(',')}>`;
|
|
170
|
+
}
|
|
171
|
+
// Format status
|
|
172
|
+
let statusStr = '';
|
|
173
|
+
switch (task.taskStatus) {
|
|
174
|
+
case 'Next':
|
|
175
|
+
statusStr = ' #next';
|
|
176
|
+
break;
|
|
177
|
+
case 'Available':
|
|
178
|
+
statusStr = ' #avail';
|
|
179
|
+
break;
|
|
180
|
+
case 'Blocked':
|
|
181
|
+
statusStr = ' #block';
|
|
182
|
+
break;
|
|
183
|
+
case 'DueSoon':
|
|
184
|
+
statusStr = ' #due';
|
|
185
|
+
break;
|
|
186
|
+
case 'Overdue':
|
|
187
|
+
statusStr = ' #over';
|
|
188
|
+
break;
|
|
189
|
+
case 'Completed':
|
|
190
|
+
statusStr = ' #compl';
|
|
191
|
+
break;
|
|
192
|
+
case 'Dropped':
|
|
193
|
+
statusStr = ' #drop';
|
|
194
|
+
break;
|
|
195
|
+
}
|
|
196
|
+
let taskOutput = `${indent}• ${flagSymbol}${task.name}${dateInfo}${durationStr}${tagsStr}${statusStr}\n`;
|
|
197
|
+
// Process subtasks
|
|
198
|
+
if (task.childIds && task.childIds.length > 0) {
|
|
199
|
+
const childTasks = database.tasks.filter((t) => task.childIds.includes(t.id));
|
|
200
|
+
for (const childTask of childTasks) {
|
|
201
|
+
taskOutput += processTask(childTask, level + 1);
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
return taskOutput;
|
|
205
|
+
}
|
|
206
|
+
// Process all root folders
|
|
207
|
+
for (const folder of rootFolders) {
|
|
208
|
+
output += processFolder(folder, 0);
|
|
209
|
+
}
|
|
210
|
+
// Process projects not in any folder (if any)
|
|
211
|
+
const rootProjects = Object.values(database.projects).filter((project) => !project.folderID);
|
|
212
|
+
for (const project of rootProjects) {
|
|
213
|
+
output += processProject(project, 0);
|
|
214
|
+
}
|
|
215
|
+
return output;
|
|
216
|
+
}
|
|
217
|
+
// Compute minimum unique prefixes for all tags (minimum 3 characters)
|
|
218
|
+
function computeMinimumUniquePrefixes(tagNames) {
|
|
219
|
+
const prefixMap = new Map();
|
|
220
|
+
// For each tag name
|
|
221
|
+
for (const tagName of tagNames) {
|
|
222
|
+
// Start with minimum length of 3
|
|
223
|
+
let prefixLength = 3;
|
|
224
|
+
let isUnique = false;
|
|
225
|
+
// Keep increasing prefix length until we find a unique prefix
|
|
226
|
+
while (!isUnique && prefixLength <= tagName.length) {
|
|
227
|
+
const prefix = tagName.substring(0, prefixLength);
|
|
228
|
+
// Check if this prefix uniquely identifies the tag
|
|
229
|
+
isUnique = tagNames.every(otherTag => {
|
|
230
|
+
// If it's the same tag, skip comparison
|
|
231
|
+
if (otherTag === tagName)
|
|
232
|
+
return true;
|
|
233
|
+
// If the other tag starts with the same prefix, it's not unique
|
|
234
|
+
return !otherTag.startsWith(prefix);
|
|
235
|
+
});
|
|
236
|
+
if (isUnique) {
|
|
237
|
+
prefixMap.set(tagName, prefix);
|
|
238
|
+
}
|
|
239
|
+
else {
|
|
240
|
+
prefixLength++;
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
// If we couldn't find a unique prefix, use the full tag name
|
|
244
|
+
if (!isUnique) {
|
|
245
|
+
prefixMap.set(tagName, tagName);
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
return prefixMap;
|
|
249
|
+
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { editItem } from '../primitives/editItem.js';
|
|
3
|
+
export const schema = z.object({
|
|
4
|
+
id: z.string().optional().describe("The ID of the task or project to edit"),
|
|
5
|
+
name: z.string().optional().describe("The name of the task or project to edit (as fallback if ID not provided)"),
|
|
6
|
+
itemType: z.enum(['task', 'project']).describe("Type of item to edit ('task' or 'project')"),
|
|
7
|
+
// Common editable fields
|
|
8
|
+
newName: z.string().optional().describe("New name for the item"),
|
|
9
|
+
newNote: z.string().optional().describe("New note for the item"),
|
|
10
|
+
newDueDate: z.string().optional().describe("New due date in ISO format (YYYY-MM-DD or full ISO date); set to empty string to clear"),
|
|
11
|
+
newDeferDate: z.string().optional().describe("New defer date in ISO format (YYYY-MM-DD or full ISO date); set to empty string to clear"),
|
|
12
|
+
newFlagged: z.boolean().optional().describe("Set flagged status (set to false for no flag, true for flag)"),
|
|
13
|
+
newEstimatedMinutes: z.number().optional().describe("New estimated minutes"),
|
|
14
|
+
// Task-specific fields
|
|
15
|
+
newStatus: z.enum(['incomplete', 'completed', 'dropped']).optional().describe("New status for tasks (incomplete, completed, dropped)"),
|
|
16
|
+
addTags: z.array(z.string()).optional().describe("Tags to add to the task"),
|
|
17
|
+
removeTags: z.array(z.string()).optional().describe("Tags to remove from the task"),
|
|
18
|
+
replaceTags: z.array(z.string()).optional().describe("Tags to replace all existing tags with"),
|
|
19
|
+
// Project-specific fields
|
|
20
|
+
newSequential: z.boolean().optional().describe("Whether the project should be sequential"),
|
|
21
|
+
newFolderName: z.string().optional().describe("New folder to move the project to"),
|
|
22
|
+
newProjectStatus: z.enum(['active', 'completed', 'dropped', 'onHold']).optional().describe("New status for projects")
|
|
23
|
+
});
|
|
24
|
+
export async function handler(args, extra) {
|
|
25
|
+
try {
|
|
26
|
+
// Validate that either id or name is provided
|
|
27
|
+
if (!args.id && !args.name) {
|
|
28
|
+
return {
|
|
29
|
+
content: [{
|
|
30
|
+
type: "text",
|
|
31
|
+
text: "Either id or name must be provided to edit an item."
|
|
32
|
+
}],
|
|
33
|
+
isError: true
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
// Call the editItem function
|
|
37
|
+
const result = await editItem(args);
|
|
38
|
+
if (result.success) {
|
|
39
|
+
// Item was edited successfully
|
|
40
|
+
const itemTypeLabel = args.itemType === 'task' ? 'Task' : 'Project';
|
|
41
|
+
let changedText = '';
|
|
42
|
+
if (result.changedProperties) {
|
|
43
|
+
changedText = ` (${result.changedProperties})`;
|
|
44
|
+
}
|
|
45
|
+
return {
|
|
46
|
+
content: [{
|
|
47
|
+
type: "text",
|
|
48
|
+
text: `✅ ${itemTypeLabel} "${result.name}" updated successfully${changedText}.`
|
|
49
|
+
}]
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
else {
|
|
53
|
+
// Item editing failed
|
|
54
|
+
let errorMsg = `Failed to update ${args.itemType}`;
|
|
55
|
+
if (result.error) {
|
|
56
|
+
if (result.error.includes("Item not found")) {
|
|
57
|
+
errorMsg = `${args.itemType.charAt(0).toUpperCase() + args.itemType.slice(1)} not found`;
|
|
58
|
+
if (args.id)
|
|
59
|
+
errorMsg += ` with ID "${args.id}"`;
|
|
60
|
+
if (args.name)
|
|
61
|
+
errorMsg += `${args.id ? ' or' : ' with'} name "${args.name}"`;
|
|
62
|
+
errorMsg += '.';
|
|
63
|
+
}
|
|
64
|
+
else {
|
|
65
|
+
errorMsg += `: ${result.error}`;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
return {
|
|
69
|
+
content: [{
|
|
70
|
+
type: "text",
|
|
71
|
+
text: errorMsg
|
|
72
|
+
}],
|
|
73
|
+
isError: true
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
catch (err) {
|
|
78
|
+
const error = err;
|
|
79
|
+
console.error(`Tool execution error: ${error.message}`);
|
|
80
|
+
return {
|
|
81
|
+
content: [{
|
|
82
|
+
type: "text",
|
|
83
|
+
text: `Error updating ${args.itemType}: ${error.message}`
|
|
84
|
+
}],
|
|
85
|
+
isError: true
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { getTaskById } from '../primitives/getTaskById.js';
|
|
3
|
+
export const schema = z.object({
|
|
4
|
+
taskId: z.string().optional().describe("The ID of the task to retrieve"),
|
|
5
|
+
taskName: z.string().optional().describe("The name of the task to retrieve (alternative to taskId)")
|
|
6
|
+
});
|
|
7
|
+
export async function handler(args, extra) {
|
|
8
|
+
try {
|
|
9
|
+
// Validate that either taskId or taskName is provided
|
|
10
|
+
if (!args.taskId && !args.taskName) {
|
|
11
|
+
return {
|
|
12
|
+
content: [{
|
|
13
|
+
type: "text",
|
|
14
|
+
text: "Error: Either taskId or taskName must be provided."
|
|
15
|
+
}],
|
|
16
|
+
isError: true
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
// Call the getTaskById function
|
|
20
|
+
const result = await getTaskById(args);
|
|
21
|
+
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
|
+
infoText += `• **Has Children**: ${task.hasChildren ? `Yes (${task.childrenCount} subtasks)` : 'No'}\n`;
|
|
37
|
+
return {
|
|
38
|
+
content: [{
|
|
39
|
+
type: "text",
|
|
40
|
+
text: infoText
|
|
41
|
+
}]
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
else {
|
|
45
|
+
// Task retrieval failed
|
|
46
|
+
return {
|
|
47
|
+
content: [{
|
|
48
|
+
type: "text",
|
|
49
|
+
text: `Failed to retrieve task: ${result.error}`
|
|
50
|
+
}],
|
|
51
|
+
isError: true
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
catch (err) {
|
|
56
|
+
const error = err;
|
|
57
|
+
console.error(`Tool execution error: ${error.message}`);
|
|
58
|
+
return {
|
|
59
|
+
content: [{
|
|
60
|
+
type: "text",
|
|
61
|
+
text: `Error retrieving task: ${error.message}`
|
|
62
|
+
}],
|
|
63
|
+
isError: true
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
}
|