@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,394 @@
1
+ import { executeOmniFocusScript } from '../../utils/scriptExecution.js';
2
+
3
+ export interface QueryOmnifocusParams {
4
+ entity: 'tasks' | 'projects' | 'folders';
5
+ filters?: {
6
+ projectId?: string;
7
+ projectName?: string;
8
+ folderId?: string;
9
+ tags?: string[];
10
+ status?: string[];
11
+ flagged?: boolean;
12
+ dueWithin?: number;
13
+ deferredUntil?: number;
14
+ hasNote?: boolean;
15
+ };
16
+ fields?: string[];
17
+ limit?: number;
18
+ sortBy?: string;
19
+ sortOrder?: 'asc' | 'desc';
20
+ includeCompleted?: boolean;
21
+ summary?: boolean;
22
+ }
23
+
24
+ interface QueryResult {
25
+ success: boolean;
26
+ items?: any[];
27
+ count?: number;
28
+ error?: string;
29
+ }
30
+
31
+ export async function queryOmnifocus(params: QueryOmnifocusParams): Promise<QueryResult> {
32
+ try {
33
+ // Create JXA script for the query
34
+ const jxaScript = generateQueryScript(params);
35
+
36
+ // Write script to temp file and execute
37
+ const tempFile = `/tmp/omnifocus_query_${Date.now()}.js`;
38
+ const fs = await import('fs');
39
+ fs.writeFileSync(tempFile, jxaScript);
40
+
41
+ // Execute the script
42
+ const result = await executeOmniFocusScript(tempFile);
43
+
44
+ // Clean up temp file
45
+ fs.unlinkSync(tempFile);
46
+
47
+ if (result.error) {
48
+ return {
49
+ success: false,
50
+ error: result.error
51
+ };
52
+ }
53
+
54
+ return {
55
+ success: true,
56
+ items: params.summary ? undefined : result.items,
57
+ count: result.count
58
+ };
59
+ } catch (error) {
60
+ console.error('Error querying OmniFocus:', error);
61
+ return {
62
+ success: false,
63
+ error: error instanceof Error ? error.message : 'Unknown error occurred'
64
+ };
65
+ }
66
+ }
67
+
68
+ function generateQueryScript(params: QueryOmnifocusParams): string {
69
+ const { entity, filters = {}, fields, limit, sortBy, sortOrder, includeCompleted = false, summary = false } = params;
70
+
71
+ // Build the JXA script based on the entity type and filters
72
+ return `(() => {
73
+ try {
74
+ const startTime = new Date();
75
+
76
+ // Helper function to format dates
77
+ function formatDate(date) {
78
+ if (!date) return null;
79
+ return date.toISOString();
80
+ }
81
+
82
+ // Helper to check date filters
83
+ function checkDateFilter(itemDate, daysFromNow) {
84
+ if (!itemDate) return false;
85
+ const futureDate = new Date();
86
+ futureDate.setDate(futureDate.getDate() + daysFromNow);
87
+ return itemDate <= futureDate;
88
+ }
89
+
90
+ // Status mappings
91
+ const taskStatusMap = {
92
+ [Task.Status.Available]: "Available",
93
+ [Task.Status.Blocked]: "Blocked",
94
+ [Task.Status.Completed]: "Completed",
95
+ [Task.Status.Dropped]: "Dropped",
96
+ [Task.Status.DueSoon]: "DueSoon",
97
+ [Task.Status.Next]: "Next",
98
+ [Task.Status.Overdue]: "Overdue"
99
+ };
100
+
101
+ const projectStatusMap = {
102
+ [Project.Status.Active]: "Active",
103
+ [Project.Status.Done]: "Done",
104
+ [Project.Status.Dropped]: "Dropped",
105
+ [Project.Status.OnHold]: "OnHold"
106
+ };
107
+
108
+ // Get the appropriate collection based on entity type
109
+ let items = [];
110
+ const entityType = "${entity}";
111
+
112
+ if (entityType === "tasks") {
113
+ items = flattenedTasks;
114
+ } else if (entityType === "projects") {
115
+ items = flattenedProjects;
116
+ } else if (entityType === "folders") {
117
+ items = flattenedFolders;
118
+ }
119
+
120
+ // Apply filters
121
+ let filtered = items.filter(item => {
122
+ // Skip completed/dropped unless explicitly requested
123
+ if (!${includeCompleted}) {
124
+ if (entityType === "tasks") {
125
+ if (item.taskStatus === Task.Status.Completed ||
126
+ item.taskStatus === Task.Status.Dropped) {
127
+ return false;
128
+ }
129
+ } else if (entityType === "projects") {
130
+ if (item.status === Project.Status.Done ||
131
+ item.status === Project.Status.Dropped) {
132
+ return false;
133
+ }
134
+ }
135
+ }
136
+
137
+ // Apply specific filters
138
+ ${generateFilterConditions(entity, filters)}
139
+
140
+ return true;
141
+ });
142
+
143
+ // Apply sorting if specified
144
+ ${sortBy ? generateSortLogic(sortBy, sortOrder) : ''}
145
+
146
+ // Apply limit if specified
147
+ ${limit ? `filtered = filtered.slice(0, ${limit});` : ''}
148
+
149
+ // If summary mode, just return count
150
+ if (${summary}) {
151
+ return JSON.stringify({
152
+ count: filtered.length,
153
+ error: null
154
+ });
155
+ }
156
+
157
+ // Transform items to return only requested fields
158
+ const results = filtered.map(item => {
159
+ ${generateFieldMapping(entity, fields)}
160
+ });
161
+
162
+ return JSON.stringify({
163
+ items: results,
164
+ count: results.length,
165
+ error: null
166
+ });
167
+
168
+ } catch (error) {
169
+ return JSON.stringify({
170
+ error: "Script execution error: " + error.toString(),
171
+ items: [],
172
+ count: 0
173
+ });
174
+ }
175
+ })();`;
176
+ }
177
+
178
+ function generateFilterConditions(entity: string, filters: any): string {
179
+ const conditions: string[] = [];
180
+
181
+ if (entity === 'tasks') {
182
+ if (filters.projectName) {
183
+ conditions.push(`
184
+ if (item.containingProject) {
185
+ const projectName = item.containingProject.name.toLowerCase();
186
+ if (!projectName.includes("${filters.projectName.toLowerCase()}")) return false;
187
+ } else if ("${filters.projectName.toLowerCase()}" !== "inbox") {
188
+ return false;
189
+ }
190
+ `);
191
+ }
192
+
193
+ if (filters.projectId) {
194
+ conditions.push(`
195
+ if (!item.containingProject ||
196
+ item.containingProject.id.primaryKey !== "${filters.projectId}") {
197
+ return false;
198
+ }
199
+ `);
200
+ }
201
+
202
+ if (filters.tags && filters.tags.length > 0) {
203
+ const tagCondition = filters.tags.map((tag: string) =>
204
+ `item.tags.some(t => t.name === "${tag}")`
205
+ ).join(' || ');
206
+ conditions.push(`if (!(${tagCondition})) return false;`);
207
+ }
208
+
209
+ if (filters.status && filters.status.length > 0) {
210
+ const statusCondition = filters.status.map((status: string) =>
211
+ `taskStatusMap[item.taskStatus] === "${status}"`
212
+ ).join(' || ');
213
+ conditions.push(`if (!(${statusCondition})) return false;`);
214
+ }
215
+
216
+ if (filters.flagged !== undefined) {
217
+ conditions.push(`if (item.flagged !== ${filters.flagged}) return false;`);
218
+ }
219
+
220
+ if (filters.dueWithin !== undefined) {
221
+ conditions.push(`
222
+ if (!item.dueDate || !checkDateFilter(item.dueDate, ${filters.dueWithin})) {
223
+ return false;
224
+ }
225
+ `);
226
+ }
227
+
228
+ if (filters.hasNote !== undefined) {
229
+ conditions.push(`
230
+ const hasNote = item.note && item.note.trim().length > 0;
231
+ if (hasNote !== ${filters.hasNote}) return false;
232
+ `);
233
+ }
234
+ }
235
+
236
+ if (entity === 'projects') {
237
+ if (filters.folderId) {
238
+ conditions.push(`
239
+ if (!item.parentFolder ||
240
+ item.parentFolder.id.primaryKey !== "${filters.folderId}") {
241
+ return false;
242
+ }
243
+ `);
244
+ }
245
+
246
+ if (filters.status && filters.status.length > 0) {
247
+ const statusCondition = filters.status.map((status: string) =>
248
+ `projectStatusMap[item.status] === "${status}"`
249
+ ).join(' || ');
250
+ conditions.push(`if (!(${statusCondition})) return false;`);
251
+ }
252
+ }
253
+
254
+ return conditions.join('\n');
255
+ }
256
+
257
+ function generateSortLogic(sortBy: string, sortOrder?: string): string {
258
+ const order = sortOrder === 'desc' ? -1 : 1;
259
+
260
+ return `
261
+ filtered.sort((a, b) => {
262
+ let aVal = a.${sortBy};
263
+ let bVal = b.${sortBy};
264
+
265
+ // Handle null/undefined values
266
+ if (aVal == null && bVal == null) return 0;
267
+ if (aVal == null) return 1;
268
+ if (bVal == null) return -1;
269
+
270
+ // Compare based on type
271
+ if (typeof aVal === 'string') {
272
+ return aVal.localeCompare(bVal) * ${order};
273
+ } else if (aVal instanceof Date) {
274
+ return (aVal.getTime() - bVal.getTime()) * ${order};
275
+ } else {
276
+ return (aVal - bVal) * ${order};
277
+ }
278
+ });
279
+ `;
280
+ }
281
+
282
+ function generateFieldMapping(entity: string, fields?: string[]): string {
283
+ // If no specific fields requested, return common fields based on entity
284
+ if (!fields || fields.length === 0) {
285
+ if (entity === 'tasks') {
286
+ return `
287
+ const obj = {
288
+ id: item.id.primaryKey,
289
+ name: item.name || "",
290
+ flagged: item.flagged || false,
291
+ taskStatus: taskStatusMap[item.taskStatus] || "Unknown",
292
+ dueDate: formatDate(item.dueDate),
293
+ deferDate: formatDate(item.deferDate),
294
+ tagNames: item.tags ? item.tags.map(t => t.name) : [],
295
+ projectName: item.containingProject ? item.containingProject.name : (item.inInbox ? "Inbox" : null),
296
+ estimatedMinutes: item.estimatedMinutes || null
297
+ };
298
+ if (item.note && item.note.trim()) obj.note = item.note;
299
+ return obj;
300
+ `;
301
+ } else if (entity === 'projects') {
302
+ return `
303
+ const taskArray = item.tasks || [];
304
+ return {
305
+ id: item.id.primaryKey,
306
+ name: item.name || "",
307
+ status: projectStatusMap[item.status] || "Unknown",
308
+ folderName: item.parentFolder ? item.parentFolder.name : null,
309
+ taskCount: taskArray.length,
310
+ flagged: item.flagged || false,
311
+ dueDate: formatDate(item.dueDate),
312
+ deferDate: formatDate(item.deferDate)
313
+ };
314
+ `;
315
+ } else if (entity === 'folders') {
316
+ return `
317
+ const projectArray = item.projects || [];
318
+ return {
319
+ id: item.id.primaryKey,
320
+ name: item.name || "",
321
+ projectCount: projectArray.length,
322
+ path: item.container ? item.container.name + "/" + item.name : item.name
323
+ };
324
+ `;
325
+ }
326
+ }
327
+
328
+ // Generate mapping for specific fields
329
+ const mappings = fields!.map(field => {
330
+ // Handle special field mappings based on entity type
331
+ if (field === 'id') {
332
+ return `id: item.id.primaryKey`;
333
+ } else if (field === 'taskStatus') {
334
+ return `taskStatus: taskStatusMap[item.taskStatus]`;
335
+ } else if (field === 'status') {
336
+ return `status: projectStatusMap[item.status]`;
337
+ } else if (field === 'modificationDate' || field === 'modified') {
338
+ return `modificationDate: formatDate(item.modified)`;
339
+ } else if (field === 'creationDate' || field === 'added') {
340
+ return `creationDate: formatDate(item.added)`;
341
+ } else if (field === 'completionDate') {
342
+ return `completionDate: item.completionDate ? formatDate(item.completionDate) : null`;
343
+ } else if (field === 'dueDate') {
344
+ return `dueDate: formatDate(item.dueDate)`;
345
+ } else if (field === 'deferDate') {
346
+ return `deferDate: formatDate(item.deferDate)`;
347
+ } else if (field === 'effectiveDueDate') {
348
+ return `effectiveDueDate: formatDate(item.effectiveDueDate)`;
349
+ } else if (field === 'effectiveDeferDate') {
350
+ return `effectiveDeferDate: formatDate(item.effectiveDeferDate)`;
351
+ } else if (field === 'tagNames') {
352
+ return `tagNames: item.tags ? item.tags.map(t => t.name) : []`;
353
+ } else if (field === 'tags') {
354
+ return `tags: item.tags ? item.tags.map(t => t.id.primaryKey) : []`;
355
+ } else if (field === 'projectName') {
356
+ return `projectName: item.containingProject ? item.containingProject.name : (item.inInbox ? "Inbox" : null)`;
357
+ } else if (field === 'projectId') {
358
+ return `projectId: item.containingProject ? item.containingProject.id.primaryKey : null`;
359
+ } else if (field === 'parentId') {
360
+ return `parentId: item.parent ? item.parent.id.primaryKey : null`;
361
+ } else if (field === 'childIds') {
362
+ return `childIds: item.children ? item.children.map(c => c.id.primaryKey) : []`;
363
+ } else if (field === 'hasChildren') {
364
+ return `hasChildren: item.children ? item.children.length > 0 : false`;
365
+ } else if (field === 'folderName') {
366
+ return `folderName: item.parentFolder ? item.parentFolder.name : null`;
367
+ } else if (field === 'folderID') {
368
+ return `folderID: item.parentFolder ? item.parentFolder.id.primaryKey : null`;
369
+ } else if (field === 'taskCount') {
370
+ return `taskCount: item.tasks ? item.tasks.length : 0`;
371
+ } else if (field === 'tasks') {
372
+ return `tasks: item.tasks ? item.tasks.map(t => t.id.primaryKey) : []`;
373
+ } else if (field === 'projectCount') {
374
+ return `projectCount: item.projects ? item.projects.length : 0`;
375
+ } else if (field === 'projects') {
376
+ return `projects: item.projects ? item.projects.map(p => p.id.primaryKey) : []`;
377
+ } else if (field === 'subfolders') {
378
+ return `subfolders: item.folders ? item.folders.map(f => f.id.primaryKey) : []`;
379
+ } else if (field === 'path') {
380
+ return `path: item.container ? item.container.name + "/" + item.name : item.name`;
381
+ } else if (field === 'estimatedMinutes') {
382
+ return `estimatedMinutes: item.estimatedMinutes || null`;
383
+ } else {
384
+ // Default: try to access the field directly
385
+ return `${field}: item.${field} !== undefined ? item.${field} : null`;
386
+ }
387
+ }).join(',\n ');
388
+
389
+ return `
390
+ return {
391
+ ${mappings}
392
+ };
393
+ `;
394
+ }
@@ -0,0 +1,139 @@
1
+ import { executeOmniFocusScript } from '../../utils/scriptExecution.js';
2
+
3
+ /**
4
+ * Debug version of queryOmnifocus that returns raw field information
5
+ * Useful for understanding what fields are available in OmniFocus
6
+ */
7
+ export async function queryOmnifocusDebug(entity: 'task' | 'project' | 'folder'): Promise<any> {
8
+ const script = `
9
+ (() => {
10
+ try {
11
+ // Get first item of the requested type
12
+ let item;
13
+ const entityType = "${entity}";
14
+
15
+ if (entityType === "task") {
16
+ item = flattenedTasks[0];
17
+ } else if (entityType === "project") {
18
+ item = flattenedProjects[0];
19
+ } else if (entityType === "folder") {
20
+ item = flattenedFolders[0];
21
+ }
22
+
23
+ if (!item) {
24
+ return JSON.stringify({ error: "No items found" });
25
+ }
26
+
27
+ // Get all properties of the item
28
+ const properties = {};
29
+ const skipProps = ['constructor', 'toString', 'valueOf'];
30
+
31
+ for (let prop in item) {
32
+ if (skipProps.includes(prop)) continue;
33
+
34
+ try {
35
+ const value = item[prop];
36
+ const valueType = typeof value;
37
+
38
+ if (value === null) {
39
+ properties[prop] = { type: 'null', value: null };
40
+ } else if (value === undefined) {
41
+ properties[prop] = { type: 'undefined', value: undefined };
42
+ } else if (valueType === 'function') {
43
+ properties[prop] = { type: 'function', value: '[Function]' };
44
+ } else if (value instanceof Date) {
45
+ properties[prop] = { type: 'Date', value: value.toISOString() };
46
+ } else if (Array.isArray(value)) {
47
+ properties[prop] = {
48
+ type: 'Array',
49
+ length: value.length,
50
+ sample: value.length > 0 ? value[0] : null
51
+ };
52
+ } else if (valueType === 'object') {
53
+ // Try to get ID if it's an OmniFocus object
54
+ if (value.id && value.id.primaryKey) {
55
+ properties[prop] = {
56
+ type: 'OFObject',
57
+ id: value.id.primaryKey,
58
+ name: value.name || null
59
+ };
60
+ } else {
61
+ properties[prop] = { type: 'object', keys: Object.keys(value) };
62
+ }
63
+ } else {
64
+ properties[prop] = { type: valueType, value: value };
65
+ }
66
+ } catch (e) {
67
+ properties[prop] = { type: 'error', error: e.toString() };
68
+ }
69
+ }
70
+
71
+ // Also check specific expected properties
72
+ const checkProps = [
73
+ 'id', 'name', 'note', 'flagged', 'dueDate', 'deferDate',
74
+ 'estimatedMinutes', 'modificationDate', 'creationDate',
75
+ 'completionDate', 'taskStatus', 'status', 'tasks', 'projects',
76
+ 'containingProject', 'parentFolder', 'parent', 'children'
77
+ ];
78
+
79
+ const expectedProps = {};
80
+ checkProps.forEach(prop => {
81
+ try {
82
+ const value = item[prop];
83
+ if (value !== undefined) {
84
+ if (value && value.id && value.id.primaryKey) {
85
+ expectedProps[prop] = {
86
+ exists: true,
87
+ type: 'OFObject',
88
+ id: value.id.primaryKey
89
+ };
90
+ } else if (value instanceof Date) {
91
+ expectedProps[prop] = {
92
+ exists: true,
93
+ type: 'Date',
94
+ value: value.toISOString()
95
+ };
96
+ } else if (Array.isArray(value)) {
97
+ expectedProps[prop] = {
98
+ exists: true,
99
+ type: 'Array',
100
+ length: value.length
101
+ };
102
+ } else {
103
+ expectedProps[prop] = {
104
+ exists: true,
105
+ type: typeof value,
106
+ value: value
107
+ };
108
+ }
109
+ } else {
110
+ expectedProps[prop] = { exists: false };
111
+ }
112
+ } catch (e) {
113
+ expectedProps[prop] = { exists: false, error: e.toString() };
114
+ }
115
+ });
116
+
117
+ return JSON.stringify({
118
+ entityType: entityType,
119
+ itemName: item.name || 'Unnamed',
120
+ allProperties: properties,
121
+ expectedProperties: expectedProps
122
+ }, null, 2);
123
+
124
+ } catch (error) {
125
+ return JSON.stringify({ error: error.toString() });
126
+ }
127
+ })();
128
+ `;
129
+
130
+ // Write script to temp file and execute
131
+ const fs = await import('fs');
132
+ const tempFile = `/tmp/omnifocus_debug_${Date.now()}.js`;
133
+ fs.writeFileSync(tempFile, script);
134
+
135
+ const result = await executeOmniFocusScript(tempFile);
136
+ fs.unlinkSync(tempFile);
137
+
138
+ return result;
139
+ }