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.
@@ -0,0 +1,80 @@
1
+ import { z } from 'zod';
2
+ import { removeItem } from '../primitives/removeItem.js';
3
+ export const schema = z.object({
4
+ id: z.string().optional().describe("The ID of the task or project to remove"),
5
+ name: z.string().optional().describe("The name of the task or project to remove (as fallback if ID not provided)"),
6
+ itemType: z.enum(['task', 'project']).describe("Type of item to remove ('task' or 'project')")
7
+ });
8
+ export async function handler(args, extra) {
9
+ try {
10
+ // Validate that either id or name is provided
11
+ if (!args.id && !args.name) {
12
+ return {
13
+ content: [{
14
+ type: "text",
15
+ text: "Either id or name must be provided to remove an item."
16
+ }],
17
+ isError: true
18
+ };
19
+ }
20
+ // Validate itemType
21
+ if (!['task', 'project'].includes(args.itemType)) {
22
+ return {
23
+ content: [{
24
+ type: "text",
25
+ text: `Invalid item type: ${args.itemType}. Must be either 'task' or 'project'.`
26
+ }],
27
+ isError: true
28
+ };
29
+ }
30
+ // Log the remove operation for debugging
31
+ console.error(`Removing ${args.itemType} with ID: ${args.id || 'not provided'}, Name: ${args.name || 'not provided'}`);
32
+ // Call the removeItem function
33
+ const result = await removeItem(args);
34
+ if (result.success) {
35
+ // Item was removed successfully
36
+ const itemTypeLabel = args.itemType === 'task' ? 'Task' : 'Project';
37
+ return {
38
+ content: [{
39
+ type: "text",
40
+ text: `✅ ${itemTypeLabel} "${result.name}" removed successfully.`
41
+ }]
42
+ };
43
+ }
44
+ else {
45
+ // Item removal failed
46
+ let errorMsg = `Failed to remove ${args.itemType}`;
47
+ if (result.error) {
48
+ if (result.error.includes("Item not found")) {
49
+ errorMsg = `${args.itemType.charAt(0).toUpperCase() + args.itemType.slice(1)} not found`;
50
+ if (args.id)
51
+ errorMsg += ` with ID "${args.id}"`;
52
+ if (args.name)
53
+ errorMsg += `${args.id ? ' or' : ' with'} name "${args.name}"`;
54
+ errorMsg += '.';
55
+ }
56
+ else {
57
+ errorMsg += `: ${result.error}`;
58
+ }
59
+ }
60
+ return {
61
+ content: [{
62
+ type: "text",
63
+ text: errorMsg
64
+ }],
65
+ isError: true
66
+ };
67
+ }
68
+ }
69
+ catch (err) {
70
+ const error = err;
71
+ console.error(`Tool execution error: ${error.message}`);
72
+ return {
73
+ content: [{
74
+ type: "text",
75
+ text: `Error removing ${args.itemType}: ${error.message}`
76
+ }],
77
+ isError: true
78
+ };
79
+ }
80
+ }
@@ -0,0 +1,121 @@
1
+ import { executeOmniFocusScript } from '../utils/scriptExecution.js';
2
+ // Main function to dump the database
3
+ export async function dumpDatabase() {
4
+ try {
5
+ // Execute the OmniFocus script
6
+ const data = await executeOmniFocusScript('@omnifocusDump.js');
7
+ // wait 1 second
8
+ await new Promise(resolve => setTimeout(resolve, 1000));
9
+ // Create an empty database if no data returned
10
+ if (!data) {
11
+ return {
12
+ exportDate: new Date().toISOString(),
13
+ tasks: [],
14
+ projects: {},
15
+ folders: {},
16
+ tags: {}
17
+ };
18
+ }
19
+ // Initialize the database object
20
+ const database = {
21
+ exportDate: data.exportDate,
22
+ tasks: [],
23
+ projects: {},
24
+ folders: {},
25
+ tags: {}
26
+ };
27
+ // Process tasks
28
+ if (data.tasks && Array.isArray(data.tasks)) {
29
+ // Convert the tasks to our OmnifocusTask format
30
+ database.tasks = data.tasks.map((task) => {
31
+ // Get tag names from the tag IDs
32
+ const tagNames = (task.tags || []).map(tagId => {
33
+ return data.tags[tagId]?.name || 'Unknown Tag';
34
+ });
35
+ return {
36
+ id: String(task.id),
37
+ name: String(task.name),
38
+ note: String(task.note || ""),
39
+ flagged: Boolean(task.flagged),
40
+ completed: task.taskStatus === "Completed",
41
+ completionDate: null, // Not available in the new format
42
+ dropDate: null, // Not available in the new format
43
+ taskStatus: String(task.taskStatus),
44
+ active: task.taskStatus !== "Completed" && task.taskStatus !== "Dropped",
45
+ dueDate: task.dueDate,
46
+ deferDate: task.deferDate,
47
+ estimatedMinutes: task.estimatedMinutes ? Number(task.estimatedMinutes) : null,
48
+ tags: task.tags || [],
49
+ tagNames: tagNames,
50
+ parentId: task.parentTaskID || null,
51
+ containingProjectId: task.projectID || null,
52
+ projectId: task.projectID || null,
53
+ childIds: task.children || [],
54
+ hasChildren: (task.children && task.children.length > 0) || false,
55
+ sequential: Boolean(task.sequential),
56
+ completedByChildren: Boolean(task.completedByChildren),
57
+ isRepeating: false, // Not available in the new format
58
+ repetitionMethod: null, // Not available in the new format
59
+ repetitionRule: null, // Not available in the new format
60
+ attachments: [], // Default empty array
61
+ linkedFileURLs: [], // Default empty array
62
+ notifications: [], // Default empty array
63
+ shouldUseFloatingTimeZone: false // Default value
64
+ };
65
+ });
66
+ }
67
+ // Process projects
68
+ if (data.projects) {
69
+ for (const [id, project] of Object.entries(data.projects)) {
70
+ database.projects[id] = {
71
+ id: String(project.id),
72
+ name: String(project.name),
73
+ status: String(project.status),
74
+ folderID: project.folderID || null,
75
+ sequential: Boolean(project.sequential),
76
+ effectiveDueDate: project.effectiveDueDate,
77
+ effectiveDeferDate: project.effectiveDeferDate,
78
+ dueDate: project.dueDate,
79
+ deferDate: project.deferDate,
80
+ completedByChildren: Boolean(project.completedByChildren),
81
+ containsSingletonActions: Boolean(project.containsSingletonActions),
82
+ note: String(project.note || ""),
83
+ tasks: project.tasks || [],
84
+ flagged: false, // Default value
85
+ estimatedMinutes: null // Default value
86
+ };
87
+ }
88
+ }
89
+ // Process folders
90
+ if (data.folders) {
91
+ for (const [id, folder] of Object.entries(data.folders)) {
92
+ database.folders[id] = {
93
+ id: String(folder.id),
94
+ name: String(folder.name),
95
+ parentFolderID: folder.parentFolderID || null,
96
+ status: String(folder.status),
97
+ projects: folder.projects || [],
98
+ subfolders: folder.subfolders || []
99
+ };
100
+ }
101
+ }
102
+ // Process tags
103
+ if (data.tags) {
104
+ for (const [id, tag] of Object.entries(data.tags)) {
105
+ database.tags[id] = {
106
+ id: String(tag.id),
107
+ name: String(tag.name),
108
+ parentTagID: tag.parentTagID || null,
109
+ active: Boolean(tag.active),
110
+ allowsNextAction: Boolean(tag.allowsNextAction),
111
+ tasks: tag.tasks || []
112
+ };
113
+ }
114
+ }
115
+ return database;
116
+ }
117
+ catch (error) {
118
+ console.error("Error in dumpDatabase:", error);
119
+ throw error;
120
+ }
121
+ }
@@ -0,0 +1,157 @@
1
+ import { exec } from 'child_process';
2
+ import { promisify } from 'util';
3
+ const execAsync = promisify(exec);
4
+ /**
5
+ * Generate pure AppleScript for task creation
6
+ */
7
+ function generateAppleScript(params) {
8
+ // Sanitize and prepare parameters for AppleScript
9
+ const name = params.name.replace(/['"\\]/g, '\\$&'); // Escape quotes and backslashes
10
+ const note = params.note?.replace(/['"\\]/g, '\\$&') || '';
11
+ const dueDate = params.dueDate || '';
12
+ const deferDate = params.deferDate || '';
13
+ const flagged = params.flagged === true;
14
+ const estimatedMinutes = params.estimatedMinutes?.toString() || '';
15
+ const tags = params.tags || [];
16
+ const projectName = params.projectName?.replace(/['"\\]/g, '\\$&') || '';
17
+ const parentTaskId = params.parentTaskId?.replace(/['"\\]/g, '\\$&') || '';
18
+ const parentTaskName = params.parentTaskName?.replace(/['"\\]/g, '\\$&') || '';
19
+ // Construct AppleScript with error handling
20
+ let script = `
21
+ try
22
+ tell application "OmniFocus"
23
+ tell front document
24
+ -- Determine the container (parent task, project, or inbox)
25
+ if "${parentTaskId}" is not "" then
26
+ -- Create subtask using parent task ID
27
+ try
28
+ set theParentTask to first flattened task where id = "${parentTaskId}"
29
+ set newTask to make new task with properties {name:"${name}"} at end of tasks of theParentTask
30
+ on error
31
+ return "{\\\"success\\\":false,\\\"error\\\":\\\"Parent task not found with ID: ${parentTaskId}\\\"}"
32
+ end try
33
+ else if "${parentTaskName}" is not "" then
34
+ -- Create subtask using parent task name
35
+ try
36
+ set theParentTask to first flattened task where name = "${parentTaskName}"
37
+ set newTask to make new task with properties {name:"${name}"} at end of tasks of theParentTask
38
+ on error
39
+ return "{\\\"success\\\":false,\\\"error\\\":\\\"Parent task not found with name: ${parentTaskName}\\\"}"
40
+ end try
41
+ else if "${projectName}" is not "" then
42
+ -- Use specified project
43
+ try
44
+ set theProject to first flattened project where name = "${projectName}"
45
+ set newTask to make new task with properties {name:"${name}"} at end of tasks of theProject
46
+ on error
47
+ return "{\\\"success\\\":false,\\\"error\\\":\\\"Project not found: ${projectName}\\\"}"
48
+ end try
49
+ else
50
+ -- Use inbox of the document
51
+ set newTask to make new inbox task with properties {name:"${name}"}
52
+ end if
53
+
54
+ -- Set task properties
55
+ ${note ? `set note of newTask to "${note}"` : ''}
56
+ ${dueDate ? `
57
+ set due date of newTask to (current date) + (time to GMT)
58
+ set due date of newTask to date "${dueDate}"` : ''}
59
+ ${deferDate ? `
60
+ set defer date of newTask to (current date) + (time to GMT)
61
+ set defer date of newTask to date "${deferDate}"` : ''}
62
+ ${flagged ? `set flagged of newTask to true` : ''}
63
+ ${estimatedMinutes ? `set estimated minutes of newTask to ${estimatedMinutes}` : ''}
64
+
65
+ -- Get the task ID
66
+ set taskId to id of newTask as string
67
+
68
+ -- Add tags if provided
69
+ ${tags.length > 0 ? tags.map(tag => {
70
+ const sanitizedTag = tag.replace(/['"\\]/g, '\\$&');
71
+ return `
72
+ try
73
+ set theTag to first flattened tag where name = "${sanitizedTag}"
74
+ tell newTask to add theTag
75
+ on error
76
+ -- Ignore errors finding/adding tags
77
+ end try`;
78
+ }).join('\n') : ''}
79
+
80
+ -- Return success with task ID
81
+ return "{\\\"success\\\":true,\\\"taskId\\\":\\"" & taskId & "\\",\\\"name\\\":\\"${name}\\"}"
82
+ end tell
83
+ end tell
84
+ on error errorMessage
85
+ return "{\\\"success\\\":false,\\\"error\\\":\\"" & errorMessage & "\\"}"
86
+ end try
87
+ `;
88
+ return script;
89
+ }
90
+ /**
91
+ * Validate parent task parameters to prevent conflicts
92
+ */
93
+ async function validateParentTaskParams(params) {
94
+ // Check if both parentTaskId and parentTaskName are provided
95
+ if (params.parentTaskId && params.parentTaskName) {
96
+ return {
97
+ valid: false,
98
+ error: "Cannot specify both parentTaskId and parentTaskName. Please use only one."
99
+ };
100
+ }
101
+ // Check if parent task is specified along with projectName
102
+ if ((params.parentTaskId || params.parentTaskName) && params.projectName) {
103
+ return {
104
+ valid: false,
105
+ error: "Cannot specify both parent task and project. Subtasks inherit project from their parent."
106
+ };
107
+ }
108
+ return { valid: true };
109
+ }
110
+ /**
111
+ * Add a task to OmniFocus
112
+ */
113
+ export async function addOmniFocusTask(params) {
114
+ try {
115
+ // Validate parent task parameters
116
+ const validation = await validateParentTaskParams(params);
117
+ if (!validation.valid) {
118
+ return {
119
+ success: false,
120
+ error: validation.error
121
+ };
122
+ }
123
+ // Generate AppleScript
124
+ const script = generateAppleScript(params);
125
+ console.error("Executing AppleScript directly...");
126
+ // Execute AppleScript directly
127
+ const { stdout, stderr } = await execAsync(`osascript -e '${script}'`);
128
+ if (stderr) {
129
+ console.error("AppleScript stderr:", stderr);
130
+ }
131
+ console.error("AppleScript stdout:", stdout);
132
+ // Parse the result
133
+ try {
134
+ const result = JSON.parse(stdout);
135
+ // Return the result
136
+ return {
137
+ success: result.success,
138
+ taskId: result.taskId,
139
+ error: result.error
140
+ };
141
+ }
142
+ catch (parseError) {
143
+ console.error("Error parsing AppleScript result:", parseError);
144
+ return {
145
+ success: false,
146
+ error: `Failed to parse result: ${stdout}`
147
+ };
148
+ }
149
+ }
150
+ catch (error) {
151
+ console.error("Error in addOmniFocusTask:", error);
152
+ return {
153
+ success: false,
154
+ error: error?.message || "Unknown error in addOmniFocusTask"
155
+ };
156
+ }
157
+ }
@@ -0,0 +1,113 @@
1
+ import { exec } from 'child_process';
2
+ import { promisify } from 'util';
3
+ const execAsync = promisify(exec);
4
+ /**
5
+ * Generate pure AppleScript for project creation
6
+ */
7
+ function generateAppleScript(params) {
8
+ // Sanitize and prepare parameters for AppleScript
9
+ const name = params.name.replace(/['"\\]/g, '\\$&'); // Escape quotes and backslashes
10
+ const note = params.note?.replace(/['"\\]/g, '\\$&') || '';
11
+ const dueDate = params.dueDate || '';
12
+ const deferDate = params.deferDate || '';
13
+ const flagged = params.flagged === true;
14
+ const estimatedMinutes = params.estimatedMinutes?.toString() || '';
15
+ const tags = params.tags || [];
16
+ const folderName = params.folderName?.replace(/['"\\]/g, '\\$&') || '';
17
+ const sequential = params.sequential === true;
18
+ // Construct AppleScript with error handling
19
+ let script = `
20
+ try
21
+ tell application "OmniFocus"
22
+ tell front document
23
+ -- Determine the container (root or folder)
24
+ if "${folderName}" is "" then
25
+ -- Create project at the root level
26
+ set newProject to make new project with properties {name:"${name}"}
27
+ else
28
+ -- Use specified folder
29
+ try
30
+ set theFolder to first flattened folder where name = "${folderName}"
31
+ set newProject to make new project with properties {name:"${name}"} at end of projects of theFolder
32
+ on error
33
+ return "{\\\"success\\\":false,\\\"error\\\":\\\"Folder not found: ${folderName}\\\"}"
34
+ end try
35
+ end if
36
+
37
+ -- Set project properties
38
+ ${note ? `set note of newProject to "${note}"` : ''}
39
+ ${dueDate ? `
40
+ set due date of newProject to (current date) + (time to GMT)
41
+ set due date of newProject to date "${dueDate}"` : ''}
42
+ ${deferDate ? `
43
+ set defer date of newProject to (current date) + (time to GMT)
44
+ set defer date of newProject to date "${deferDate}"` : ''}
45
+ ${flagged ? `set flagged of newProject to true` : ''}
46
+ ${estimatedMinutes ? `set estimated minutes of newProject to ${estimatedMinutes}` : ''}
47
+ ${`set sequential of newProject to ${sequential}`}
48
+
49
+ -- Get the project ID
50
+ set projectId to id of newProject as string
51
+
52
+ -- Add tags if provided
53
+ ${tags.length > 0 ? tags.map(tag => {
54
+ const sanitizedTag = tag.replace(/['"\\]/g, '\\$&');
55
+ return `
56
+ try
57
+ set theTag to first flattened tag where name = "${sanitizedTag}"
58
+ tell newProject to add theTag
59
+ on error
60
+ -- Ignore errors finding/adding tags
61
+ end try`;
62
+ }).join('\n') : ''}
63
+
64
+ -- Return success with project ID
65
+ return "{\\\"success\\\":true,\\\"projectId\\\":\\"" & projectId & "\\",\\\"name\\\":\\"${name}\\"}"
66
+ end tell
67
+ end tell
68
+ on error errorMessage
69
+ return "{\\\"success\\\":false,\\\"error\\\":\\"" & errorMessage & "\\"}"
70
+ end try
71
+ `;
72
+ return script;
73
+ }
74
+ /**
75
+ * Add a project to OmniFocus
76
+ */
77
+ export async function addProject(params) {
78
+ try {
79
+ // Generate AppleScript
80
+ const script = generateAppleScript(params);
81
+ console.error("Executing AppleScript directly...");
82
+ // Execute AppleScript directly
83
+ const { stdout, stderr } = await execAsync(`osascript -e '${script}'`);
84
+ if (stderr) {
85
+ console.error("AppleScript stderr:", stderr);
86
+ }
87
+ console.error("AppleScript stdout:", stdout);
88
+ // Parse the result
89
+ try {
90
+ const result = JSON.parse(stdout);
91
+ // Return the result
92
+ return {
93
+ success: result.success,
94
+ projectId: result.projectId,
95
+ error: result.error
96
+ };
97
+ }
98
+ catch (parseError) {
99
+ console.error("Error parsing AppleScript result:", parseError);
100
+ return {
101
+ success: false,
102
+ error: `Failed to parse result: ${stdout}`
103
+ };
104
+ }
105
+ }
106
+ catch (error) {
107
+ console.error("Error in addProject:", error);
108
+ return {
109
+ success: false,
110
+ error: error?.message || "Unknown error in addProject"
111
+ };
112
+ }
113
+ }
@@ -0,0 +1,37 @@
1
+ import { addOmniFocusTask } from './addOmniFocusTask.js';
2
+ /**
3
+ * Add a subtask to an existing task in OmniFocus
4
+ * This is a convenience function that wraps addOmniFocusTask with parent task parameters
5
+ */
6
+ export async function addSubtask(params) {
7
+ // Validate that either parentTaskId or parentTaskName is provided
8
+ if (!params.parentTaskId && !params.parentTaskName) {
9
+ return {
10
+ success: false,
11
+ error: "Either parentTaskId or parentTaskName must be provided for subtask creation"
12
+ };
13
+ }
14
+ // Convert AddSubtaskParams to AddOmniFocusTaskParams
15
+ const taskParams = {
16
+ name: params.name,
17
+ note: params.note,
18
+ dueDate: params.dueDate,
19
+ deferDate: params.deferDate,
20
+ flagged: params.flagged,
21
+ estimatedMinutes: params.estimatedMinutes,
22
+ tags: params.tags,
23
+ parentTaskId: params.parentTaskId,
24
+ parentTaskName: params.parentTaskName,
25
+ // Don't set projectName for subtasks - they inherit from parent
26
+ };
27
+ try {
28
+ const result = await addOmniFocusTask(taskParams);
29
+ return result;
30
+ }
31
+ catch (error) {
32
+ return {
33
+ success: false,
34
+ error: error?.message || "Unknown error in addSubtask"
35
+ };
36
+ }
37
+ }
@@ -0,0 +1,87 @@
1
+ import { addOmniFocusTask } from './addOmniFocusTask.js';
2
+ import { addProject } from './addProject.js';
3
+ /**
4
+ * Add multiple items (tasks or projects) to OmniFocus
5
+ */
6
+ export async function batchAddItems(items) {
7
+ try {
8
+ // Results array to track individual operation outcomes
9
+ const results = [];
10
+ // Process each item in sequence
11
+ for (const item of items) {
12
+ try {
13
+ if (item.type === 'task') {
14
+ // Extract task-specific params
15
+ const taskParams = {
16
+ name: item.name,
17
+ note: item.note,
18
+ dueDate: item.dueDate,
19
+ deferDate: item.deferDate,
20
+ flagged: item.flagged,
21
+ estimatedMinutes: item.estimatedMinutes,
22
+ tags: item.tags,
23
+ projectName: item.projectName,
24
+ parentTaskId: item.parentTaskId,
25
+ parentTaskName: item.parentTaskName
26
+ };
27
+ // Add task
28
+ const taskResult = await addOmniFocusTask(taskParams);
29
+ results.push({
30
+ success: taskResult.success,
31
+ id: taskResult.taskId,
32
+ error: taskResult.error
33
+ });
34
+ }
35
+ else if (item.type === 'project') {
36
+ // Extract project-specific params
37
+ const projectParams = {
38
+ name: item.name,
39
+ note: item.note,
40
+ dueDate: item.dueDate,
41
+ deferDate: item.deferDate,
42
+ flagged: item.flagged,
43
+ estimatedMinutes: item.estimatedMinutes,
44
+ tags: item.tags,
45
+ folderName: item.folderName,
46
+ sequential: item.sequential
47
+ };
48
+ // Add project
49
+ const projectResult = await addProject(projectParams);
50
+ results.push({
51
+ success: projectResult.success,
52
+ id: projectResult.projectId,
53
+ error: projectResult.error
54
+ });
55
+ }
56
+ else {
57
+ // Invalid type
58
+ results.push({
59
+ success: false,
60
+ error: `Invalid item type: ${item.type}`
61
+ });
62
+ }
63
+ }
64
+ catch (itemError) {
65
+ // Handle individual item errors
66
+ results.push({
67
+ success: false,
68
+ error: itemError.message || "Unknown error processing item"
69
+ });
70
+ }
71
+ }
72
+ // Determine overall success (true if at least one item was added successfully)
73
+ const overallSuccess = results.some(result => result.success);
74
+ return {
75
+ success: overallSuccess,
76
+ results: results
77
+ };
78
+ }
79
+ catch (error) {
80
+ console.error("Error in batchAddItems:", error);
81
+ return {
82
+ success: false,
83
+ results: [],
84
+ error: error.message || "Unknown error in batchAddItems"
85
+ };
86
+ }
87
+ }
@@ -0,0 +1,44 @@
1
+ import { removeItem } from './removeItem.js';
2
+ /**
3
+ * Remove multiple items (tasks or projects) from OmniFocus
4
+ */
5
+ export async function batchRemoveItems(items) {
6
+ try {
7
+ // Results array to track individual operation outcomes
8
+ const results = [];
9
+ // Process each item in sequence
10
+ for (const item of items) {
11
+ try {
12
+ // Remove item
13
+ const itemResult = await removeItem(item);
14
+ results.push({
15
+ success: itemResult.success,
16
+ id: itemResult.id,
17
+ name: itemResult.name,
18
+ error: itemResult.error
19
+ });
20
+ }
21
+ catch (itemError) {
22
+ // Handle individual item errors
23
+ results.push({
24
+ success: false,
25
+ error: itemError.message || "Unknown error processing item"
26
+ });
27
+ }
28
+ }
29
+ // Determine overall success (true if at least one item was removed successfully)
30
+ const overallSuccess = results.some(result => result.success);
31
+ return {
32
+ success: overallSuccess,
33
+ results: results
34
+ };
35
+ }
36
+ catch (error) {
37
+ console.error("Error in batchRemoveItems:", error);
38
+ return {
39
+ success: false,
40
+ results: [],
41
+ error: error.message || "Unknown error in batchRemoveItems"
42
+ };
43
+ }
44
+ }