@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,187 @@
1
+ import { executeOmniFocusScript } from './scriptExecution.js';
2
+ class OmniFocusCacheManager {
3
+ cache = new Map();
4
+ options;
5
+ totalSize = 0;
6
+ constructor(options = {}) {
7
+ this.options = {
8
+ ttlSeconds: options.ttlSeconds ?? 300, // 5 minutes default
9
+ maxSize: options.maxSize ?? 50, // 50MB default
10
+ useChecksum: options.useChecksum ?? true
11
+ };
12
+ }
13
+ /**
14
+ * Get cached data if valid, otherwise return null
15
+ */
16
+ async get(key) {
17
+ const entry = this.cache.get(key);
18
+ if (!entry) {
19
+ return null;
20
+ }
21
+ // Check if cache has expired
22
+ const age = Date.now() - entry.timestamp.getTime();
23
+ if (age > this.options.ttlSeconds * 1000) {
24
+ this.cache.delete(key);
25
+ return null;
26
+ }
27
+ // If using checksums, validate the cache is still current
28
+ if (this.options.useChecksum && entry.checksum) {
29
+ const currentChecksum = await this.getDatabaseChecksum();
30
+ if (currentChecksum !== entry.checksum) {
31
+ this.cache.delete(key);
32
+ return null;
33
+ }
34
+ }
35
+ return entry.data;
36
+ }
37
+ /**
38
+ * Store data in cache
39
+ */
40
+ async set(key, data) {
41
+ // Estimate size (rough approximation)
42
+ const dataSize = JSON.stringify(data).length / (1024 * 1024); // Convert to MB
43
+ // Check if adding this would exceed max size
44
+ if (this.totalSize + dataSize > this.options.maxSize) {
45
+ this.evictOldest();
46
+ }
47
+ const checksum = this.options.useChecksum ? await this.getDatabaseChecksum() : undefined;
48
+ this.cache.set(key, {
49
+ data,
50
+ timestamp: new Date(),
51
+ checksum
52
+ });
53
+ this.totalSize += dataSize;
54
+ }
55
+ /**
56
+ * Invalidate specific cache entry
57
+ */
58
+ invalidate(key) {
59
+ this.cache.delete(key);
60
+ }
61
+ /**
62
+ * Clear all cache entries
63
+ */
64
+ clear() {
65
+ this.cache.clear();
66
+ this.totalSize = 0;
67
+ }
68
+ /**
69
+ * Get cache statistics
70
+ */
71
+ getStats() {
72
+ let oldestEntry = null;
73
+ this.cache.forEach(entry => {
74
+ if (!oldestEntry || entry.timestamp < oldestEntry) {
75
+ oldestEntry = entry.timestamp;
76
+ }
77
+ });
78
+ return {
79
+ entries: this.cache.size,
80
+ sizeEstimateMB: this.totalSize,
81
+ oldestEntry,
82
+ hitRate: this.calculateHitRate()
83
+ };
84
+ }
85
+ /**
86
+ * Get a lightweight checksum of the database state
87
+ */
88
+ async getDatabaseChecksum() {
89
+ try {
90
+ const script = `
91
+ (() => {
92
+ try {
93
+ // Get counts and latest modification times as a simple checksum
94
+ const taskCount = flattenedTasks.length;
95
+ const projectCount = flattenedProjects.length;
96
+
97
+ // Get the most recent modification time
98
+ let latestMod = new Date(0);
99
+
100
+ flattenedTasks.forEach(task => {
101
+ if (task.modificationDate && task.modificationDate > latestMod) {
102
+ latestMod = task.modificationDate;
103
+ }
104
+ });
105
+
106
+ flattenedProjects.forEach(project => {
107
+ if (project.modificationDate && project.modificationDate > latestMod) {
108
+ latestMod = project.modificationDate;
109
+ }
110
+ });
111
+
112
+ // Create a simple checksum string
113
+ const checksum = taskCount + "-" + projectCount + "-" + latestMod.getTime();
114
+
115
+ return JSON.stringify({ checksum });
116
+ } catch (error) {
117
+ return JSON.stringify({ checksum: "error-" + Date.now() });
118
+ }
119
+ })();
120
+ `;
121
+ // Write to temp file and execute
122
+ const fs = await import('fs');
123
+ const tempFile = `/tmp/omnifocus_checksum_${Date.now()}.js`;
124
+ fs.writeFileSync(tempFile, script);
125
+ const result = await executeOmniFocusScript(tempFile);
126
+ fs.unlinkSync(tempFile);
127
+ return result.checksum || `fallback-${Date.now()}`;
128
+ }
129
+ catch (error) {
130
+ console.error('Error getting database checksum:', error);
131
+ return `error-${Date.now()}`;
132
+ }
133
+ }
134
+ /**
135
+ * Evict oldest cache entries when size limit is reached
136
+ */
137
+ evictOldest() {
138
+ let oldestKey = null;
139
+ let oldestTime = new Date();
140
+ this.cache.forEach((entry, key) => {
141
+ if (entry.timestamp < oldestTime) {
142
+ oldestTime = entry.timestamp;
143
+ oldestKey = key;
144
+ }
145
+ });
146
+ if (oldestKey) {
147
+ this.cache.delete(oldestKey);
148
+ // Recalculate total size (simplified for now)
149
+ this.totalSize *= 0.9;
150
+ }
151
+ }
152
+ // Tracking for hit rate calculation
153
+ hits = 0;
154
+ misses = 0;
155
+ calculateHitRate() {
156
+ const total = this.hits + this.misses;
157
+ if (total === 0)
158
+ return 0;
159
+ return (this.hits / total) * 100;
160
+ }
161
+ trackHit() {
162
+ this.hits++;
163
+ }
164
+ trackMiss() {
165
+ this.misses++;
166
+ }
167
+ }
168
+ // Singleton instance
169
+ let cacheManager = null;
170
+ /**
171
+ * Get or create the cache manager instance
172
+ */
173
+ export function getCacheManager(options) {
174
+ if (!cacheManager) {
175
+ cacheManager = new OmniFocusCacheManager(options);
176
+ }
177
+ return cacheManager;
178
+ }
179
+ /**
180
+ * Reset the cache manager (useful for testing)
181
+ */
182
+ export function resetCacheManager() {
183
+ if (cacheManager) {
184
+ cacheManager.clear();
185
+ }
186
+ cacheManager = null;
187
+ }
@@ -0,0 +1,58 @@
1
+ /**
2
+ * Version 2 of date formatting utilities that work around AppleScript restrictions
3
+ * Dates must be constructed outside of tell blocks
4
+ */
5
+ /**
6
+ * Generate AppleScript to construct a date variable outside tell blocks
7
+ * @param isoDateString - ISO format date string
8
+ * @param varName - Name for the date variable
9
+ * @returns AppleScript code to construct the date
10
+ */
11
+ export function createDateOutsideTellBlock(isoDateString, varName) {
12
+ // Parse the ISO date string
13
+ const date = new Date(isoDateString);
14
+ // Check if the date is valid
15
+ if (isNaN(date.getTime())) {
16
+ throw new Error(`Invalid date string: ${isoDateString}`);
17
+ }
18
+ // Extract date components
19
+ const year = date.getFullYear();
20
+ const month = date.getMonth() + 1; // JavaScript months are 0-indexed
21
+ const day = date.getDate();
22
+ const hours = date.getHours();
23
+ const minutes = date.getMinutes();
24
+ const seconds = date.getSeconds();
25
+ // Generate AppleScript to construct date outside tell blocks
26
+ return `copy current date to ${varName}
27
+ set year of ${varName} to ${year}
28
+ set month of ${varName} to ${month}
29
+ set day of ${varName} to ${day}
30
+ set hours of ${varName} to ${hours}
31
+ set minutes of ${varName} to ${minutes}
32
+ set seconds of ${varName} to ${seconds}`;
33
+ }
34
+ /**
35
+ * Generate date assignment that works with AppleScript restrictions
36
+ */
37
+ export function generateDateAssignmentV2(objectName, propertyName, isoDateString) {
38
+ if (isoDateString === undefined) {
39
+ return null; // No date change requested
40
+ }
41
+ if (isoDateString === '') {
42
+ // Clear the date
43
+ return {
44
+ preScript: '',
45
+ assignmentScript: `set ${propertyName} of ${objectName} to missing value`
46
+ };
47
+ }
48
+ // Generate unique variable name
49
+ const varName = `dateVar${Math.random().toString(36).substr(2, 9)}`;
50
+ // Generate the date construction (outside tell blocks)
51
+ const preScript = createDateOutsideTellBlock(isoDateString, varName);
52
+ // Generate the assignment (inside tell blocks)
53
+ const assignmentScript = `set ${propertyName} of ${objectName} to ${varName}`;
54
+ return {
55
+ preScript,
56
+ assignmentScript
57
+ };
58
+ }
@@ -0,0 +1,169 @@
1
+ // OmniJS script to get the current perspective view in OmniFocus
2
+ (() => {
3
+ try {
4
+ // Note: We can't easily switch perspectives via OmniJS
5
+ // We can only report what's currently visible in the window
6
+
7
+ // Get the current window and its perspective
8
+ const window = document.windows[0];
9
+ if (!window) {
10
+ return JSON.stringify({
11
+ success: false,
12
+ error: "No OmniFocus window is open"
13
+ });
14
+ }
15
+
16
+ // Get the current perspective
17
+ const currentPerspective = window.perspective;
18
+ let perspectiveName = "Unknown";
19
+
20
+ // Identify the perspective
21
+ if (currentPerspective) {
22
+ if (currentPerspective === Perspective.BuiltIn.Inbox) {
23
+ perspectiveName = "Inbox";
24
+ } else if (currentPerspective === Perspective.BuiltIn.Projects) {
25
+ perspectiveName = "Projects";
26
+ } else if (currentPerspective === Perspective.BuiltIn.Tags) {
27
+ perspectiveName = "Tags";
28
+ } else if (currentPerspective === Perspective.BuiltIn.Forecast) {
29
+ perspectiveName = "Forecast";
30
+ } else if (currentPerspective === Perspective.BuiltIn.Flagged) {
31
+ perspectiveName = "Flagged";
32
+ } else if (currentPerspective === Perspective.BuiltIn.Review) {
33
+ perspectiveName = "Review";
34
+ } else if (currentPerspective.name) {
35
+ // Custom perspective
36
+ perspectiveName = currentPerspective.name;
37
+ }
38
+ }
39
+
40
+ // Get visible items based on the perspective
41
+ const items = [];
42
+ const selection = window.selection;
43
+ const selectedTasks = selection.tasks;
44
+ const selectedProjects = selection.projects;
45
+
46
+ // Helper function to format dates
47
+ function formatDate(date) {
48
+ if (!date) return null;
49
+ return date.toISOString();
50
+ }
51
+
52
+ // Helper to get task details
53
+ function getTaskDetails(task) {
54
+ const details = {
55
+ id: task.id.primaryKey,
56
+ name: task.name,
57
+ completed: task.completed,
58
+ flagged: task.flagged,
59
+ note: task.note || '',
60
+ dueDate: formatDate(task.dueDate),
61
+ deferDate: formatDate(task.deferDate),
62
+ completionDate: formatDate(task.completionDate),
63
+ estimatedMinutes: task.estimatedMinutes
64
+ };
65
+
66
+ // Task status
67
+ const statusMap = {
68
+ [Task.Status.Available]: "Available",
69
+ [Task.Status.Blocked]: "Blocked",
70
+ [Task.Status.Completed]: "Completed",
71
+ [Task.Status.Dropped]: "Dropped",
72
+ [Task.Status.DueSoon]: "DueSoon",
73
+ [Task.Status.Next]: "Next",
74
+ [Task.Status.Overdue]: "Overdue"
75
+ };
76
+ details.taskStatus = statusMap[task.taskStatus] || "Unknown";
77
+
78
+ // Project context
79
+ const project = task.containingProject;
80
+ details.projectName = project ? project.name : null;
81
+
82
+ // Tags
83
+ details.tagNames = task.tags.map(tag => tag.name);
84
+
85
+ return details;
86
+ }
87
+
88
+ // Get project details
89
+ function getProjectDetails(project) {
90
+ return {
91
+ id: project.id.primaryKey,
92
+ name: project.name,
93
+ type: 'project',
94
+ status: project.status,
95
+ note: project.note || '',
96
+ flagged: project.flagged || false,
97
+ dueDate: formatDate(project.dueDate),
98
+ deferDate: formatDate(project.deferDate),
99
+ folderName: project.parentFolder ? project.parentFolder.name : null
100
+ };
101
+ }
102
+
103
+ // Try to get content based on perspective type
104
+ if (perspectiveName === "Inbox") {
105
+ // Get inbox tasks - inbox is a global in OmniJS
106
+ inbox.forEach(task => {
107
+ items.push(getTaskDetails(task));
108
+ });
109
+ } else if (perspectiveName === "Projects") {
110
+ // Get all projects - using flattenedProjects global
111
+ flattenedProjects.forEach(project => {
112
+ if (project.status === Project.Status.Active) {
113
+ items.push(getProjectDetails(project));
114
+ }
115
+ });
116
+ } else if (perspectiveName === "Tags") {
117
+ // Get tagged tasks - using flattenedTags global
118
+ flattenedTags.forEach(tag => {
119
+ tag.remainingTasks.forEach(task => {
120
+ const taskDetail = getTaskDetails(task);
121
+ if (!items.some(item => item.id === taskDetail.id)) {
122
+ items.push(taskDetail);
123
+ }
124
+ });
125
+ });
126
+ } else if (perspectiveName === "Flagged") {
127
+ // Get flagged items - using flattenedTasks global
128
+ flattenedTasks.forEach(task => {
129
+ if (task.flagged && !task.completed) {
130
+ items.push(getTaskDetails(task));
131
+ }
132
+ });
133
+ } else {
134
+ // For other perspectives, try to get selected or visible items
135
+ if (selectedTasks.length > 0) {
136
+ selectedTasks.forEach(task => {
137
+ items.push(getTaskDetails(task));
138
+ });
139
+ }
140
+ if (selectedProjects.length > 0) {
141
+ selectedProjects.forEach(project => {
142
+ items.push(getProjectDetails(project));
143
+ });
144
+ }
145
+
146
+ // If no selection, get some available tasks
147
+ if (items.length === 0) {
148
+ const availableTasks = flattenedTasks.filter(task =>
149
+ task.taskStatus === Task.Status.Available && !task.completed
150
+ );
151
+ availableTasks.slice(0, 100).forEach(task => {
152
+ items.push(getTaskDetails(task));
153
+ });
154
+ }
155
+ }
156
+
157
+ return JSON.stringify({
158
+ success: true,
159
+ perspectiveName: perspectiveName,
160
+ items: items.slice(0, 100) // Limit to 100 items by default
161
+ });
162
+
163
+ } catch (error) {
164
+ return JSON.stringify({
165
+ success: false,
166
+ error: error.toString()
167
+ });
168
+ }
169
+ })()
@@ -0,0 +1,59 @@
1
+ // OmniJS script to list available perspectives in OmniFocus
2
+ (() => {
3
+ try {
4
+ const perspectives = [];
5
+
6
+ // Get all built-in perspectives
7
+ // According to the API: Perspective.BuiltIn has these properties
8
+ const builtInPerspectives = [
9
+ { obj: Perspective.BuiltIn.Inbox, name: 'Inbox' },
10
+ { obj: Perspective.BuiltIn.Projects, name: 'Projects' },
11
+ { obj: Perspective.BuiltIn.Tags, name: 'Tags' },
12
+ { obj: Perspective.BuiltIn.Forecast, name: 'Forecast' },
13
+ { obj: Perspective.BuiltIn.Flagged, name: 'Flagged' },
14
+ { obj: Perspective.BuiltIn.Review, name: 'Review' }
15
+ ];
16
+
17
+ // Add built-in perspectives
18
+ builtInPerspectives.forEach(p => {
19
+ perspectives.push({
20
+ id: 'builtin_' + p.name.toLowerCase(),
21
+ name: p.name,
22
+ type: 'builtin',
23
+ isBuiltIn: true,
24
+ canModify: false
25
+ });
26
+ });
27
+
28
+ // Get all custom perspectives
29
+ // According to the API: Perspective.Custom.all returns all custom perspectives
30
+ try {
31
+ const customPerspectives = Perspective.Custom.all;
32
+ if (customPerspectives && customPerspectives.length > 0) {
33
+ customPerspectives.forEach(p => {
34
+ perspectives.push({
35
+ id: p.identifier || 'custom_' + p.name.toLowerCase().replace(/\s+/g, '_'),
36
+ name: p.name,
37
+ type: 'custom',
38
+ isBuiltIn: false,
39
+ canModify: true
40
+ });
41
+ });
42
+ }
43
+ } catch (e) {
44
+ // Custom perspectives might not be available (Standard edition)
45
+ // This is not a fatal error
46
+ }
47
+
48
+ return JSON.stringify({
49
+ success: true,
50
+ perspectives: perspectives
51
+ });
52
+
53
+ } catch (error) {
54
+ return JSON.stringify({
55
+ success: false,
56
+ error: error.toString()
57
+ });
58
+ }
59
+ })()
@@ -0,0 +1,223 @@
1
+ // OmniJS script to export active tasks from OmniFocus database - Optimized
2
+ (() => {
3
+ try {
4
+ const startTime = new Date();
5
+
6
+ // Helper function to format dates consistently or return null
7
+ function formatDate(date) {
8
+ if (!date) return null;
9
+ return date.toISOString();
10
+ }
11
+
12
+ // Helper function to safely get enum values - Simplified with direct mapping
13
+ const taskStatusMap = {
14
+ [Task.Status.Available]: "Available",
15
+ [Task.Status.Blocked]: "Blocked",
16
+ [Task.Status.Completed]: "Completed",
17
+ [Task.Status.Dropped]: "Dropped",
18
+ [Task.Status.DueSoon]: "DueSoon",
19
+ [Task.Status.Next]: "Next",
20
+ [Task.Status.Overdue]: "Overdue"
21
+ };
22
+
23
+ const projectStatusMap = {
24
+ [Project.Status.Active]: "Active",
25
+ [Project.Status.Done]: "Done",
26
+ [Project.Status.Dropped]: "Dropped",
27
+ [Project.Status.OnHold]: "OnHold"
28
+ };
29
+
30
+ const folderStatusMap = {
31
+ [Folder.Status.Active]: "Active",
32
+ [Folder.Status.Dropped]: "Dropped"
33
+ };
34
+
35
+ function getEnumValue(enumObj, mapObj) {
36
+ if (enumObj === null || enumObj === undefined) return null;
37
+ return mapObj[enumObj] || "Unknown";
38
+ }
39
+
40
+ // Create database export object using Maps for faster lookups
41
+ const exportData = {
42
+ exportDate: new Date().toISOString(),
43
+ tasks: [],
44
+ projects: {},
45
+ folders: {},
46
+ tags: {}
47
+ };
48
+
49
+ // Filter active projects first to avoid unnecessary processing
50
+ const activeProjects = flattenedProjects.filter(project =>
51
+ project.status !== Project.Status.Done &&
52
+ project.status !== Project.Status.Dropped
53
+ );
54
+
55
+ // Pre-filter active tasks to avoid repeated filtering
56
+ const activeTasks = flattenedTasks.filter(task =>
57
+ task.taskStatus !== Task.Status.Completed &&
58
+ task.taskStatus !== Task.Status.Dropped
59
+ );
60
+
61
+ // Pre-filter active folders
62
+ const activeFolders = flattenedFolders.filter(folder =>
63
+ folder.status !== Folder.Status.Dropped
64
+ );
65
+
66
+ // Pre-filter active tags
67
+ const activeTags = flattenedTags.filter(tag => tag.active);
68
+
69
+ // Process projects in a single pass and store in Map for O(1) lookups
70
+ const projectsMap = new Map();
71
+ activeProjects.forEach(project => {
72
+ try {
73
+ const projectId = project.id.primaryKey;
74
+ const projectData = {
75
+ id: projectId,
76
+ name: project.name,
77
+ status: getEnumValue(project.status, projectStatusMap),
78
+ folderID: project.parentFolder ? project.parentFolder.id.primaryKey : null,
79
+ sequential: project.task.sequential || false,
80
+ effectiveDueDate: formatDate(project.effectiveDueDate),
81
+ effectiveDeferDate: formatDate(project.effectiveDeferDate),
82
+ dueDate: formatDate(project.dueDate),
83
+ deferDate: formatDate(project.deferDate),
84
+ completedByChildren: project.completedByChildren,
85
+ containsSingletonActions: project.containsSingletonActions,
86
+ note: project.note || "",
87
+ tasks: [] // Will be populated in the task loop
88
+ };
89
+ projectsMap.set(projectId, projectData);
90
+ exportData.projects[projectId] = projectData;
91
+ } catch (projectError) {
92
+ // Silently handle project processing errors
93
+ }
94
+ });
95
+
96
+ // Process folders in a single pass
97
+ const foldersMap = new Map();
98
+ activeFolders.forEach(folder => {
99
+ try {
100
+ const folderId = folder.id.primaryKey;
101
+ const folderData = {
102
+ id: folderId,
103
+ name: folder.name,
104
+ parentFolderID: folder.parent ? folder.parent.id.primaryKey : null,
105
+ status: getEnumValue(folder.status, folderStatusMap),
106
+ projects: [],
107
+ subfolders: []
108
+ };
109
+ foldersMap.set(folderId, folderData);
110
+ exportData.folders[folderId] = folderData;
111
+ } catch (folderError) {
112
+ // Silently handle folder processing errors
113
+ }
114
+ });
115
+
116
+ // Process tags in a single pass
117
+ const tagsMap = new Map();
118
+ activeTags.forEach(tag => {
119
+ try {
120
+ const tagId = tag.id.primaryKey;
121
+ const tagData = {
122
+ id: tagId,
123
+ name: tag.name,
124
+ parentTagID: tag.parent ? tag.parent.id.primaryKey : null,
125
+ active: tag.active,
126
+ allowsNextAction: tag.allowsNextAction,
127
+ tasks: []
128
+ };
129
+ tagsMap.set(tagId, tagData);
130
+ exportData.tags[tagId] = tagData;
131
+ } catch (tagError) {
132
+ // Silently handle tag processing errors
133
+ }
134
+ });
135
+
136
+ console.log("Building relationships and processing tasks simultaneously...");
137
+
138
+ // Build folder relationships and project-folder relationships as we go
139
+ foldersMap.forEach((folder, folderId) => {
140
+ if (folder.parentFolderID && foldersMap.has(folder.parentFolderID)) {
141
+ const parentFolder = foldersMap.get(folder.parentFolderID);
142
+ if (!parentFolder.subfolders.includes(folder.id)) {
143
+ parentFolder.subfolders.push(folder.id);
144
+ }
145
+ }
146
+ });
147
+
148
+ console.log(`Processing ${activeTasks.length} active tasks...`);
149
+
150
+ // Process tasks with an optimized approach
151
+ // Process in batches of 100 to prevent UI freezing
152
+ const BATCH_SIZE = 100;
153
+
154
+ for (let i = 0; i < activeTasks.length; i += BATCH_SIZE) {
155
+ const taskBatch = activeTasks.slice(i, i + BATCH_SIZE);
156
+
157
+ taskBatch.forEach(task => {
158
+ try {
159
+ // Get task data with minimal processing
160
+ const taskTags = task.tags.map(tag => tag.id.primaryKey);
161
+ const projectID = task.containingProject ? task.containingProject.id.primaryKey : null;
162
+
163
+ const taskData = {
164
+ id: task.id.primaryKey,
165
+ name: task.name,
166
+ note: task.note || "",
167
+ taskStatus: getEnumValue(task.taskStatus, taskStatusMap),
168
+ flagged: task.flagged,
169
+ dueDate: formatDate(task.dueDate),
170
+ deferDate: formatDate(task.deferDate),
171
+ effectiveDueDate: formatDate(task.effectiveDueDate),
172
+ effectiveDeferDate: formatDate(task.effectiveDeferDate),
173
+ estimatedMinutes: task.estimatedMinutes,
174
+ completedByChildren: task.completedByChildren,
175
+ sequential: task.sequential || false,
176
+ tags: taskTags,
177
+ projectID: projectID,
178
+ parentTaskID: task.parent ? task.parent.id.primaryKey : null,
179
+ children: task.children.map(child => child.id.primaryKey),
180
+ inInbox: task.inInbox
181
+ };
182
+
183
+ // Add task to export
184
+ exportData.tasks.push(taskData);
185
+
186
+ // Add task ID to associated project (if it exists)
187
+ if (projectID && projectsMap.has(projectID)) {
188
+ projectsMap.get(projectID).tasks.push(taskData.id);
189
+
190
+ // Update folder-project relationship (only once per project)
191
+ const project = projectsMap.get(projectID);
192
+ if (project.folderID && foldersMap.has(project.folderID)) {
193
+ const folder = foldersMap.get(project.folderID);
194
+ if (!folder.projects.includes(project.id)) {
195
+ folder.projects.push(project.id);
196
+ }
197
+ }
198
+ }
199
+
200
+ // Add task ID to associated tags
201
+ taskTags.forEach(tagID => {
202
+ if (tagsMap.has(tagID)) {
203
+ tagsMap.get(tagID).tasks.push(taskData.id);
204
+ }
205
+ });
206
+ } catch (taskError) {
207
+ // Silently handle task processing errors
208
+ }
209
+ });
210
+ }
211
+
212
+ // Return the complete database export
213
+ const jsonData = JSON.stringify(exportData);
214
+ return jsonData;
215
+
216
+ } catch (error) {
217
+ return JSON.stringify({
218
+ success: false,
219
+ error: `Error exporting database: ${error}`
220
+ });
221
+ }
222
+ }
223
+ )();