@iflow-mcp/omnifocus-mcp 1.2.3

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.
Files changed (69) hide show
  1. package/QUERY_TOOL_EXAMPLES.md +298 -0
  2. package/QUERY_TOOL_REFERENCE.md +228 -0
  3. package/README.md +250 -0
  4. package/assets/omnifocus-mcp-logo.png +0 -0
  5. package/cli.cjs +9 -0
  6. package/dist/omnifocustypes.js +48 -0
  7. package/dist/server.js +44 -0
  8. package/dist/tools/definitions/addOmniFocusTask.js +76 -0
  9. package/dist/tools/definitions/addProject.js +61 -0
  10. package/dist/tools/definitions/batchAddItems.js +89 -0
  11. package/dist/tools/definitions/batchRemoveItems.js +74 -0
  12. package/dist/tools/definitions/dumpDatabase.js +259 -0
  13. package/dist/tools/definitions/editItem.js +88 -0
  14. package/dist/tools/definitions/getPerspectiveView.js +107 -0
  15. package/dist/tools/definitions/listPerspectives.js +65 -0
  16. package/dist/tools/definitions/queryOmnifocus.js +190 -0
  17. package/dist/tools/definitions/removeItem.js +80 -0
  18. package/dist/tools/dumpDatabase.js +121 -0
  19. package/dist/tools/dumpDatabaseOptimized.js +192 -0
  20. package/dist/tools/primitives/addOmniFocusTask.js +227 -0
  21. package/dist/tools/primitives/addProject.js +132 -0
  22. package/dist/tools/primitives/batchAddItems.js +166 -0
  23. package/dist/tools/primitives/batchRemoveItems.js +44 -0
  24. package/dist/tools/primitives/editItem.js +443 -0
  25. package/dist/tools/primitives/getPerspectiveView.js +50 -0
  26. package/dist/tools/primitives/listPerspectives.js +34 -0
  27. package/dist/tools/primitives/queryOmnifocus.js +365 -0
  28. package/dist/tools/primitives/queryOmnifocusDebug.js +135 -0
  29. package/dist/tools/primitives/removeItem.js +177 -0
  30. package/dist/types.js +1 -0
  31. package/dist/utils/cacheManager.js +187 -0
  32. package/dist/utils/dateFormatting.js +58 -0
  33. package/dist/utils/omnifocusScripts/getPerspectiveView.js +169 -0
  34. package/dist/utils/omnifocusScripts/listPerspectives.js +59 -0
  35. package/dist/utils/omnifocusScripts/omnifocusDump.js +223 -0
  36. package/dist/utils/scriptExecution.js +113 -0
  37. package/package.json +37 -0
  38. package/src/omnifocustypes.ts +89 -0
  39. package/src/server.ts +109 -0
  40. package/src/tools/definitions/addOmniFocusTask.ts +80 -0
  41. package/src/tools/definitions/addProject.ts +67 -0
  42. package/src/tools/definitions/batchAddItems.ts +98 -0
  43. package/src/tools/definitions/batchRemoveItems.ts +80 -0
  44. package/src/tools/definitions/dumpDatabase.ts +311 -0
  45. package/src/tools/definitions/editItem.ts +96 -0
  46. package/src/tools/definitions/getPerspectiveView.ts +125 -0
  47. package/src/tools/definitions/listPerspectives.ts +72 -0
  48. package/src/tools/definitions/queryOmnifocus.ts +212 -0
  49. package/src/tools/definitions/removeItem.ts +86 -0
  50. package/src/tools/dumpDatabase.ts +196 -0
  51. package/src/tools/dumpDatabaseOptimized.ts +231 -0
  52. package/src/tools/primitives/addOmniFocusTask.ts +252 -0
  53. package/src/tools/primitives/addProject.ts +156 -0
  54. package/src/tools/primitives/batchAddItems.ts +207 -0
  55. package/src/tools/primitives/batchRemoveItems.ts +64 -0
  56. package/src/tools/primitives/editItem.ts +507 -0
  57. package/src/tools/primitives/getPerspectiveView.ts +71 -0
  58. package/src/tools/primitives/listPerspectives.ts +53 -0
  59. package/src/tools/primitives/queryOmnifocus.ts +394 -0
  60. package/src/tools/primitives/queryOmnifocusDebug.ts +139 -0
  61. package/src/tools/primitives/removeItem.ts +195 -0
  62. package/src/types.ts +107 -0
  63. package/src/utils/cacheManager.ts +234 -0
  64. package/src/utils/dateFormatting.ts +81 -0
  65. package/src/utils/omnifocusScripts/getPerspectiveView.js +169 -0
  66. package/src/utils/omnifocusScripts/listPerspectives.js +59 -0
  67. package/src/utils/omnifocusScripts/omnifocusDump.js +223 -0
  68. package/src/utils/scriptExecution.ts +128 -0
  69. package/tsconfig.json +15 -0
