@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.
- package/QUERY_TOOL_EXAMPLES.md +298 -0
- package/QUERY_TOOL_REFERENCE.md +228 -0
- package/README.md +250 -0
- package/assets/omnifocus-mcp-logo.png +0 -0
- package/cli.cjs +9 -0
- package/dist/omnifocustypes.js +48 -0
- package/dist/server.js +44 -0
- package/dist/tools/definitions/addOmniFocusTask.js +76 -0
- package/dist/tools/definitions/addProject.js +61 -0
- package/dist/tools/definitions/batchAddItems.js +89 -0
- package/dist/tools/definitions/batchRemoveItems.js +74 -0
- package/dist/tools/definitions/dumpDatabase.js +259 -0
- package/dist/tools/definitions/editItem.js +88 -0
- package/dist/tools/definitions/getPerspectiveView.js +107 -0
- package/dist/tools/definitions/listPerspectives.js +65 -0
- package/dist/tools/definitions/queryOmnifocus.js +190 -0
- package/dist/tools/definitions/removeItem.js +80 -0
- package/dist/tools/dumpDatabase.js +121 -0
- package/dist/tools/dumpDatabaseOptimized.js +192 -0
- package/dist/tools/primitives/addOmniFocusTask.js +227 -0
- package/dist/tools/primitives/addProject.js +132 -0
- package/dist/tools/primitives/batchAddItems.js +166 -0
- package/dist/tools/primitives/batchRemoveItems.js +44 -0
- package/dist/tools/primitives/editItem.js +443 -0
- package/dist/tools/primitives/getPerspectiveView.js +50 -0
- package/dist/tools/primitives/listPerspectives.js +34 -0
- package/dist/tools/primitives/queryOmnifocus.js +365 -0
- package/dist/tools/primitives/queryOmnifocusDebug.js +135 -0
- package/dist/tools/primitives/removeItem.js +177 -0
- package/dist/types.js +1 -0
- package/dist/utils/cacheManager.js +187 -0
- package/dist/utils/dateFormatting.js +58 -0
- package/dist/utils/omnifocusScripts/getPerspectiveView.js +169 -0
- package/dist/utils/omnifocusScripts/listPerspectives.js +59 -0
- package/dist/utils/omnifocusScripts/omnifocusDump.js +223 -0
- package/dist/utils/scriptExecution.js +113 -0
- package/package.json +37 -0
- package/src/omnifocustypes.ts +89 -0
- package/src/server.ts +109 -0
- package/src/tools/definitions/addOmniFocusTask.ts +80 -0
- package/src/tools/definitions/addProject.ts +67 -0
- package/src/tools/definitions/batchAddItems.ts +98 -0
- package/src/tools/definitions/batchRemoveItems.ts +80 -0
- package/src/tools/definitions/dumpDatabase.ts +311 -0
- package/src/tools/definitions/editItem.ts +96 -0
- package/src/tools/definitions/getPerspectiveView.ts +125 -0
- package/src/tools/definitions/listPerspectives.ts +72 -0
- package/src/tools/definitions/queryOmnifocus.ts +212 -0
- package/src/tools/definitions/removeItem.ts +86 -0
- package/src/tools/dumpDatabase.ts +196 -0
- package/src/tools/dumpDatabaseOptimized.ts +231 -0
- package/src/tools/primitives/addOmniFocusTask.ts +252 -0
- package/src/tools/primitives/addProject.ts +156 -0
- package/src/tools/primitives/batchAddItems.ts +207 -0
- package/src/tools/primitives/batchRemoveItems.ts +64 -0
- package/src/tools/primitives/editItem.ts +507 -0
- package/src/tools/primitives/getPerspectiveView.ts +71 -0
- package/src/tools/primitives/listPerspectives.ts +53 -0
- package/src/tools/primitives/queryOmnifocus.ts +394 -0
- package/src/tools/primitives/queryOmnifocusDebug.ts +139 -0
- package/src/tools/primitives/removeItem.ts +195 -0
- package/src/types.ts +107 -0
- package/src/utils/cacheManager.ts +234 -0
- package/src/utils/dateFormatting.ts +81 -0
- package/src/utils/omnifocusScripts/getPerspectiveView.js +169 -0
- package/src/utils/omnifocusScripts/listPerspectives.js +59 -0
- package/src/utils/omnifocusScripts/omnifocusDump.js +223 -0
- package/src/utils/scriptExecution.ts +128 -0
- 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
|
+
|