@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,443 @@
1
+ import { exec } from 'child_process';
2
+ import { promisify } from 'util';
3
+ import { writeFileSync, unlinkSync } from 'fs';
4
+ import { join } from 'path';
5
+ import { tmpdir } from 'os';
6
+ import { generateDateAssignmentV2 } from '../../utils/dateFormatting.js';
7
+ const execAsync = promisify(exec);
8
+ /**
9
+ * Generate pure AppleScript for item editing with dates constructed outside tell blocks
10
+ */
11
+ function generateAppleScript(params) {
12
+ // Sanitize and prepare parameters for AppleScript
13
+ const id = params.id?.replace(/['"\\]/g, '\\$&') || ''; // Escape quotes and backslashes
14
+ const name = params.name?.replace(/['"\\]/g, '\\$&') || '';
15
+ const itemType = params.itemType;
16
+ // Verify we have at least one identifier
17
+ if (!id && !name) {
18
+ return `return "{\\\"success\\\":false,\\\"error\\\":\\\"Either id or name must be provided\\\"}"`;
19
+ }
20
+ // Collect all date constructions that need to happen outside tell blocks
21
+ const datePreScripts = [];
22
+ const dateAssignments = {};
23
+ // Process due date if provided
24
+ const dueDateParts = generateDateAssignmentV2('foundItem', 'due date', params.newDueDate);
25
+ if (dueDateParts) {
26
+ if (dueDateParts.preScript) {
27
+ datePreScripts.push(dueDateParts.preScript);
28
+ }
29
+ dateAssignments['due date'] = dueDateParts.assignmentScript;
30
+ }
31
+ // Process defer date if provided
32
+ const deferDateParts = generateDateAssignmentV2('foundItem', 'defer date', params.newDeferDate);
33
+ if (deferDateParts) {
34
+ if (deferDateParts.preScript) {
35
+ datePreScripts.push(deferDateParts.preScript);
36
+ }
37
+ dateAssignments['defer date'] = deferDateParts.assignmentScript;
38
+ }
39
+ // Build the complete script
40
+ let script = '';
41
+ // Add date constructions outside tell blocks
42
+ if (datePreScripts.length > 0) {
43
+ script += datePreScripts.join('\n') + '\n\n';
44
+ }
45
+ // Start the main script
46
+ script += `try
47
+ tell application "OmniFocus"
48
+ tell front document
49
+ -- Find the item to edit
50
+ set foundItem to missing value
51
+ `;
52
+ // Add ID search if provided
53
+ if (id) {
54
+ if (itemType === 'task') {
55
+ script += `
56
+ -- Try to find task by ID
57
+ repeat with aTask in (flattened tasks)
58
+ if (id of aTask as string) = "${id}" then
59
+ set foundItem to aTask
60
+ exit repeat
61
+ end if
62
+ end repeat
63
+
64
+ -- If not found in projects, search in inbox
65
+ if foundItem is missing value then
66
+ repeat with aTask in (inbox tasks)
67
+ if (id of aTask as string) = "${id}" then
68
+ set foundItem to aTask
69
+ exit repeat
70
+ end if
71
+ end repeat
72
+ end if
73
+ `;
74
+ }
75
+ else {
76
+ script += `
77
+ -- Try to find project by ID
78
+ repeat with aProject in (flattened projects)
79
+ if (id of aProject as string) = "${id}" then
80
+ set foundItem to aProject
81
+ exit repeat
82
+ end if
83
+ end repeat
84
+ `;
85
+ }
86
+ }
87
+ // Add name search if provided (and no ID or as fallback)
88
+ if (!id && name) {
89
+ if (itemType === 'task') {
90
+ script += `
91
+ -- Find task by name (search in projects first, then inbox)
92
+ repeat with aTask in (flattened tasks)
93
+ if (name of aTask) = "${name}" then
94
+ set foundItem to aTask
95
+ exit repeat
96
+ end if
97
+ end repeat
98
+
99
+ -- If not found in projects, search in inbox
100
+ if foundItem is missing value then
101
+ repeat with aTask in (inbox tasks)
102
+ if (name of aTask) = "${name}" then
103
+ set foundItem to aTask
104
+ exit repeat
105
+ end if
106
+ end repeat
107
+ end if
108
+ `;
109
+ }
110
+ else {
111
+ script += `
112
+ -- Find project by name
113
+ repeat with aProject in (flattened projects)
114
+ if (name of aProject) = "${name}" then
115
+ set foundItem to aProject
116
+ exit repeat
117
+ end if
118
+ end repeat
119
+ `;
120
+ }
121
+ }
122
+ else if (id && name) {
123
+ if (itemType === 'task') {
124
+ script += `
125
+ -- If ID search failed, try to find by name as fallback
126
+ if foundItem is missing value then
127
+ repeat with aTask in (flattened tasks)
128
+ if (name of aTask) = "${name}" then
129
+ set foundItem to aTask
130
+ exit repeat
131
+ end if
132
+ end repeat
133
+ end if
134
+
135
+ -- If still not found, search in inbox
136
+ if foundItem is missing value then
137
+ repeat with aTask in (inbox tasks)
138
+ if (name of aTask) = "${name}" then
139
+ set foundItem to aTask
140
+ exit repeat
141
+ end if
142
+ end repeat
143
+ end if
144
+ `;
145
+ }
146
+ else {
147
+ script += `
148
+ -- If ID search failed, try to find project by name as fallback
149
+ if foundItem is missing value then
150
+ repeat with aProject in (flattened projects)
151
+ if (name of aProject) = "${name}" then
152
+ set foundItem to aProject
153
+ exit repeat
154
+ end if
155
+ end repeat
156
+ end if
157
+ `;
158
+ }
159
+ }
160
+ // Add the item editing logic
161
+ script += `
162
+ -- If we found the item, edit it
163
+ if foundItem is not missing value then
164
+ set itemName to name of foundItem
165
+ set itemId to id of foundItem as string
166
+ set changedProperties to {}
167
+ `;
168
+ // Common property updates for both tasks and projects
169
+ if (params.newName !== undefined) {
170
+ script += `
171
+ -- Update name
172
+ set name of foundItem to "${params.newName.replace(/['"\\]/g, '\\$&')}"
173
+ set end of changedProperties to "name"
174
+ `;
175
+ }
176
+ if (params.newNote !== undefined) {
177
+ script += `
178
+ -- Update note
179
+ set note of foundItem to "${params.newNote.replace(/['"\\]/g, '\\$&')}"
180
+ set end of changedProperties to "note"
181
+ `;
182
+ }
183
+ // Add date assignments (using pre-constructed dates)
184
+ if (dateAssignments['due date']) {
185
+ script += `
186
+ -- Update due date
187
+ ${dateAssignments['due date']}
188
+ set end of changedProperties to "due date"
189
+ `;
190
+ }
191
+ if (dateAssignments['defer date']) {
192
+ script += `
193
+ -- Update defer date
194
+ ${dateAssignments['defer date']}
195
+ set end of changedProperties to "defer date"
196
+ `;
197
+ }
198
+ if (params.newFlagged !== undefined) {
199
+ script += `
200
+ -- Update flagged status
201
+ set flagged of foundItem to ${params.newFlagged}
202
+ set end of changedProperties to "flagged"
203
+ `;
204
+ }
205
+ if (params.newEstimatedMinutes !== undefined) {
206
+ script += `
207
+ -- Update estimated minutes
208
+ set estimated minutes of foundItem to ${params.newEstimatedMinutes}
209
+ set end of changedProperties to "estimated minutes"
210
+ `;
211
+ }
212
+ // Task-specific updates
213
+ if (itemType === 'task') {
214
+ // Update task status
215
+ if (params.newStatus !== undefined) {
216
+ if (params.newStatus === 'completed') {
217
+ script += `
218
+ -- Mark task as completed
219
+ set completed of foundItem to true
220
+ set end of changedProperties to "status (completed)"
221
+ `;
222
+ }
223
+ else if (params.newStatus === 'dropped') {
224
+ script += `
225
+ -- Mark task as dropped
226
+ set dropped of foundItem to true
227
+ set end of changedProperties to "status (dropped)"
228
+ `;
229
+ }
230
+ else if (params.newStatus === 'incomplete') {
231
+ script += `
232
+ -- Mark task as incomplete
233
+ set completed of foundItem to false
234
+ set dropped of foundItem to false
235
+ set end of changedProperties to "status (incomplete)"
236
+ `;
237
+ }
238
+ }
239
+ // Handle tag operations
240
+ if (params.replaceTags && params.replaceTags.length > 0) {
241
+ const tagsList = params.replaceTags.map(tag => `"${tag.replace(/['"\\]/g, '\\$&')}"`).join(", ");
242
+ script += `
243
+ -- Replace all tags
244
+ set tagNames to {${tagsList}}
245
+ set existingTags to tags of foundItem
246
+
247
+ -- First clear all existing tags
248
+ repeat with existingTag in existingTags
249
+ remove existingTag from tags of foundItem
250
+ end repeat
251
+
252
+ -- Then add new tags
253
+ repeat with tagName in tagNames
254
+ set tagObj to missing value
255
+ try
256
+ set tagObj to first flattened tag where name = (tagName as string)
257
+ on error
258
+ -- Tag doesn't exist, create it
259
+ set tagObj to make new tag with properties {name:(tagName as string)}
260
+ end try
261
+ if tagObj is not missing value then
262
+ add tagObj to tags of foundItem
263
+ end if
264
+ end repeat
265
+ set end of changedProperties to "tags (replaced)"
266
+ `;
267
+ }
268
+ else {
269
+ // Add tags if specified
270
+ if (params.addTags && params.addTags.length > 0) {
271
+ const tagsList = params.addTags.map(tag => `"${tag.replace(/['"\\]/g, '\\$&')}"`).join(", ");
272
+ script += `
273
+ -- Add tags
274
+ set tagNames to {${tagsList}}
275
+ repeat with tagName in tagNames
276
+ set tagObj to missing value
277
+ try
278
+ set tagObj to first flattened tag where name = (tagName as string)
279
+ on error
280
+ -- Tag doesn't exist, create it
281
+ set tagObj to make new tag with properties {name:(tagName as string)}
282
+ end try
283
+ if tagObj is not missing value then
284
+ add tagObj to tags of foundItem
285
+ end if
286
+ end repeat
287
+ set end of changedProperties to "tags (added)"
288
+ `;
289
+ }
290
+ // Remove tags if specified
291
+ if (params.removeTags && params.removeTags.length > 0) {
292
+ const tagsList = params.removeTags.map(tag => `"${tag.replace(/['"\\]/g, '\\$&')}"`).join(", ");
293
+ script += `
294
+ -- Remove tags
295
+ set tagNames to {${tagsList}}
296
+ repeat with tagName in tagNames
297
+ try
298
+ set tagObj to first flattened tag where name = (tagName as string)
299
+ remove tagObj from tags of foundItem
300
+ end try
301
+ end repeat
302
+ set end of changedProperties to "tags (removed)"
303
+ `;
304
+ }
305
+ }
306
+ }
307
+ // Project-specific updates
308
+ if (itemType === 'project') {
309
+ // Update sequential status
310
+ if (params.newSequential !== undefined) {
311
+ script += `
312
+ -- Update sequential status
313
+ set sequential of foundItem to ${params.newSequential}
314
+ set end of changedProperties to "sequential"
315
+ `;
316
+ }
317
+ // Update project status
318
+ if (params.newProjectStatus !== undefined) {
319
+ const statusValue = params.newProjectStatus === 'active' ? 'active status' :
320
+ params.newProjectStatus === 'completed' ? 'done status' :
321
+ params.newProjectStatus === 'dropped' ? 'dropped status' :
322
+ 'on hold status';
323
+ script += `
324
+ -- Update project status
325
+ set status of foundItem to ${statusValue}
326
+ set end of changedProperties to "status"
327
+ `;
328
+ }
329
+ // Move to a new folder
330
+ if (params.newFolderName !== undefined) {
331
+ const folderName = params.newFolderName.replace(/['"\\]/g, '\\$&');
332
+ script += `
333
+ -- Move to new folder
334
+ set destFolder to missing value
335
+ try
336
+ set destFolder to first flattened folder where name = "${folderName}"
337
+ end try
338
+
339
+ if destFolder is missing value then
340
+ -- Create the folder if it doesn't exist
341
+ set destFolder to make new folder with properties {name:"${folderName}"}
342
+ end if
343
+
344
+ -- Move project to the folder
345
+ move foundItem to destFolder
346
+ set end of changedProperties to "folder"
347
+ `;
348
+ }
349
+ }
350
+ script += `
351
+ -- Prepare the changed properties as a string
352
+ set changedPropsText to ""
353
+ repeat with i from 1 to count of changedProperties
354
+ set changedPropsText to changedPropsText & item i of changedProperties
355
+ if i < count of changedProperties then
356
+ set changedPropsText to changedPropsText & ", "
357
+ end if
358
+ end repeat
359
+
360
+ -- Return success with details
361
+ return "{\\\"success\\\":true,\\\"id\\\":\\"" & itemId & "\\",\\\"name\\\":\\"" & itemName & "\\",\\\"changedProperties\\\":\\"" & changedPropsText & "\\"}"
362
+ else
363
+ -- Item not found
364
+ return "{\\\"success\\\":false,\\\"error\\\":\\\"Item not found\\"}"
365
+ end if
366
+ end tell
367
+ end tell
368
+ on error errorMessage
369
+ return "{\\\"success\\\":false,\\\"error\\\":\\"" & errorMessage & "\\"}"
370
+ end try
371
+ `;
372
+ return script;
373
+ }
374
+ /**
375
+ * Edit a task or project in OmniFocus
376
+ */
377
+ export async function editItem(params) {
378
+ let tempFile;
379
+ try {
380
+ // Generate AppleScript
381
+ const script = generateAppleScript(params);
382
+ console.error("Executing AppleScript for editing (V2)...");
383
+ console.error(`Item type: ${params.itemType}, ID: ${params.id || 'not provided'}, Name: ${params.name || 'not provided'}`);
384
+ // Log a preview of the script for debugging (first few lines)
385
+ const scriptPreview = script.split('\n').slice(0, 10).join('\n') + '\n...';
386
+ console.error("AppleScript preview:\n", scriptPreview);
387
+ // Write script to temporary file to avoid shell escaping issues
388
+ tempFile = join(tmpdir(), `edit_omnifocus_${Date.now()}.applescript`);
389
+ writeFileSync(tempFile, script);
390
+ // Execute AppleScript from file
391
+ const { stdout, stderr } = await execAsync(`osascript ${tempFile}`);
392
+ // Clean up temp file
393
+ try {
394
+ unlinkSync(tempFile);
395
+ }
396
+ catch (cleanupError) {
397
+ console.error("Failed to clean up temp file:", cleanupError);
398
+ }
399
+ if (stderr) {
400
+ console.error("AppleScript stderr:", stderr);
401
+ }
402
+ console.error("AppleScript stdout:", stdout);
403
+ // Parse the result
404
+ try {
405
+ const result = JSON.parse(stdout);
406
+ // Return the result
407
+ return {
408
+ success: result.success,
409
+ id: result.id,
410
+ name: result.name,
411
+ changedProperties: result.changedProperties,
412
+ error: result.error
413
+ };
414
+ }
415
+ catch (parseError) {
416
+ console.error("Error parsing AppleScript result:", parseError);
417
+ return {
418
+ success: false,
419
+ error: `Failed to parse result: ${stdout}`
420
+ };
421
+ }
422
+ }
423
+ catch (error) {
424
+ // Clean up temp file if it exists
425
+ if (tempFile) {
426
+ try {
427
+ unlinkSync(tempFile);
428
+ }
429
+ catch (cleanupError) {
430
+ // Ignore cleanup errors
431
+ }
432
+ }
433
+ console.error("Error in editItem execution:", error);
434
+ // Include more detailed error information
435
+ if (error.message && error.message.includes('syntax error')) {
436
+ console.error("This appears to be an AppleScript syntax error. Review the script generation logic.");
437
+ }
438
+ return {
439
+ success: false,
440
+ error: error?.message || "Unknown error in editItem"
441
+ };
442
+ }
443
+ }
@@ -0,0 +1,50 @@
1
+ import { executeOmniFocusScript } from '../../utils/scriptExecution.js';
2
+ export async function getPerspectiveView(params) {
3
+ const { perspectiveName, limit = 100, includeMetadata = true, fields } = params;
4
+ try {
5
+ // Execute the OmniJS script to get perspective view
6
+ // Note: This gets the current perspective view, not a specific one
7
+ // OmniJS doesn't easily allow switching perspectives
8
+ const result = await executeOmniFocusScript('@getPerspectiveView.js');
9
+ if (result.error) {
10
+ return {
11
+ success: false,
12
+ error: result.error
13
+ };
14
+ }
15
+ // Check if the current perspective matches what was requested
16
+ const currentPerspective = result.perspectiveName;
17
+ if (currentPerspective && currentPerspective.toLowerCase() !== perspectiveName.toLowerCase()) {
18
+ console.warn(`Note: Current perspective is "${currentPerspective}", not "${perspectiveName}". OmniJS cannot easily switch perspectives.`);
19
+ }
20
+ // Filter and limit items
21
+ let items = result.items || [];
22
+ // Apply field filtering if specified
23
+ if (fields && fields.length > 0) {
24
+ items = items.map((item) => {
25
+ const filtered = {};
26
+ fields.forEach(field => {
27
+ if (item.hasOwnProperty(field)) {
28
+ filtered[field] = item[field];
29
+ }
30
+ });
31
+ return filtered;
32
+ });
33
+ }
34
+ // Apply limit
35
+ if (limit && items.length > limit) {
36
+ items = items.slice(0, limit);
37
+ }
38
+ return {
39
+ success: true,
40
+ items: items
41
+ };
42
+ }
43
+ catch (error) {
44
+ console.error('Error getting perspective view:', error);
45
+ return {
46
+ success: false,
47
+ error: error instanceof Error ? error.message : 'Unknown error occurred'
48
+ };
49
+ }
50
+ }
@@ -0,0 +1,34 @@
1
+ import { executeOmniFocusScript } from '../../utils/scriptExecution.js';
2
+ export async function listPerspectives(params = {}) {
3
+ const { includeBuiltIn = true, includeCustom = true } = params;
4
+ try {
5
+ // Execute the OmniJS script to list perspectives
6
+ // This uses the built-in OmniFocus JavaScript API
7
+ const result = await executeOmniFocusScript('@listPerspectives.js');
8
+ if (result.error) {
9
+ return {
10
+ success: false,
11
+ error: result.error
12
+ };
13
+ }
14
+ // Filter perspectives based on parameters
15
+ let perspectives = result.perspectives || [];
16
+ if (!includeBuiltIn) {
17
+ perspectives = perspectives.filter((p) => p.type !== 'builtin');
18
+ }
19
+ if (!includeCustom) {
20
+ perspectives = perspectives.filter((p) => p.type !== 'custom');
21
+ }
22
+ return {
23
+ success: true,
24
+ perspectives: perspectives
25
+ };
26
+ }
27
+ catch (error) {
28
+ console.error('Error listing perspectives:', error);
29
+ return {
30
+ success: false,
31
+ error: error instanceof Error ? error.message : 'Unknown error occurred'
32
+ };
33
+ }
34
+ }