@@ -0,0 +1,231 @@
1
+ import { OmnifocusDatabase } from '../types.js';
2
+ import { executeOmniFocusScript } from '../utils/scriptExecution.js';
3
+ import { getCacheManager } from '../utils/cacheManager.js';
4
+ import { dumpDatabase as originalDumpDatabase } from './dumpDatabase.js';
5
+
6
+ /**
7
+ * Optimized version of dumpDatabase that uses caching
8
+ * Falls back to original implementation if caching fails
9
+ */
10
+ export async function dumpDatabaseOptimized(options?: {
11
+ forceRefresh?: boolean;
12
+ cacheKey?: string;
13
+ }): Promise<OmnifocusDatabase> {
14
+ const cacheManager = getCacheManager({
15
+ ttlSeconds: 300, // 5 minute cache
16
+ useChecksum: true // Validate with database checksum
17
+ });
18
+
19
+ const cacheKey = options?.cacheKey || 'full-dump';
20
+
21
+ // Check if we should force a refresh
22
+ if (!options?.forceRefresh) {
23
+ const cached = await cacheManager.get(cacheKey);
24
+ if (cached) {
25
+ cacheManager.trackHit();
26
+ console.log(`Cache hit for ${cacheKey}. Stats:`, cacheManager.getStats());
27
+ return cached;
28
+ }
29
+ }
30
+
31
+ cacheManager.trackMiss();
32
+ console.log(`Cache miss for ${cacheKey}. Fetching fresh data...`);
33
+
34
+ // Fetch fresh data using the original implementation
35
+ const freshData = await originalDumpDatabase();
36
+
37
+ // Store in cache for next time
38
+ await cacheManager.set(cacheKey, freshData);
39
+
40
+ return freshData;
41
+ }
42
+
43
+ /**
44
+ * Get just database statistics without full dump
45
+ * Much more efficient for overview information
46
+ */
47
+ export async function getDatabaseStats(): Promise<{
48
+ taskCount: number;
49
+ activeTaskCount: number;
50
+ projectCount: number;
51
+ activeProjectCount: number;
52
+ folderCount: number;
53
+ tagCount: number;
54
+ overdueCount: number;
55
+ nextActionCount: number;
56
+ flaggedCount: number;
57
+ inboxCount: number;
58
+ lastModified: string;
59
+ }> {
60
+ const script = `
61
+ (() => {
62
+ try {
63
+ // Calculate statistics without fetching full data
64
+ const allTasks = flattenedTasks;
65
+ const activeTasks = allTasks.filter(task =>
66
+ task.taskStatus !== Task.Status.Completed &&
67
+ task.taskStatus !== Task.Status.Dropped
68
+ );
69
+
70
+ const allProjects = flattenedProjects;
71
+ const activeProjects = allProjects.filter(project =>
72
+ project.status === Project.Status.Active
73
+ );
74
+
75
+ // Count specific task statuses
76
+ const overdueCount = activeTasks.filter(task =>
77
+ task.taskStatus === Task.Status.Overdue
78
+ ).length;
79
+
80
+ const nextActionCount = activeTasks.filter(task =>
81
+ task.taskStatus === Task.Status.Next
82
+ ).length;
83
+
84
+ const flaggedCount = activeTasks.filter(task => task.flagged).length;
85
+
86
+ const inboxCount = activeTasks.filter(task => task.inInbox).length;
87
+
88
+ // Get latest modification time
89
+ let lastModified = new Date(0);
90
+ allTasks.forEach(task => {
91
+ if (task.modificationDate && task.modificationDate > lastModified) {
92
+ lastModified = task.modificationDate;
93
+ }
94
+ });
95
+
96
+ return JSON.stringify({
97
+ taskCount: allTasks.length,
98
+ activeTaskCount: activeTasks.length,
99
+ projectCount: allProjects.length,
100
+ activeProjectCount: activeProjects.length,
101
+ folderCount: flattenedFolders.length,
102
+ tagCount: flattenedTags.filter(tag => tag.active).length,
103
+ overdueCount: overdueCount,
104
+ nextActionCount: nextActionCount,
105
+ flaggedCount: flaggedCount,
106
+ inboxCount: inboxCount,
107
+ lastModified: lastModified.toISOString()
108
+ });
109
+
110
+ } catch (error) {
111
+ return JSON.stringify({
112
+ error: "Failed to get database stats: " + error.toString()
113
+ });
114
+ }
115
+ })();
116
+ `;
117
+
118
+ // Write script to temp file and execute
119
+ const fs = await import('fs');
120
+ const tempFile = `/tmp/omnifocus_stats_${Date.now()}.js`;
121
+ fs.writeFileSync(tempFile, script);
122
+
123
+ const result = await executeOmniFocusScript(tempFile);
124
+ fs.unlinkSync(tempFile);
125
+
126
+ if (result.error) {
127
+ throw new Error(result.error);
128
+ }
129
+
130
+ return result;
131
+ }
132
+
133
+ /**
134
+ * Get incremental changes since a specific timestamp
135
+ * Much more efficient for periodic updates
136
+ */
137
+ export async function getChangesSince(since: Date): Promise<{
138
+ newTasks: any[];
139
+ updatedTasks: any[];
140
+ completedTasks: any[];
141
+ newProjects: any[];
142
+ updatedProjects: any[];
143
+ }> {
144
+ const script = `
145
+ (() => {
146
+ try {
147
+ const sinceDate = new Date("${since.toISOString()}");
148
+
149
+ // Find tasks that changed since the given date
150
+ const allTasks = flattenedTasks;
151
+
152
+ const newTasks = allTasks.filter(task =>
153
+ task.creationDate && task.creationDate > sinceDate
154
+ ).map(task => ({
155
+ id: task.id.primaryKey,
156
+ name: task.name,
157
+ creationDate: task.creationDate.toISOString()
158
+ }));
159
+
160
+ const updatedTasks = allTasks.filter(task =>
161
+ task.modificationDate &&
162
+ task.modificationDate > sinceDate &&
163
+ task.creationDate &&
164
+ task.creationDate <= sinceDate
165
+ ).map(task => ({
166
+ id: task.id.primaryKey,
167
+ name: task.name,
168
+ modificationDate: task.modificationDate.toISOString()
169
+ }));
170
+
171
+ const completedTasks = allTasks.filter(task =>
172
+ task.completionDate &&
173
+ task.completionDate > sinceDate
174
+ ).map(task => ({
175
+ id: task.id.primaryKey,
176
+ name: task.name,
177
+ completionDate: task.completionDate.toISOString()
178
+ }));
179
+
180
+ // Find projects that changed
181
+ const allProjects = flattenedProjects;
182
+
183
+ const newProjects = allProjects.filter(project =>
184
+ project.creationDate && project.creationDate > sinceDate
185
+ ).map(project => ({
186
+ id: project.id.primaryKey,
187
+ name: project.name,
188
+ creationDate: project.creationDate.toISOString()
189
+ }));
190
+
191
+ const updatedProjects = allProjects.filter(project =>
192
+ project.modificationDate &&
193
+ project.modificationDate > sinceDate &&
194
+ project.creationDate &&
195
+ project.creationDate <= sinceDate
196
+ ).map(project => ({
197
+ id: project.id.primaryKey,
198
+ name: project.name,
199
+ modificationDate: project.modificationDate.toISOString()
200
+ }));
201
+
202
+ return JSON.stringify({
203
+ newTasks: newTasks,
204
+ updatedTasks: updatedTasks,
205
+ completedTasks: completedTasks,
206
+ newProjects: newProjects,
207
+ updatedProjects: updatedProjects
208
+ });
209
+
210
+ } catch (error) {
211
+ return JSON.stringify({
212
+ error: "Failed to get changes: " + error.toString()
213
+ });
214
+ }
215
+ })();
216
+ `;
217
+
218
+ // Write script to temp file and execute
219
+ const fs = await import('fs');
220
+ const tempFile = `/tmp/omnifocus_changes_${Date.now()}.js`;
221
+ fs.writeFileSync(tempFile, script);
222
+
223
+ const result = await executeOmniFocusScript(tempFile);
224
+ fs.unlinkSync(tempFile);
225
+
226
+ if (result.error) {
227
+ throw new Error(result.error);
228
+ }
229
+
230
+ return result;
231
+ }
@@ -0,0 +1,252 @@
1
+ import { exec } from 'child_process';
2
+ import { promisify } from 'util';
3
+ import { writeFileSync, unlinkSync } from 'fs';
4
+ import { tmpdir } from 'os';
5
+ import { join } from 'path';
6
+ import { createDateOutsideTellBlock } from '../../utils/dateFormatting.js';
7
+ const execAsync = promisify(exec);
8
+
9
+ // Interface for task creation parameters
10
+ export interface AddOmniFocusTaskParams {
11
+ name: string;
12
+ note?: string;
13
+ dueDate?: string; // ISO date string
14
+ deferDate?: string; // ISO date string
15
+ flagged?: boolean;
16
+ estimatedMinutes?: number;
17
+ tags?: string[]; // Tag names
18
+ projectName?: string; // Project name to add task to
19
+ // Hierarchy support
20
+ parentTaskId?: string;
21
+ parentTaskName?: string;
22
+ hierarchyLevel?: number; // ignored for single add
23
+ }
24
+
25
+ /**
26
+ * Generate pure AppleScript for task creation
27
+ */
28
+ function generateAppleScript(params: AddOmniFocusTaskParams): string {
29
+ // Sanitize and prepare parameters for AppleScript
30
+ const name = params.name.replace(/['"\\]/g, '\\$&'); // Escape quotes and backslashes
31
+ const note = params.note?.replace(/['"\\]/g, '\\$&') || '';
32
+ const dueDate = params.dueDate || '';
33
+ const deferDate = params.deferDate || '';
34
+ const flagged = params.flagged === true;
35
+ const estimatedMinutes = params.estimatedMinutes?.toString() || '';
36
+ const tags = params.tags || [];
37
+ const projectName = params.projectName?.replace(/['"\\]/g, '\\$&') || '';
38
+ const parentTaskId = params.parentTaskId?.replace(/['"\\]/g, '\\$&') || '';
39
+ const parentTaskName = params.parentTaskName?.replace(/['"\\]/g, '\\$&') || '';
40
+
41
+ // Generate date constructions outside tell blocks
42
+ let datePreScript = '';
43
+ let dueDateVar = '';
44
+ let deferDateVar = '';
45
+
46
+ if (dueDate) {
47
+ dueDateVar = `dueDate${Math.random().toString(36).substr(2, 9)}`;
48
+ datePreScript += createDateOutsideTellBlock(dueDate, dueDateVar) + '\n\n';
49
+ }
50
+
51
+ if (deferDate) {
52
+ deferDateVar = `deferDate${Math.random().toString(36).substr(2, 9)}`;
53
+ datePreScript += createDateOutsideTellBlock(deferDate, deferDateVar) + '\n\n';
54
+ }
55
+
56
+ // Construct AppleScript with error handling
57
+ let script = datePreScript + `
58
+ try
59
+ tell application "OmniFocus"
60
+ tell front document
61
+ -- Resolve parent task if provided
62
+ set newTask to missing value
63
+ set parentTask to missing value
64
+ set placement to ""
65
+
66
+ if "${parentTaskId}" is not "" then
67
+ try
68
+ set parentTask to first flattened task where id = "${parentTaskId}"
69
+ end try
70
+ if parentTask is missing value then
71
+ try
72
+ set parentTask to first inbox task where id = "${parentTaskId}"
73
+ end try
74
+ end if
75
+ -- If projectName provided, ensure parent is within that project
76
+ if parentTask is not missing value and "${projectName}" is not "" then
77
+ try
78
+ set pproj to containing project of parentTask
79
+ if pproj is missing value or name of pproj is not "${projectName}" then set parentTask to missing value
80
+ end try
81
+ end if
82
+ end if
83
+
84
+ if parentTask is missing value and "${parentTaskName}" is not "" then
85
+ if "${projectName}" is not "" then
86
+ -- Find by name but constrain to the specified project
87
+ try
88
+ set parentTask to first flattened task where name = "${parentTaskName}"
89
+ end try
90
+ if parentTask is not missing value then
91
+ try
92
+ set pproj to containing project of parentTask
93
+ if pproj is missing value or name of pproj is not "${projectName}" then set parentTask to missing value
94
+ end try
95
+ end if
96
+ else
97
+ -- No project specified; allow global or inbox match by name
98
+ try
99
+ set parentTask to first flattened task where name = "${parentTaskName}"
100
+ end try
101
+ if parentTask is missing value then
102
+ try
103
+ set parentTask to first inbox task where name = "${parentTaskName}"
104
+ end try
105
+ end if
106
+ end if
107
+ end if
108
+
109
+ if parentTask is not missing value then
110
+ -- Create task under parent task
111
+ set newTask to make new task with properties {name:"${name}"} at end of tasks of parentTask
112
+ else if "${projectName}" is not "" then
113
+ -- Create under specified project
114
+ try
115
+ set theProject to first flattened project where name = "${projectName}"
116
+ set newTask to make new task with properties {name:"${name}"} at end of tasks of theProject
117
+ on error
118
+ return "{\\\"success\\\":false,\\\"error\\\":\\\"Project not found: ${projectName}\\\"}"
119
+ end try
120
+ else
121
+ -- Fallback to inbox
122
+ set newTask to make new inbox task with properties {name:"${name}"}
123
+ end if
124
+
125
+ -- Set task properties
126
+ ${note ? `set note of newTask to "${note}"` : ''}
127
+ ${dueDate ? `
128
+ -- Set due date
129
+ set due date of newTask to ` + dueDateVar : ''}
130
+ ${deferDate ? `
131
+ -- Set defer date
132
+ set defer date of newTask to ` + deferDateVar : ''}
133
+ ${flagged ? `set flagged of newTask to true` : ''}
134
+ ${estimatedMinutes ? `set estimated minutes of newTask to ${estimatedMinutes}` : ''}
135
+
136
+ -- Derive placement from container; distinguish real parent vs project root task
137
+ try
138
+ set placement to "inbox"
139
+ set ctr to container of newTask
140
+ set cclass to class of ctr as string
141
+ set ctrId to id of ctr as string
142
+ if cclass is "project" then
143
+ set placement to "project"
144
+ else if cclass is "task" then
145
+ if parentTask is not missing value then
146
+ set parentId to id of parentTask as string
147
+ if ctrId is equal to parentId then
148
+ set placement to "parent"
149
+ else
150
+ -- Likely the project's root task; treat as project
151
+ set placement to "project"
152
+ end if
153
+ else
154
+ -- No explicit parent requested; container is root task -> treat as project
155
+ set placement to "project"
156
+ end if
157
+ else
158
+ set placement to "inbox"
159
+ end if
160
+ on error
161
+ -- If container access fails (e.g., inbox), default based on projectName
162
+ if "${projectName}" is not "" then
163
+ set placement to "project"
164
+ else
165
+ set placement to "inbox"
166
+ end if
167
+ end try
168
+
169
+ -- Get the task ID
170
+ set taskId to id of newTask as string
171
+
172
+ -- Add tags if provided
173
+ ${tags.length > 0 ? tags.map(tag => {
174
+ const sanitizedTag = tag.replace(/['"\\]/g, '\\$&');
175
+ return `
176
+ try
177
+ set theTag to first flattened tag where name = "${sanitizedTag}"
178
+ add theTag to tags of newTask
179
+ on error
180
+ -- Tag might not exist, try to create it
181
+ try
182
+ set theTag to make new tag with properties {name:"${sanitizedTag}"}
183
+ add theTag to tags of newTask
184
+ on error
185
+ -- Could not create or add tag
186
+ end try
187
+ end try`;
188
+ }).join('\n') : ''}
189
+
190
+ -- Return success with task ID and placement
191
+ return "{\\\"success\\\":true,\\\"taskId\\\":\\"" & taskId & "\\",\\\"name\\\":\\"${name}\\\",\\\"placement\\\":\\"" & placement & "\\"}"
192
+ end tell
193
+ end tell
194
+ on error errorMessage
195
+ return "{\\\"success\\\":false,\\\"error\\\":\\"" & errorMessage & "\\"}"
196
+ end try
197
+ `;
198
+
199
+ return script;
200
+ }
201
+
202
+ /**
203
+ * Add a task to OmniFocus
204
+ */
205
+ export async function addOmniFocusTask(params: AddOmniFocusTaskParams): Promise<{success: boolean, taskId?: string, error?: string, placement?: 'parent' | 'project' | 'inbox'}> {
206
+ try {
207
+ // Generate AppleScript
208
+ const script = generateAppleScript(params);
209
+ console.error("Executing AppleScript via temp file...");
210
+
211
+ // Write to a temporary AppleScript file to avoid shell escaping issues
212
+ const tempFile = join(tmpdir(), `omnifocus_add_${Date.now()}.applescript`);
213
+ writeFileSync(tempFile, script, { encoding: 'utf8' });
214
+
215
+ // Execute AppleScript from file
216
+ const { stdout, stderr } = await execAsync(`osascript ${tempFile}`);
217
+
218
+ if (stderr) {
219
+ console.error("AppleScript stderr:", stderr);
220
+ }
221
+
222
+ console.error("AppleScript stdout:", stdout);
223
+
224
+ // Cleanup temp file
225
+ try { unlinkSync(tempFile); } catch {}
226
+
227
+ // Parse the result
228
+ try {
229
+ const result = JSON.parse(stdout);
230
+
231
+ // Return the result
232
+ return {
233
+ success: result.success,
234
+ taskId: result.taskId,
235
+ error: result.error,
236
+ placement: result.placement
237
+ };
238
+ } catch (parseError) {
239
+ console.error("Error parsing AppleScript result:", parseError);
240
+ return {
241
+ success: false,
242
+ error: `Failed to parse result: ${stdout}`
243
+ };
244
+ }
245
+ } catch (error: any) {
246
+ console.error("Error in addOmniFocusTask:", error);
247
+ return {
248
+ success: false,
249
+ error: error?.message || "Unknown error in addOmniFocusTask"
250
+ };
251
+ }
252
+ }
@@ -0,0 +1,156 @@
1
+ import { exec } from 'child_process';
2
+ import { promisify } from 'util';
3
+ import { createDateOutsideTellBlock } from '../../utils/dateFormatting.js';
4
+ const execAsync = promisify(exec);
5
+
6
+ // Interface for project creation parameters
7
+ export interface AddProjectParams {
8
+ name: string;
9
+ note?: string;
10
+ dueDate?: string; // ISO date string
11
+ deferDate?: string; // ISO date string
12
+ flagged?: boolean;
13
+ estimatedMinutes?: number;
14
+ tags?: string[]; // Tag names
15
+ folderName?: string; // Folder name to add project to
16
+ sequential?: boolean; // Whether tasks should be sequential or parallel
17
+ }
18
+
19
+ /**
20
+ * Generate pure AppleScript for project creation
21
+ */
22
+ function generateAppleScript(params: AddProjectParams): string {
23
+ // Sanitize and prepare parameters for AppleScript
24
+ const name = params.name.replace(/['"\\]/g, '\\$&'); // Escape quotes and backslashes
25
+ const note = params.note?.replace(/['"\\]/g, '\\$&') || '';
26
+ const dueDate = params.dueDate || '';
27
+ const deferDate = params.deferDate || '';
28
+ const flagged = params.flagged === true;
29
+ const estimatedMinutes = params.estimatedMinutes?.toString() || '';
30
+ const tags = params.tags || [];
31
+ const folderName = params.folderName?.replace(/['"\\]/g, '\\$&') || '';
32
+ const sequential = params.sequential === true;
33
+
34
+ // Generate date constructions outside tell blocks
35
+ let datePreScript = '';
36
+ let dueDateVar = '';
37
+ let deferDateVar = '';
38
+
39
+ if (dueDate) {
40
+ dueDateVar = `dueDate${Math.random().toString(36).substr(2, 9)}`;
41
+ datePreScript += createDateOutsideTellBlock(dueDate, dueDateVar) + '\n\n';
42
+ }
43
+
44
+ if (deferDate) {
45
+ deferDateVar = `deferDate${Math.random().toString(36).substr(2, 9)}`;
46
+ datePreScript += createDateOutsideTellBlock(deferDate, deferDateVar) + '\n\n';
47
+ }
48
+
49
+ // Construct AppleScript with error handling
50
+ let script = datePreScript + `
51
+ try
52
+ tell application "OmniFocus"
53
+ tell front document
54
+ -- Determine the container (root or folder)
55
+ if "${folderName}" is "" then
56
+ -- Create project at the root level
57
+ set newProject to make new project with properties {name:"${name}"}
58
+ else
59
+ -- Use specified folder
60
+ try
61
+ set theFolder to first flattened folder where name = "${folderName}"
62
+ set newProject to make new project with properties {name:"${name}"} at end of projects of theFolder
63
+ on error
64
+ return "{\\\"success\\\":false,\\\"error\\\":\\\"Folder not found: ${folderName}\\\"}"
65
+ end try
66
+ end if
67
+
68
+ -- Set project properties
69
+ ${note ? `set note of newProject to "${note}"` : ''}
70
+ ${dueDate ? `
71
+ -- Set due date
72
+ set due date of newProject to ` + dueDateVar : ''}
73
+ ${deferDate ? `
74
+ -- Set defer date
75
+ set defer date of newProject to ` + deferDateVar : ''}
76
+ ${flagged ? `set flagged of newProject to true` : ''}
77
+ ${estimatedMinutes ? `set estimated minutes of newProject to ${estimatedMinutes}` : ''}
78
+ ${`set sequential of newProject to ${sequential}`}
79
+
80
+ -- Get the project ID
81
+ set projectId to id of newProject as string
82
+
83
+ -- Add tags if provided
84
+ ${tags.length > 0 ? tags.map(tag => {
85
+ const sanitizedTag = tag.replace(/['"\\]/g, '\\$&');
86
+ return `
87
+ try
88
+ set theTag to first flattened tag where name = "${sanitizedTag}"
89
+ add theTag to tags of newProject
90
+ on error
91
+ -- Tag might not exist, try to create it
92
+ try
93
+ set theTag to make new tag with properties {name:"${sanitizedTag}"}
94
+ add theTag to tags of newProject
95
+ on error
96
+ -- Could not create or add tag
97
+ end try
98
+ end try`;
99
+ }).join('\n') : ''}
100
+
101
+ -- Return success with project ID
102
+ return "{\\\"success\\\":true,\\\"projectId\\\":\\"" & projectId & "\\",\\\"name\\\":\\"${name}\\"}"
103
+ end tell
104
+ end tell
105
+ on error errorMessage
106
+ return "{\\\"success\\\":false,\\\"error\\\":\\"" & errorMessage & "\\"}"
107
+ end try
108
+ `;
109
+
110
+ return script;
111
+ }
112
+
113
+ /**
114
+ * Add a project to OmniFocus
115
+ */
116
+ export async function addProject(params: AddProjectParams): Promise<{success: boolean, projectId?: string, error?: string}> {
117
+ try {
118
+ // Generate AppleScript
119
+ const script = generateAppleScript(params);
120
+
121
+ console.error("Executing AppleScript directly...");
122
+
123
+ // Execute AppleScript directly
124
+ const { stdout, stderr } = await execAsync(`osascript -e '${script}'`);
125
+
126
+ if (stderr) {
127
+ console.error("AppleScript stderr:", stderr);
128
+ }
129
+
130
+ console.error("AppleScript stdout:", stdout);
131
+
132
+ // Parse the result
133
+ try {
134
+ const result = JSON.parse(stdout);
135
+
136
+ // Return the result
137
+ return {
138
+ success: result.success,
139
+ projectId: result.projectId,
140
+ error: result.error
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
+ } catch (error: any) {
150
+ console.error("Error in addProject:", error);
151
+ return {
152
+ success: false,
153
+ error: error?.message || "Unknown error in addProject"
154
+ };
155
+ }
156
+ }