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