@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,212 @@
1
+ import { z } from 'zod';
2
+ import { queryOmnifocus, QueryOmnifocusParams } from '../primitives/queryOmnifocus.js';
3
+ import { RequestHandlerExtra } from '@modelcontextprotocol/sdk/shared/protocol.js';
4
+
5
+ export const schema = z.object({
6
+ 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"),
7
+
8
+ filters: z.object({
9
+ projectId: z.string().optional().describe("Filter tasks by exact project ID (use when you know the specific project ID)"),
10
+ 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"),
11
+ folderId: z.string().optional().describe("Filter projects by exact folder ID (use when you know the specific folder ID)"),
12
+ 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"),
13
+ 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'"),
14
+ flagged: z.boolean().optional().describe("Filter by flagged status. true = only flagged items, false = only unflagged items"),
15
+ 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)"),
16
+ 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"),
17
+ 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")
18
+ }).optional().describe("Optional filters to narrow results. ALL filters combine with AND logic (must match all). Within array filters (tags, status) OR logic applies"),
19
+
20
+ 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"),
21
+
22
+ limit: z.number().optional().describe("Maximum number of items to return. Useful for large result sets. Default: no limit"),
23
+
24
+ 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)"),
25
+
26
+ 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'"),
27
+
28
+ includeCompleted: z.boolean().optional().describe("Include completed and dropped items. Default: false (active items only)"),
29
+
30
+ summary: z.boolean().optional().describe("Return only count of matches, not full details. Efficient for statistics. Default: false")
31
+ });
32
+
33
+ export async function handler(args: z.infer<typeof schema>, extra: RequestHandlerExtra) {
34
+ try {
35
+ // Call the queryOmniFocus function
36
+ const result = await queryOmnifocus(args as QueryOmnifocusParams);
37
+
38
+ if (result.success) {
39
+ // Format response based on whether it's a summary or full results
40
+ if (args.summary) {
41
+ return {
42
+ content: [{
43
+ type: "text" as const,
44
+ text: `Found ${result.count} ${args.entity} matching your criteria.`
45
+ }]
46
+ };
47
+ } else {
48
+ // Format the results in a compact, readable format
49
+ const items = result.items || [];
50
+ let output = formatQueryResults(items, args.entity, args.filters);
51
+
52
+ // Add metadata about the query
53
+ if (items.length === args.limit) {
54
+ output += `\n\n⚠️ Results limited to ${args.limit} items. More may be available.`;
55
+ }
56
+
57
+ return {
58
+ content: [{
59
+ type: "text" as const,
60
+ text: output
61
+ }]
62
+ };
63
+ }
64
+ } else {
65
+ return {
66
+ content: [{
67
+ type: "text" as const,
68
+ text: `Query failed: ${result.error}`
69
+ }],
70
+ isError: true
71
+ };
72
+ }
73
+ } catch (err: unknown) {
74
+ const error = err as Error;
75
+ console.error(`Query execution error: ${error.message}`);
76
+ return {
77
+ content: [{
78
+ type: "text" as const,
79
+ text: `Error executing query: ${error.message}`
80
+ }],
81
+ isError: true
82
+ };
83
+ }
84
+ }
85
+
86
+ // Helper function to format query results in a compact way
87
+ function formatQueryResults(items: any[], entity: string, filters?: any): string {
88
+ if (items.length === 0) {
89
+ return `No ${entity} found matching the specified criteria.`;
90
+ }
91
+
92
+ let output = `## Query Results: ${items.length} ${entity}\n\n`;
93
+
94
+ // Add filter summary if filters were applied
95
+ if (filters && Object.keys(filters).length > 0) {
96
+ output += `Filters applied: ${formatFilters(filters)}\n\n`;
97
+ }
98
+
99
+ // Format each item based on entity type
100
+ switch (entity) {
101
+ case 'tasks':
102
+ output += formatTasks(items);
103
+ break;
104
+ case 'projects':
105
+ output += formatProjects(items);
106
+ break;
107
+ case 'folders':
108
+ output += formatFolders(items);
109
+ break;
110
+ }
111
+
112
+ return output;
113
+ }
114
+
115
+ function formatFilters(filters: any): string {
116
+ const parts = [];
117
+ if (filters.projectId) parts.push(`projectId: "${filters.projectId}"`);
118
+ if (filters.projectName) parts.push(`project: "${filters.projectName}"`);
119
+ if (filters.folderId) parts.push(`folderId: "${filters.folderId}"`);
120
+ if (filters.tags) parts.push(`tags: [${filters.tags.join(', ')}]`);
121
+ if (filters.status) parts.push(`status: [${filters.status.join(', ')}]`);
122
+ if (filters.flagged !== undefined) parts.push(`flagged: ${filters.flagged}`);
123
+ if (filters.dueWithin) parts.push(`due within ${filters.dueWithin} days`);
124
+ if (filters.deferredUntil) parts.push(`deferred becoming available within ${filters.deferredUntil} days`);
125
+ if (filters.hasNote !== undefined) parts.push(`has note: ${filters.hasNote}`);
126
+ return parts.join(', ');
127
+ }
128
+
129
+ function formatTasks(tasks: any[]): string {
130
+ return tasks.map(task => {
131
+ const parts = [];
132
+
133
+ // Core display
134
+ const flag = task.flagged ? '🚩 ' : '';
135
+ parts.push(`• ${flag}${task.name || 'Unnamed'}`);
136
+
137
+ // Add ID if present
138
+ if (task.id) {
139
+ parts.push(`[${task.id}]`);
140
+ }
141
+
142
+ // Project context
143
+ if (task.projectName) {
144
+ parts.push(`(${task.projectName})`);
145
+ }
146
+
147
+ // Dates
148
+ if (task.dueDate) {
149
+ parts.push(`[due: ${formatDate(task.dueDate)}]`);
150
+ }
151
+ if (task.deferDate) {
152
+ parts.push(`[defer: ${formatDate(task.deferDate)}]`);
153
+ }
154
+
155
+ // Time estimate
156
+ if (task.estimatedMinutes) {
157
+ const hours = task.estimatedMinutes >= 60
158
+ ? `${Math.floor(task.estimatedMinutes / 60)}h`
159
+ : `${task.estimatedMinutes}m`;
160
+ parts.push(`(${hours})`);
161
+ }
162
+
163
+ // Tags
164
+ if (task.tagNames?.length > 0) {
165
+ parts.push(`<${task.tagNames.join(',')}>`);
166
+ }
167
+
168
+ // Status
169
+ if (task.taskStatus) {
170
+ parts.push(`#${task.taskStatus.toLowerCase()}`);
171
+ }
172
+
173
+ // Metadata dates if requested
174
+ if (task.creationDate) {
175
+ parts.push(`[created: ${formatDate(task.creationDate)}]`);
176
+ }
177
+ if (task.modificationDate) {
178
+ parts.push(`[modified: ${formatDate(task.modificationDate)}]`);
179
+ }
180
+ if (task.completionDate) {
181
+ parts.push(`[completed: ${formatDate(task.completionDate)}]`);
182
+ }
183
+
184
+ return parts.join(' ');
185
+ }).join('\n');
186
+ }
187
+
188
+ function formatProjects(projects: any[]): string {
189
+ return projects.map(project => {
190
+ const status = project.status !== 'Active' ? ` [${project.status}]` : '';
191
+ const folder = project.folderName ? ` 📁 ${project.folderName}` : '';
192
+ const taskCount = project.taskCount !== undefined && project.taskCount !== null ? ` (${project.taskCount} tasks)` : '';
193
+ const flagged = project.flagged ? '🚩 ' : '';
194
+ const due = project.dueDate ? ` [due: ${formatDate(project.dueDate)}]` : '';
195
+
196
+ return `P: ${flagged}${project.name}${status}${due}${folder}${taskCount}`;
197
+ }).join('\n');
198
+ }
199
+
200
+ function formatFolders(folders: any[]): string {
201
+ return folders.map(folder => {
202
+ const projectCount = folder.projectCount !== undefined ? ` (${folder.projectCount} projects)` : '';
203
+ const path = folder.path ? ` 📍 ${folder.path}` : '';
204
+
205
+ return `F: ${folder.name}${projectCount}${path}`;
206
+ }).join('\n');
207
+ }
208
+
209
+ function formatDate(dateStr: string): string {
210
+ const date = new Date(dateStr);
211
+ return `${date.getMonth() + 1}/${date.getDate()}`;
212
+ }
@@ -0,0 +1,86 @@
1
+ import { z } from 'zod';
2
+ import { removeItem, RemoveItemParams } from '../primitives/removeItem.js';
3
+ import { RequestHandlerExtra } from '@modelcontextprotocol/sdk/shared/protocol.js';
4
+
5
+ export const schema = z.object({
6
+ id: z.string().optional().describe("The ID of the task or project to remove"),
7
+ name: z.string().optional().describe("The name of the task or project to remove (as fallback if ID not provided)"),
8
+ itemType: z.enum(['task', 'project']).describe("Type of item to remove ('task' or 'project')")
9
+ });
10
+
11
+ export async function handler(args: z.infer<typeof schema>, extra: RequestHandlerExtra) {
12
+ try {
13
+ // Validate that either id or name is provided
14
+ if (!args.id && !args.name) {
15
+ return {
16
+ content: [{
17
+ type: "text" as const,
18
+ text: "Either id or name must be provided to remove an item."
19
+ }],
20
+ isError: true
21
+ };
22
+ }
23
+
24
+ // Validate itemType
25
+ if (!['task', 'project'].includes(args.itemType)) {
26
+ return {
27
+ content: [{
28
+ type: "text" as const,
29
+ text: `Invalid item type: ${args.itemType}. Must be either 'task' or 'project'.`
30
+ }],
31
+ isError: true
32
+ };
33
+ }
34
+
35
+ // Log the remove operation for debugging
36
+ console.error(`Removing ${args.itemType} with ID: ${args.id || 'not provided'}, Name: ${args.name || 'not provided'}`);
37
+
38
+ // Call the removeItem function
39
+ const result = await removeItem(args as RemoveItemParams);
40
+
41
+ if (result.success) {
42
+ // Item was removed successfully
43
+ const itemTypeLabel = args.itemType === 'task' ? 'Task' : 'Project';
44
+
45
+ return {
46
+ content: [{
47
+ type: "text" as const,
48
+ text: `✅ ${itemTypeLabel} "${result.name}" removed successfully.`
49
+ }]
50
+ };
51
+ } else {
52
+ // Item removal failed
53
+ let errorMsg = `Failed to remove ${args.itemType}`;
54
+
55
+ if (result.error) {
56
+ if (result.error.includes("Item not found")) {
57
+ errorMsg = `${args.itemType.charAt(0).toUpperCase() + args.itemType.slice(1)} not found`;
58
+ if (args.id) errorMsg += ` with ID "${args.id}"`;
59
+ if (args.name) errorMsg += `${args.id ? ' or' : ' with'} name "${args.name}"`;
60
+ errorMsg += '.';
61
+ } else {
62
+ errorMsg += `: ${result.error}`;
63
+ }
64
+ }
65
+
66
+ return {
67
+ content: [{
68
+ type: "text" as const,
69
+ text: errorMsg
70
+ }],
71
+ isError: true
72
+ };
73
+ }
74
+ } catch (err: unknown) {
75
+ const error = err as Error;
76
+ console.error(`Tool execution error: ${error.message}`);
77
+
78
+ return {
79
+ content: [{
80
+ type: "text" as const,
81
+ text: `Error removing ${args.itemType}: ${error.message}`
82
+ }],
83
+ isError: true
84
+ };
85
+ }
86
+ }
@@ -0,0 +1,196 @@
1
+ import { OmnifocusDatabase, OmnifocusTask, OmnifocusProject, OmnifocusFolder, OmnifocusTag } from '../types.js';
2
+ import { executeOmniFocusScript } from '../utils/scriptExecution.js';
3
+
4
+ import fs from 'fs';
5
+ // Define interfaces for the data returned from the script
6
+ interface OmnifocusDumpTask {
7
+ id: string;
8
+ name: string;
9
+ note?: string;
10
+ taskStatus: string;
11
+ flagged: boolean;
12
+ dueDate: string | null;
13
+ deferDate: string | null;
14
+ effectiveDueDate: string | null;
15
+ effectiveDeferDate: string | null;
16
+ estimatedMinutes: number | null;
17
+ completedByChildren: boolean;
18
+ sequential: boolean;
19
+ tags: string[];
20
+ projectID: string | null;
21
+ parentTaskID: string | null;
22
+ children: string[];
23
+ inInbox: boolean;
24
+ }
25
+
26
+ interface OmnifocusDumpProject {
27
+ id: string;
28
+ name: string;
29
+ status: string;
30
+ folderID: string | null;
31
+ sequential: boolean;
32
+ effectiveDueDate: string | null;
33
+ effectiveDeferDate: string | null;
34
+ dueDate: string | null;
35
+ deferDate: string | null;
36
+ completedByChildren: boolean;
37
+ containsSingletonActions: boolean;
38
+ note: string;
39
+ tasks: string[];
40
+ }
41
+
42
+ interface OmnifocusDumpFolder {
43
+ id: string;
44
+ name: string;
45
+ parentFolderID: string | null;
46
+ status: string;
47
+ projects: string[];
48
+ subfolders: string[];
49
+ }
50
+
51
+ interface OmnifocusDumpTag {
52
+ id: string;
53
+ name: string;
54
+ parentTagID: string | null;
55
+ active: boolean;
56
+ allowsNextAction: boolean;
57
+ tasks: string[];
58
+ }
59
+
60
+ interface OmnifocusDumpData {
61
+ exportDate: string;
62
+ tasks: OmnifocusDumpTask[];
63
+ projects: Record<string, OmnifocusDumpProject>;
64
+ folders: Record<string, OmnifocusDumpFolder>;
65
+ tags: Record<string, OmnifocusDumpTag>;
66
+ }
67
+
68
+ // Main function to dump the database
69
+ export async function dumpDatabase(): Promise<OmnifocusDatabase> {
70
+
71
+ try {
72
+ // Execute the OmniFocus script
73
+ const data = await executeOmniFocusScript('@omnifocusDump.js') as OmnifocusDumpData;
74
+ // wait 1 second
75
+ await new Promise(resolve => setTimeout(resolve, 1000));
76
+
77
+ // Create an empty database if no data returned
78
+ if (!data) {
79
+ return {
80
+ exportDate: new Date().toISOString(),
81
+ tasks: [],
82
+ projects: {},
83
+ folders: {},
84
+ tags: {}
85
+ };
86
+ }
87
+
88
+ // Initialize the database object
89
+ const database: OmnifocusDatabase = {
90
+ exportDate: data.exportDate,
91
+ tasks: [],
92
+ projects: {},
93
+ folders: {},
94
+ tags: {}
95
+ };
96
+
97
+ // Process tasks
98
+ if (data.tasks && Array.isArray(data.tasks)) {
99
+ // Convert the tasks to our OmnifocusTask format
100
+ database.tasks = data.tasks.map((task: OmnifocusDumpTask) => {
101
+ // Get tag names from the tag IDs
102
+ const tagNames = (task.tags || []).map(tagId => {
103
+ return data.tags[tagId]?.name || 'Unknown Tag';
104
+ });
105
+
106
+ return {
107
+ id: String(task.id),
108
+ name: String(task.name),
109
+ note: String(task.note || ""),
110
+ flagged: Boolean(task.flagged),
111
+ completed: task.taskStatus === "Completed",
112
+ completionDate: null, // Not available in the new format
113
+ dropDate: null, // Not available in the new format
114
+ taskStatus: String(task.taskStatus),
115
+ active: task.taskStatus !== "Completed" && task.taskStatus !== "Dropped",
116
+ dueDate: task.dueDate,
117
+ deferDate: task.deferDate,
118
+ estimatedMinutes: task.estimatedMinutes ? Number(task.estimatedMinutes) : null,
119
+ tags: task.tags || [],
120
+ tagNames: tagNames,
121
+ parentId: task.parentTaskID || null,
122
+ containingProjectId: task.projectID || null,
123
+ projectId: task.projectID || null,
124
+ childIds: task.children || [],
125
+ hasChildren: (task.children && task.children.length > 0) || false,
126
+ sequential: Boolean(task.sequential),
127
+ completedByChildren: Boolean(task.completedByChildren),
128
+ isRepeating: false, // Not available in the new format
129
+ repetitionMethod: null, // Not available in the new format
130
+ repetitionRule: null, // Not available in the new format
131
+ attachments: [], // Default empty array
132
+ linkedFileURLs: [], // Default empty array
133
+ notifications: [], // Default empty array
134
+ shouldUseFloatingTimeZone: false // Default value
135
+ };
136
+ });
137
+ }
138
+
139
+ // Process projects
140
+ if (data.projects) {
141
+ for (const [id, project] of Object.entries(data.projects)) {
142
+ database.projects[id] = {
143
+ id: String(project.id),
144
+ name: String(project.name),
145
+ status: String(project.status),
146
+ folderID: project.folderID || null,
147
+ sequential: Boolean(project.sequential),
148
+ effectiveDueDate: project.effectiveDueDate,
149
+ effectiveDeferDate: project.effectiveDeferDate,
150
+ dueDate: project.dueDate,
151
+ deferDate: project.deferDate,
152
+ completedByChildren: Boolean(project.completedByChildren),
153
+ containsSingletonActions: Boolean(project.containsSingletonActions),
154
+ note: String(project.note || ""),
155
+ tasks: project.tasks || [],
156
+ flagged: false, // Default value
157
+ estimatedMinutes: null // Default value
158
+ };
159
+ }
160
+ }
161
+
162
+ // Process folders
163
+ if (data.folders) {
164
+ for (const [id, folder] of Object.entries(data.folders)) {
165
+ database.folders[id] = {
166
+ id: String(folder.id),
167
+ name: String(folder.name),
168
+ parentFolderID: folder.parentFolderID || null,
169
+ status: String(folder.status),
170
+ projects: folder.projects || [],
171
+ subfolders: folder.subfolders || []
172
+ };
173
+ }
174
+ }
175
+
176
+ // Process tags
177
+ if (data.tags) {
178
+ for (const [id, tag] of Object.entries(data.tags)) {
179
+ database.tags[id] = {
180
+ id: String(tag.id),
181
+ name: String(tag.name),
182
+ parentTagID: tag.parentTagID || null,
183
+ active: Boolean(tag.active),
184
+ allowsNextAction: Boolean(tag.allowsNextAction),
185
+ tasks: tag.tasks || []
186
+ };
187
+ }
188
+ }
189
+
190
+ return database;
191
+ } catch (error) {
192
+ console.error("Error in dumpDatabase:", error);
193
+ throw error;
194
+ }
195
+ }
196
+