@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,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
|
+
}
|