@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,190 @@
1
+ import { z } from 'zod';
2
+ import { queryOmnifocus } from '../primitives/queryOmnifocus.js';
3
+ export const schema = z.object({
4
+ entity: z.enum(['tasks', 'projects', 'folders']).describe("Type of entity to query. Choose 'tasks' for individual tasks, 'projects' for projects, or 'folders' for folder organization"),
5
+ filters: z.object({
6
+ projectId: z.string().optional().describe("Filter tasks by exact project ID (use when you know the specific project ID)"),
7
+ projectName: z.string().optional().describe("Filter tasks by project name. CASE-INSENSITIVE PARTIAL MATCHING - 'review' matches 'Weekly Review', 'Review Documents', etc. Special value: 'inbox' returns inbox tasks"),
8
+ folderId: z.string().optional().describe("Filter projects by exact folder ID (use when you know the specific folder ID)"),
9
+ tags: z.array(z.string()).optional().describe("Filter by tag names. EXACT MATCH, CASE-SENSITIVE. OR logic - items must have at least ONE of the specified tags. Example: ['Work'] and ['work'] are different"),
10
+ status: z.array(z.string()).optional().describe("Filter by status (OR logic - matches any). TASKS: 'Next' (next action), 'Available' (ready to work), 'Blocked' (waiting), 'DueSoon' (due <24h), 'Overdue' (past due), 'Completed', 'Dropped'. PROJECTS: 'Active', 'OnHold', 'Done', 'Dropped'"),
11
+ flagged: z.boolean().optional().describe("Filter by flagged status. true = only flagged items, false = only unflagged items"),
12
+ dueWithin: z.number().optional().describe("Returns items due from TODAY through N days in future. Example: 7 = items due within next week (today + 6 days)"),
13
+ deferredUntil: z.number().optional().describe("Returns items CURRENTLY DEFERRED that will become available within N days. Example: 3 = items becoming available in next 3 days"),
14
+ hasNote: z.boolean().optional().describe("Filter by note presence. true = items with non-empty notes (whitespace ignored), false = items with no notes or only whitespace")
15
+ }).optional().describe("Optional filters to narrow results. ALL filters combine with AND logic (must match all). Within array filters (tags, status) OR logic applies"),
16
+ fields: z.array(z.string()).optional().describe("Specific fields to return (reduces response size). TASK FIELDS: id, name, note, flagged, taskStatus, dueDate, deferDate, completionDate, estimatedMinutes, tagNames, tags, projectName, projectId, parentId, childIds, hasChildren, sequential, completedByChildren, inInbox, modificationDate (or modified), creationDate (or added). PROJECT FIELDS: id, name, status, note, folderName, folderID, sequential, dueDate, deferDate, effectiveDueDate, effectiveDeferDate, completedByChildren, containsSingletonActions, taskCount, tasks, modificationDate, creationDate. FOLDER FIELDS: id, name, path, parentFolderID, status, projectCount, projects, subfolders. NOTE: Date fields use 'added' and 'modified' in OmniFocus API"),
17
+ limit: z.number().optional().describe("Maximum number of items to return. Useful for large result sets. Default: no limit"),
18
+ sortBy: z.string().optional().describe("Field to sort by. OPTIONS: name (alphabetical), dueDate (earliest first, null last), deferDate (earliest first, null last), modificationDate (most recent first), creationDate (oldest first), estimatedMinutes (shortest first), taskStatus (groups by status)"),
19
+ sortOrder: z.enum(['asc', 'desc']).optional().describe("Sort order. 'asc' = ascending (A-Z, old-new, small-large), 'desc' = descending (Z-A, new-old, large-small). Default: 'asc'"),
20
+ includeCompleted: z.boolean().optional().describe("Include completed and dropped items. Default: false (active items only)"),
21
+ summary: z.boolean().optional().describe("Return only count of matches, not full details. Efficient for statistics. Default: false")
22
+ });
23
+ export async function handler(args, extra) {
24
+ try {
25
+ // Call the queryOmniFocus function
26
+ const result = await queryOmnifocus(args);
27
+ if (result.success) {
28
+ // Format response based on whether it's a summary or full results
29
+ if (args.summary) {
30
+ return {
31
+ content: [{
32
+ type: "text",
33
+ text: `Found ${result.count} ${args.entity} matching your criteria.`
34
+ }]
35
+ };
36
+ }
37
+ else {
38
+ // Format the results in a compact, readable format
39
+ const items = result.items || [];
40
+ let output = formatQueryResults(items, args.entity, args.filters);
41
+ // Add metadata about the query
42
+ if (items.length === args.limit) {
43
+ output += `\n\n⚠️ Results limited to ${args.limit} items. More may be available.`;
44
+ }
45
+ return {
46
+ content: [{
47
+ type: "text",
48
+ text: output
49
+ }]
50
+ };
51
+ }
52
+ }
53
+ else {
54
+ return {
55
+ content: [{
56
+ type: "text",
57
+ text: `Query failed: ${result.error}`
58
+ }],
59
+ isError: true
60
+ };
61
+ }
62
+ }
63
+ catch (err) {
64
+ const error = err;
65
+ console.error(`Query execution error: ${error.message}`);
66
+ return {
67
+ content: [{
68
+ type: "text",
69
+ text: `Error executing query: ${error.message}`
70
+ }],
71
+ isError: true
72
+ };
73
+ }
74
+ }
75
+ // Helper function to format query results in a compact way
76
+ function formatQueryResults(items, entity, filters) {
77
+ if (items.length === 0) {
78
+ return `No ${entity} found matching the specified criteria.`;
79
+ }
80
+ let output = `## Query Results: ${items.length} ${entity}\n\n`;
81
+ // Add filter summary if filters were applied
82
+ if (filters && Object.keys(filters).length > 0) {
83
+ output += `Filters applied: ${formatFilters(filters)}\n\n`;
84
+ }
85
+ // Format each item based on entity type
86
+ switch (entity) {
87
+ case 'tasks':
88
+ output += formatTasks(items);
89
+ break;
90
+ case 'projects':
91
+ output += formatProjects(items);
92
+ break;
93
+ case 'folders':
94
+ output += formatFolders(items);
95
+ break;
96
+ }
97
+ return output;
98
+ }
99
+ function formatFilters(filters) {
100
+ const parts = [];
101
+ if (filters.projectId)
102
+ parts.push(`projectId: "${filters.projectId}"`);
103
+ if (filters.projectName)
104
+ parts.push(`project: "${filters.projectName}"`);
105
+ if (filters.folderId)
106
+ parts.push(`folderId: "${filters.folderId}"`);
107
+ if (filters.tags)
108
+ parts.push(`tags: [${filters.tags.join(', ')}]`);
109
+ if (filters.status)
110
+ parts.push(`status: [${filters.status.join(', ')}]`);
111
+ if (filters.flagged !== undefined)
112
+ parts.push(`flagged: ${filters.flagged}`);
113
+ if (filters.dueWithin)
114
+ parts.push(`due within ${filters.dueWithin} days`);
115
+ if (filters.deferredUntil)
116
+ parts.push(`deferred becoming available within ${filters.deferredUntil} days`);
117
+ if (filters.hasNote !== undefined)
118
+ parts.push(`has note: ${filters.hasNote}`);
119
+ return parts.join(', ');
120
+ }
121
+ function formatTasks(tasks) {
122
+ return tasks.map(task => {
123
+ const parts = [];
124
+ // Core display
125
+ const flag = task.flagged ? '🚩 ' : '';
126
+ parts.push(`• ${flag}${task.name || 'Unnamed'}`);
127
+ // Add ID if present
128
+ if (task.id) {
129
+ parts.push(`[${task.id}]`);
130
+ }
131
+ // Project context
132
+ if (task.projectName) {
133
+ parts.push(`(${task.projectName})`);
134
+ }
135
+ // Dates
136
+ if (task.dueDate) {
137
+ parts.push(`[due: ${formatDate(task.dueDate)}]`);
138
+ }
139
+ if (task.deferDate) {
140
+ parts.push(`[defer: ${formatDate(task.deferDate)}]`);
141
+ }
142
+ // Time estimate
143
+ if (task.estimatedMinutes) {
144
+ const hours = task.estimatedMinutes >= 60
145
+ ? `${Math.floor(task.estimatedMinutes / 60)}h`
146
+ : `${task.estimatedMinutes}m`;
147
+ parts.push(`(${hours})`);
148
+ }
149
+ // Tags
150
+ if (task.tagNames?.length > 0) {
151
+ parts.push(`<${task.tagNames.join(',')}>`);
152
+ }
153
+ // Status
154
+ if (task.taskStatus) {
155
+ parts.push(`#${task.taskStatus.toLowerCase()}`);
156
+ }
157
+ // Metadata dates if requested
158
+ if (task.creationDate) {
159
+ parts.push(`[created: ${formatDate(task.creationDate)}]`);
160
+ }
161
+ if (task.modificationDate) {
162
+ parts.push(`[modified: ${formatDate(task.modificationDate)}]`);
163
+ }
164
+ if (task.completionDate) {
165
+ parts.push(`[completed: ${formatDate(task.completionDate)}]`);
166
+ }
167
+ return parts.join(' ');
168
+ }).join('\n');
169
+ }
170
+ function formatProjects(projects) {
171
+ return projects.map(project => {
172
+ const status = project.status !== 'Active' ? ` [${project.status}]` : '';
173
+ const folder = project.folderName ? ` 📁 ${project.folderName}` : '';
174
+ const taskCount = project.taskCount !== undefined && project.taskCount !== null ? ` (${project.taskCount} tasks)` : '';
175
+ const flagged = project.flagged ? '🚩 ' : '';
176
+ const due = project.dueDate ? ` [due: ${formatDate(project.dueDate)}]` : '';
177
+ return `P: ${flagged}${project.name}${status}${due}${folder}${taskCount}`;
178
+ }).join('\n');
179
+ }
180
+ function formatFolders(folders) {
181
+ return folders.map(folder => {
182
+ const projectCount = folder.projectCount !== undefined ? ` (${folder.projectCount} projects)` : '';
183
+ const path = folder.path ? ` 📍 ${folder.path}` : '';
184
+ return `F: ${folder.name}${projectCount}${path}`;
185
+ }).join('\n');
186
+ }
187
+ function formatDate(dateStr) {
188
+ const date = new Date(dateStr);
189
+ return `${date.getMonth() + 1}/${date.getDate()}`;
190
+ }
@@ -0,0 +1,80 @@
1
+ import { z } from 'zod';
2
+ import { removeItem } from '../primitives/removeItem.js';
3
+ export const schema = z.object({
4
+ id: z.string().optional().describe("The ID of the task or project to remove"),
5
+ name: z.string().optional().describe("The name of the task or project to remove (as fallback if ID not provided)"),
6
+ itemType: z.enum(['task', 'project']).describe("Type of item to remove ('task' or 'project')")
7
+ });
8
+ export async function handler(args, extra) {
9
+ try {
10
+ // Validate that either id or name is provided
11
+ if (!args.id && !args.name) {
12
+ return {
13
+ content: [{
14
+ type: "text",
15
+ text: "Either id or name must be provided to remove an item."
16
+ }],
17
+ isError: true
18
+ };
19
+ }
20
+ // Validate itemType
21
+ if (!['task', 'project'].includes(args.itemType)) {
22
+ return {
23
+ content: [{
24
+ type: "text",
25
+ text: `Invalid item type: ${args.itemType}. Must be either 'task' or 'project'.`
26
+ }],
27
+ isError: true
28
+ };
29
+ }
30
+ // Log the remove operation for debugging
31
+ console.error(`Removing ${args.itemType} with ID: ${args.id || 'not provided'}, Name: ${args.name || 'not provided'}`);
32
+ // Call the removeItem function
33
+ const result = await removeItem(args);
34
+ if (result.success) {
35
+ // Item was removed successfully
36
+ const itemTypeLabel = args.itemType === 'task' ? 'Task' : 'Project';
37
+ return {
38
+ content: [{
39
+ type: "text",
40
+ text: `✅ ${itemTypeLabel} "${result.name}" removed successfully.`
41
+ }]
42
+ };
43
+ }
44
+ else {
45
+ // Item removal failed
46
+ let errorMsg = `Failed to remove ${args.itemType}`;
47
+ if (result.error) {
48
+ if (result.error.includes("Item not found")) {
49
+ errorMsg = `${args.itemType.charAt(0).toUpperCase() + args.itemType.slice(1)} not found`;
50
+ if (args.id)
51
+ errorMsg += ` with ID "${args.id}"`;
52
+ if (args.name)
53
+ errorMsg += `${args.id ? ' or' : ' with'} name "${args.name}"`;
54
+ errorMsg += '.';
55
+ }
56
+ else {
57
+ errorMsg += `: ${result.error}`;
58
+ }
59
+ }
60
+ return {
61
+ content: [{
62
+ type: "text",
63
+ text: errorMsg
64
+ }],
65
+ isError: true
66
+ };
67
+ }
68
+ }
69
+ catch (err) {
70
+ const error = err;
71
+ console.error(`Tool execution error: ${error.message}`);
72
+ return {
73
+ content: [{
74
+ type: "text",
75
+ text: `Error removing ${args.itemType}: ${error.message}`
76
+ }],
77
+ isError: true
78
+ };
79
+ }
80
+ }
@@ -0,0 +1,121 @@
1
+ import { executeOmniFocusScript } from '../utils/scriptExecution.js';
2
+ // Main function to dump the database
3
+ export async function dumpDatabase() {
4
+ try {
5
+ // Execute the OmniFocus script
6
+ const data = await executeOmniFocusScript('@omnifocusDump.js');
7
+ // wait 1 second
8
+ await new Promise(resolve => setTimeout(resolve, 1000));
9
+ // Create an empty database if no data returned
10
+ if (!data) {
11
+ return {
12
+ exportDate: new Date().toISOString(),
13
+ tasks: [],
14
+ projects: {},
15
+ folders: {},
16
+ tags: {}
17
+ };
18
+ }
19
+ // Initialize the database object
20
+ const database = {
21
+ exportDate: data.exportDate,
22
+ tasks: [],
23
+ projects: {},
24
+ folders: {},
25
+ tags: {}
26
+ };
27
+ // Process tasks
28
+ if (data.tasks && Array.isArray(data.tasks)) {
29
+ // Convert the tasks to our OmnifocusTask format
30
+ database.tasks = data.tasks.map((task) => {
31
+ // Get tag names from the tag IDs
32
+ const tagNames = (task.tags || []).map(tagId => {
33
+ return data.tags[tagId]?.name || 'Unknown Tag';
34
+ });
35
+ return {
36
+ id: String(task.id),
37
+ name: String(task.name),
38
+ note: String(task.note || ""),
39
+ flagged: Boolean(task.flagged),
40
+ completed: task.taskStatus === "Completed",
41
+ completionDate: null, // Not available in the new format
42
+ dropDate: null, // Not available in the new format
43
+ taskStatus: String(task.taskStatus),
44
+ active: task.taskStatus !== "Completed" && task.taskStatus !== "Dropped",
45
+ dueDate: task.dueDate,
46
+ deferDate: task.deferDate,
47
+ estimatedMinutes: task.estimatedMinutes ? Number(task.estimatedMinutes) : null,
48
+ tags: task.tags || [],
49
+ tagNames: tagNames,
50
+ parentId: task.parentTaskID || null,
51
+ containingProjectId: task.projectID || null,
52
+ projectId: task.projectID || null,
53
+ childIds: task.children || [],
54
+ hasChildren: (task.children && task.children.length > 0) || false,
55
+ sequential: Boolean(task.sequential),
56
+ completedByChildren: Boolean(task.completedByChildren),
57
+ isRepeating: false, // Not available in the new format
58
+ repetitionMethod: null, // Not available in the new format
59
+ repetitionRule: null, // Not available in the new format
60
+ attachments: [], // Default empty array
61
+ linkedFileURLs: [], // Default empty array
62
+ notifications: [], // Default empty array
63
+ shouldUseFloatingTimeZone: false // Default value
64
+ };
65
+ });
66
+ }
67
+ // Process projects
68
+ if (data.projects) {
69
+ for (const [id, project] of Object.entries(data.projects)) {
70
+ database.projects[id] = {
71
+ id: String(project.id),
72
+ name: String(project.name),
73
+ status: String(project.status),
74
+ folderID: project.folderID || null,
75
+ sequential: Boolean(project.sequential),
76
+ effectiveDueDate: project.effectiveDueDate,
77
+ effectiveDeferDate: project.effectiveDeferDate,
78
+ dueDate: project.dueDate,
79
+ deferDate: project.deferDate,
80
+ completedByChildren: Boolean(project.completedByChildren),
81
+ containsSingletonActions: Boolean(project.containsSingletonActions),
82
+ note: String(project.note || ""),
83
+ tasks: project.tasks || [],
84
+ flagged: false, // Default value
85
+ estimatedMinutes: null // Default value
86
+ };
87
+ }
88
+ }
89
+ // Process folders
90
+ if (data.folders) {
91
+ for (const [id, folder] of Object.entries(data.folders)) {
92
+ database.folders[id] = {
93
+ id: String(folder.id),
94
+ name: String(folder.name),
95
+ parentFolderID: folder.parentFolderID || null,
96
+ status: String(folder.status),
97
+ projects: folder.projects || [],
98
+ subfolders: folder.subfolders || []
99
+ };
100
+ }
101
+ }
102
+ // Process tags
103
+ if (data.tags) {
104
+ for (const [id, tag] of Object.entries(data.tags)) {
105
+ database.tags[id] = {
106
+ id: String(tag.id),
107
+ name: String(tag.name),
108
+ parentTagID: tag.parentTagID || null,
109
+ active: Boolean(tag.active),
110
+ allowsNextAction: Boolean(tag.allowsNextAction),
111
+ tasks: tag.tasks || []
112
+ };
113
+ }
114
+ }
115
+ return database;
116
+ }
117
+ catch (error) {
118
+ console.error("Error in dumpDatabase:", error);
119
+ throw error;
120
+ }
121
+ }
@@ -0,0 +1,192 @@
1
+ import { executeOmniFocusScript } from '../utils/scriptExecution.js';
2
+ import { getCacheManager } from '../utils/cacheManager.js';
3
+ import { dumpDatabase as originalDumpDatabase } from './dumpDatabase.js';
4
+ /**
5
+ * Optimized version of dumpDatabase that uses caching
6
+ * Falls back to original implementation if caching fails
7
+ */
8
+ export async function dumpDatabaseOptimized(options) {
9
+ const cacheManager = getCacheManager({
10
+ ttlSeconds: 300, // 5 minute cache
11
+ useChecksum: true // Validate with database checksum
12
+ });
13
+ const cacheKey = options?.cacheKey || 'full-dump';
14
+ // Check if we should force a refresh
15
+ if (!options?.forceRefresh) {
16
+ const cached = await cacheManager.get(cacheKey);
17
+ if (cached) {
18
+ cacheManager.trackHit();
19
+ console.log(`Cache hit for ${cacheKey}. Stats:`, cacheManager.getStats());
20
+ return cached;
21
+ }
22
+ }
23
+ cacheManager.trackMiss();
24
+ console.log(`Cache miss for ${cacheKey}. Fetching fresh data...`);
25
+ // Fetch fresh data using the original implementation
26
+ const freshData = await originalDumpDatabase();
27
+ // Store in cache for next time
28
+ await cacheManager.set(cacheKey, freshData);
29
+ return freshData;
30
+ }
31
+ /**
32
+ * Get just database statistics without full dump
33
+ * Much more efficient for overview information
34
+ */
35
+ export async function getDatabaseStats() {
36
+ const script = `
37
+ (() => {
38
+ try {
39
+ // Calculate statistics without fetching full data
40
+ const allTasks = flattenedTasks;
41
+ const activeTasks = allTasks.filter(task =>
42
+ task.taskStatus !== Task.Status.Completed &&
43
+ task.taskStatus !== Task.Status.Dropped
44
+ );
45
+
46
+ const allProjects = flattenedProjects;
47
+ const activeProjects = allProjects.filter(project =>
48
+ project.status === Project.Status.Active
49
+ );
50
+
51
+ // Count specific task statuses
52
+ const overdueCount = activeTasks.filter(task =>
53
+ task.taskStatus === Task.Status.Overdue
54
+ ).length;
55
+
56
+ const nextActionCount = activeTasks.filter(task =>
57
+ task.taskStatus === Task.Status.Next
58
+ ).length;
59
+
60
+ const flaggedCount = activeTasks.filter(task => task.flagged).length;
61
+
62
+ const inboxCount = activeTasks.filter(task => task.inInbox).length;
63
+
64
+ // Get latest modification time
65
+ let lastModified = new Date(0);
66
+ allTasks.forEach(task => {
67
+ if (task.modificationDate && task.modificationDate > lastModified) {
68
+ lastModified = task.modificationDate;
69
+ }
70
+ });
71
+
72
+ return JSON.stringify({
73
+ taskCount: allTasks.length,
74
+ activeTaskCount: activeTasks.length,
75
+ projectCount: allProjects.length,
76
+ activeProjectCount: activeProjects.length,
77
+ folderCount: flattenedFolders.length,
78
+ tagCount: flattenedTags.filter(tag => tag.active).length,
79
+ overdueCount: overdueCount,
80
+ nextActionCount: nextActionCount,
81
+ flaggedCount: flaggedCount,
82
+ inboxCount: inboxCount,
83
+ lastModified: lastModified.toISOString()
84
+ });
85
+
86
+ } catch (error) {
87
+ return JSON.stringify({
88
+ error: "Failed to get database stats: " + error.toString()
89
+ });
90
+ }
91
+ })();
92
+ `;
93
+ // Write script to temp file and execute
94
+ const fs = await import('fs');
95
+ const tempFile = `/tmp/omnifocus_stats_${Date.now()}.js`;
96
+ fs.writeFileSync(tempFile, script);
97
+ const result = await executeOmniFocusScript(tempFile);
98
+ fs.unlinkSync(tempFile);
99
+ if (result.error) {
100
+ throw new Error(result.error);
101
+ }
102
+ return result;
103
+ }
104
+ /**
105
+ * Get incremental changes since a specific timestamp
106
+ * Much more efficient for periodic updates
107
+ */
108
+ export async function getChangesSince(since) {
109
+ const script = `
110
+ (() => {
111
+ try {
112
+ const sinceDate = new Date("${since.toISOString()}");
113
+
114
+ // Find tasks that changed since the given date
115
+ const allTasks = flattenedTasks;
116
+
117
+ const newTasks = allTasks.filter(task =>
118
+ task.creationDate && task.creationDate > sinceDate
119
+ ).map(task => ({
120
+ id: task.id.primaryKey,
121
+ name: task.name,
122
+ creationDate: task.creationDate.toISOString()
123
+ }));
124
+
125
+ const updatedTasks = allTasks.filter(task =>
126
+ task.modificationDate &&
127
+ task.modificationDate > sinceDate &&
128
+ task.creationDate &&
129
+ task.creationDate <= sinceDate
130
+ ).map(task => ({
131
+ id: task.id.primaryKey,
132
+ name: task.name,
133
+ modificationDate: task.modificationDate.toISOString()
134
+ }));
135
+
136
+ const completedTasks = allTasks.filter(task =>
137
+ task.completionDate &&
138
+ task.completionDate > sinceDate
139
+ ).map(task => ({
140
+ id: task.id.primaryKey,
141
+ name: task.name,
142
+ completionDate: task.completionDate.toISOString()
143
+ }));
144
+
145
+ // Find projects that changed
146
+ const allProjects = flattenedProjects;
147
+
148
+ const newProjects = allProjects.filter(project =>
149
+ project.creationDate && project.creationDate > sinceDate
150
+ ).map(project => ({
151
+ id: project.id.primaryKey,
152
+ name: project.name,
153
+ creationDate: project.creationDate.toISOString()
154
+ }));
155
+
156
+ const updatedProjects = allProjects.filter(project =>
157
+ project.modificationDate &&
158
+ project.modificationDate > sinceDate &&
159
+ project.creationDate &&
160
+ project.creationDate <= sinceDate
161
+ ).map(project => ({
162
+ id: project.id.primaryKey,
163
+ name: project.name,
164
+ modificationDate: project.modificationDate.toISOString()
165
+ }));
166
+
167
+ return JSON.stringify({
168
+ newTasks: newTasks,
169
+ updatedTasks: updatedTasks,
170
+ completedTasks: completedTasks,
171
+ newProjects: newProjects,
172
+ updatedProjects: updatedProjects
173
+ });
174
+
175
+ } catch (error) {
176
+ return JSON.stringify({
177
+ error: "Failed to get changes: " + error.toString()
178
+ });
179
+ }
180
+ })();
181
+ `;
182
+ // Write script to temp file and execute
183
+ const fs = await import('fs');
184
+ const tempFile = `/tmp/omnifocus_changes_${Date.now()}.js`;
185
+ fs.writeFileSync(tempFile, script);
186
+ const result = await executeOmniFocusScript(tempFile);
187
+ fs.unlinkSync(tempFile);
188
+ if (result.error) {
189
+ throw new Error(result.error);
190
+ }
191
+ return result;
192
+ }