@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,195 @@
|
|
|
1
|
+
import { exec } from 'child_process';
|
|
2
|
+
import { promisify } from 'util';
|
|
3
|
+
const execAsync = promisify(exec);
|
|
4
|
+
|
|
5
|
+
// Interface for item removal parameters
|
|
6
|
+
export interface RemoveItemParams {
|
|
7
|
+
id?: string; // ID of the task or project to remove
|
|
8
|
+
name?: string; // Name of the task or project to remove (as fallback if ID not provided)
|
|
9
|
+
itemType: 'task' | 'project'; // Type of item to remove
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Generate pure AppleScript for item removal
|
|
14
|
+
*/
|
|
15
|
+
function generateAppleScript(params: RemoveItemParams): string {
|
|
16
|
+
// Sanitize and prepare parameters for AppleScript
|
|
17
|
+
const id = params.id?.replace(/['"\\]/g, '\\$&') || ''; // Escape quotes and backslashes
|
|
18
|
+
const name = params.name?.replace(/['"\\]/g, '\\$&') || '';
|
|
19
|
+
const itemType = params.itemType;
|
|
20
|
+
|
|
21
|
+
// Verify we have at least one identifier
|
|
22
|
+
if (!id && !name) {
|
|
23
|
+
return `return "{\\\"success\\\":false,\\\"error\\\":\\\"Either id or name must be provided\\\"}"`;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// Construct AppleScript with error handling
|
|
27
|
+
let script = `
|
|
28
|
+
try
|
|
29
|
+
tell application "OmniFocus"
|
|
30
|
+
tell front document
|
|
31
|
+
-- Find the item to remove
|
|
32
|
+
set foundItem to missing value
|
|
33
|
+
`;
|
|
34
|
+
|
|
35
|
+
// Add ID search if provided
|
|
36
|
+
if (id) {
|
|
37
|
+
if (itemType === 'task') {
|
|
38
|
+
script += `
|
|
39
|
+
-- Try to find task by ID (search in projects first, then inbox)
|
|
40
|
+
try
|
|
41
|
+
set foundItem to first flattened task where id = "${id}"
|
|
42
|
+
end try
|
|
43
|
+
|
|
44
|
+
-- If not found in projects, search in inbox
|
|
45
|
+
if foundItem is missing value then
|
|
46
|
+
try
|
|
47
|
+
set foundItem to first inbox task where id = "${id}"
|
|
48
|
+
end try
|
|
49
|
+
end if
|
|
50
|
+
`;
|
|
51
|
+
} else {
|
|
52
|
+
script += `
|
|
53
|
+
-- Try to find project by ID
|
|
54
|
+
try
|
|
55
|
+
set foundItem to first flattened project where id = "${id}"
|
|
56
|
+
end try
|
|
57
|
+
`;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Add name search if provided (and no ID or as fallback)
|
|
62
|
+
if (!id && name) {
|
|
63
|
+
if (itemType === 'task') {
|
|
64
|
+
script += `
|
|
65
|
+
-- Find task by name (search in projects first, then inbox)
|
|
66
|
+
try
|
|
67
|
+
set foundItem to first flattened task where name = "${name}"
|
|
68
|
+
end try
|
|
69
|
+
|
|
70
|
+
-- If not found in projects, search in inbox
|
|
71
|
+
if foundItem is missing value then
|
|
72
|
+
try
|
|
73
|
+
set foundItem to first inbox task where name = "${name}"
|
|
74
|
+
end try
|
|
75
|
+
end if
|
|
76
|
+
`;
|
|
77
|
+
} else {
|
|
78
|
+
script += `
|
|
79
|
+
-- Find project by name
|
|
80
|
+
try
|
|
81
|
+
set foundItem to first flattened project where name = "${name}"
|
|
82
|
+
end try
|
|
83
|
+
`;
|
|
84
|
+
}
|
|
85
|
+
} else if (id && name) {
|
|
86
|
+
if (itemType === 'task') {
|
|
87
|
+
script += `
|
|
88
|
+
-- If ID search failed, try to find by name as fallback
|
|
89
|
+
if foundItem is missing value then
|
|
90
|
+
try
|
|
91
|
+
set foundItem to first flattened task where name = "${name}"
|
|
92
|
+
end try
|
|
93
|
+
end if
|
|
94
|
+
|
|
95
|
+
-- If still not found, search in inbox
|
|
96
|
+
if foundItem is missing value then
|
|
97
|
+
try
|
|
98
|
+
set foundItem to first inbox task where name = "${name}"
|
|
99
|
+
end try
|
|
100
|
+
end if
|
|
101
|
+
`;
|
|
102
|
+
} else {
|
|
103
|
+
script += `
|
|
104
|
+
-- If ID search failed, try to find project by name as fallback
|
|
105
|
+
if foundItem is missing value then
|
|
106
|
+
try
|
|
107
|
+
set foundItem to first flattened project where name = "${name}"
|
|
108
|
+
end try
|
|
109
|
+
end if
|
|
110
|
+
`;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// Add the rest of the script
|
|
115
|
+
script += `
|
|
116
|
+
-- If we found the item, remove it
|
|
117
|
+
if foundItem is not missing value then
|
|
118
|
+
set itemName to name of foundItem
|
|
119
|
+
set itemId to id of foundItem as string
|
|
120
|
+
|
|
121
|
+
-- Delete the item
|
|
122
|
+
delete foundItem
|
|
123
|
+
|
|
124
|
+
-- Return success
|
|
125
|
+
return "{\\\"success\\\":true,\\\"id\\\":\\"" & itemId & "\\",\\\"name\\\":\\"" & itemName & "\\"}"
|
|
126
|
+
else
|
|
127
|
+
-- Item not found
|
|
128
|
+
return "{\\\"success\\\":false,\\\"error\\\":\\\"Item not found\\\"}"
|
|
129
|
+
end if
|
|
130
|
+
end tell
|
|
131
|
+
end tell
|
|
132
|
+
on error errorMessage
|
|
133
|
+
return "{\\\"success\\\":false,\\\"error\\\":\\"" & errorMessage & "\\"}"
|
|
134
|
+
end try
|
|
135
|
+
`;
|
|
136
|
+
|
|
137
|
+
return script;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Remove a task or project from OmniFocus
|
|
142
|
+
*/
|
|
143
|
+
export async function removeItem(params: RemoveItemParams): Promise<{success: boolean, id?: string, name?: string, error?: string}> {
|
|
144
|
+
try {
|
|
145
|
+
// Generate AppleScript
|
|
146
|
+
const script = generateAppleScript(params);
|
|
147
|
+
|
|
148
|
+
console.error("Executing AppleScript for removal...");
|
|
149
|
+
console.error(`Item type: ${params.itemType}, ID: ${params.id || 'not provided'}, Name: ${params.name || 'not provided'}`);
|
|
150
|
+
|
|
151
|
+
// Log a preview of the script for debugging (first few lines)
|
|
152
|
+
const scriptPreview = script.split('\n').slice(0, 10).join('\n') + '\n...';
|
|
153
|
+
console.error("AppleScript preview:\n", scriptPreview);
|
|
154
|
+
|
|
155
|
+
// Execute AppleScript directly
|
|
156
|
+
const { stdout, stderr } = await execAsync(`osascript -e '${script}'`);
|
|
157
|
+
|
|
158
|
+
if (stderr) {
|
|
159
|
+
console.error("AppleScript stderr:", stderr);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
console.error("AppleScript stdout:", stdout);
|
|
163
|
+
|
|
164
|
+
// Parse the result
|
|
165
|
+
try {
|
|
166
|
+
const result = JSON.parse(stdout);
|
|
167
|
+
|
|
168
|
+
// Return the result
|
|
169
|
+
return {
|
|
170
|
+
success: result.success,
|
|
171
|
+
id: result.id,
|
|
172
|
+
name: result.name,
|
|
173
|
+
error: result.error
|
|
174
|
+
};
|
|
175
|
+
} catch (parseError) {
|
|
176
|
+
console.error("Error parsing AppleScript result:", parseError);
|
|
177
|
+
return {
|
|
178
|
+
success: false,
|
|
179
|
+
error: `Failed to parse result: ${stdout}`
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
} catch (error: any) {
|
|
183
|
+
console.error("Error in removeItem execution:", error);
|
|
184
|
+
|
|
185
|
+
// Include more detailed error information
|
|
186
|
+
if (error.message && error.message.includes('syntax error')) {
|
|
187
|
+
console.error("This appears to be an AppleScript syntax error. Review the script generation logic.");
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
return {
|
|
191
|
+
success: false,
|
|
192
|
+
error: error?.message || "Unknown error in removeItem"
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
}
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
export interface OmnifocusTask {
|
|
2
|
+
id: string;
|
|
3
|
+
name: string;
|
|
4
|
+
note: string;
|
|
5
|
+
flagged: boolean;
|
|
6
|
+
|
|
7
|
+
// Status
|
|
8
|
+
completed: boolean;
|
|
9
|
+
completionDate: string | null;
|
|
10
|
+
dropDate: string | null;
|
|
11
|
+
taskStatus: string; // One of Task.Status values
|
|
12
|
+
active: boolean;
|
|
13
|
+
|
|
14
|
+
// Dates
|
|
15
|
+
dueDate: string | null;
|
|
16
|
+
deferDate: string | null;
|
|
17
|
+
estimatedMinutes: number | null;
|
|
18
|
+
|
|
19
|
+
// Organization
|
|
20
|
+
tags: string[]; // Tag IDs
|
|
21
|
+
tagNames: string[]; // Human-readable tag names
|
|
22
|
+
parentId: string | null;
|
|
23
|
+
containingProjectId: string | null;
|
|
24
|
+
projectId: string | null;
|
|
25
|
+
|
|
26
|
+
// Task relationships
|
|
27
|
+
childIds: string[];
|
|
28
|
+
hasChildren: boolean;
|
|
29
|
+
sequential: boolean;
|
|
30
|
+
completedByChildren: boolean;
|
|
31
|
+
|
|
32
|
+
// Recurring task information
|
|
33
|
+
repetitionRule: string | null; // Textual representation of repetition rule
|
|
34
|
+
isRepeating: boolean;
|
|
35
|
+
repetitionMethod: string | null; // Fixed or due-based repetition
|
|
36
|
+
|
|
37
|
+
// Attachments
|
|
38
|
+
attachments: any[]; // FileWrapper representations
|
|
39
|
+
linkedFileURLs: string[];
|
|
40
|
+
|
|
41
|
+
// Notifications
|
|
42
|
+
notifications: any[]; // Task.Notification representations
|
|
43
|
+
|
|
44
|
+
// Settings
|
|
45
|
+
shouldUseFloatingTimeZone: boolean;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export interface OmnifocusDatabase {
|
|
49
|
+
exportDate: string;
|
|
50
|
+
tasks: OmnifocusTask[];
|
|
51
|
+
projects: Record<string, OmnifocusProject>;
|
|
52
|
+
folders: Record<string, OmnifocusFolder>;
|
|
53
|
+
tags: Record<string, OmnifocusTag>;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export interface OmnifocusProject {
|
|
57
|
+
id: string;
|
|
58
|
+
name: string;
|
|
59
|
+
status: string;
|
|
60
|
+
folderID: string | null;
|
|
61
|
+
sequential: boolean;
|
|
62
|
+
effectiveDueDate: string | null;
|
|
63
|
+
effectiveDeferDate: string | null;
|
|
64
|
+
dueDate: string | null;
|
|
65
|
+
deferDate: string | null;
|
|
66
|
+
completedByChildren: boolean;
|
|
67
|
+
containsSingletonActions: boolean;
|
|
68
|
+
note: string;
|
|
69
|
+
tasks: string[]; // Task IDs
|
|
70
|
+
flagged?: boolean;
|
|
71
|
+
estimatedMinutes?: number | null;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export interface OmnifocusFolder {
|
|
75
|
+
id: string;
|
|
76
|
+
name: string;
|
|
77
|
+
parentFolderID: string | null;
|
|
78
|
+
status: string;
|
|
79
|
+
projects: string[]; // Project IDs
|
|
80
|
+
subfolders: string[]; // Subfolder IDs
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export interface OmnifocusTag {
|
|
84
|
+
id: string;
|
|
85
|
+
name: string;
|
|
86
|
+
parentTagID: string | null;
|
|
87
|
+
active: boolean;
|
|
88
|
+
allowsNextAction: boolean;
|
|
89
|
+
tasks: string[]; // Task IDs
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export interface OmnifocusPerspective {
|
|
93
|
+
id: string;
|
|
94
|
+
name: string;
|
|
95
|
+
type: 'builtin' | 'custom';
|
|
96
|
+
isBuiltIn: boolean;
|
|
97
|
+
canModify: boolean; // false for built-in perspectives
|
|
98
|
+
// Filter rules for custom perspectives (if applicable)
|
|
99
|
+
filterRules?: {
|
|
100
|
+
availability?: string[];
|
|
101
|
+
tags?: string[];
|
|
102
|
+
projects?: string[];
|
|
103
|
+
flagged?: boolean;
|
|
104
|
+
dueWithin?: number;
|
|
105
|
+
// Additional filter properties as needed
|
|
106
|
+
};
|
|
107
|
+
}
|
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
import { OmnifocusDatabase } from '../types.js';
|
|
2
|
+
import { executeOmniFocusScript } from './scriptExecution.js';
|
|
3
|
+
|
|
4
|
+
interface CacheEntry {
|
|
5
|
+
data: OmnifocusDatabase;
|
|
6
|
+
timestamp: Date;
|
|
7
|
+
checksum?: string;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
interface CacheOptions {
|
|
11
|
+
ttlSeconds?: number; // Time to live in seconds
|
|
12
|
+
maxSize?: number; // Maximum cache size in MB
|
|
13
|
+
useChecksum?: boolean; // Whether to use checksum for validation
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
class OmniFocusCacheManager {
|
|
17
|
+
private cache: Map<string, CacheEntry> = new Map();
|
|
18
|
+
private options: Required<CacheOptions>;
|
|
19
|
+
private totalSize: number = 0;
|
|
20
|
+
|
|
21
|
+
constructor(options: CacheOptions = {}) {
|
|
22
|
+
this.options = {
|
|
23
|
+
ttlSeconds: options.ttlSeconds ?? 300, // 5 minutes default
|
|
24
|
+
maxSize: options.maxSize ?? 50, // 50MB default
|
|
25
|
+
useChecksum: options.useChecksum ?? true
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Get cached data if valid, otherwise return null
|
|
31
|
+
*/
|
|
32
|
+
async get(key: string): Promise<OmnifocusDatabase | null> {
|
|
33
|
+
const entry = this.cache.get(key);
|
|
34
|
+
|
|
35
|
+
if (!entry) {
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Check if cache has expired
|
|
40
|
+
const age = Date.now() - entry.timestamp.getTime();
|
|
41
|
+
if (age > this.options.ttlSeconds * 1000) {
|
|
42
|
+
this.cache.delete(key);
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// If using checksums, validate the cache is still current
|
|
47
|
+
if (this.options.useChecksum && entry.checksum) {
|
|
48
|
+
const currentChecksum = await this.getDatabaseChecksum();
|
|
49
|
+
if (currentChecksum !== entry.checksum) {
|
|
50
|
+
this.cache.delete(key);
|
|
51
|
+
return null;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return entry.data;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Store data in cache
|
|
60
|
+
*/
|
|
61
|
+
async set(key: string, data: OmnifocusDatabase): Promise<void> {
|
|
62
|
+
// Estimate size (rough approximation)
|
|
63
|
+
const dataSize = JSON.stringify(data).length / (1024 * 1024); // Convert to MB
|
|
64
|
+
|
|
65
|
+
// Check if adding this would exceed max size
|
|
66
|
+
if (this.totalSize + dataSize > this.options.maxSize) {
|
|
67
|
+
this.evictOldest();
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const checksum = this.options.useChecksum ? await this.getDatabaseChecksum() : undefined;
|
|
71
|
+
|
|
72
|
+
this.cache.set(key, {
|
|
73
|
+
data,
|
|
74
|
+
timestamp: new Date(),
|
|
75
|
+
checksum
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
this.totalSize += dataSize;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Invalidate specific cache entry
|
|
83
|
+
*/
|
|
84
|
+
invalidate(key: string): void {
|
|
85
|
+
this.cache.delete(key);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Clear all cache entries
|
|
90
|
+
*/
|
|
91
|
+
clear(): void {
|
|
92
|
+
this.cache.clear();
|
|
93
|
+
this.totalSize = 0;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Get cache statistics
|
|
98
|
+
*/
|
|
99
|
+
getStats(): {
|
|
100
|
+
entries: number;
|
|
101
|
+
sizeEstimateMB: number;
|
|
102
|
+
oldestEntry: Date | null;
|
|
103
|
+
hitRate: number;
|
|
104
|
+
} {
|
|
105
|
+
let oldestEntry: Date | null = null;
|
|
106
|
+
|
|
107
|
+
this.cache.forEach(entry => {
|
|
108
|
+
if (!oldestEntry || entry.timestamp < oldestEntry) {
|
|
109
|
+
oldestEntry = entry.timestamp;
|
|
110
|
+
}
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
return {
|
|
114
|
+
entries: this.cache.size,
|
|
115
|
+
sizeEstimateMB: this.totalSize,
|
|
116
|
+
oldestEntry,
|
|
117
|
+
hitRate: this.calculateHitRate()
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Get a lightweight checksum of the database state
|
|
123
|
+
*/
|
|
124
|
+
private async getDatabaseChecksum(): Promise<string> {
|
|
125
|
+
try {
|
|
126
|
+
const script = `
|
|
127
|
+
(() => {
|
|
128
|
+
try {
|
|
129
|
+
// Get counts and latest modification times as a simple checksum
|
|
130
|
+
const taskCount = flattenedTasks.length;
|
|
131
|
+
const projectCount = flattenedProjects.length;
|
|
132
|
+
|
|
133
|
+
// Get the most recent modification time
|
|
134
|
+
let latestMod = new Date(0);
|
|
135
|
+
|
|
136
|
+
flattenedTasks.forEach(task => {
|
|
137
|
+
if (task.modificationDate && task.modificationDate > latestMod) {
|
|
138
|
+
latestMod = task.modificationDate;
|
|
139
|
+
}
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
flattenedProjects.forEach(project => {
|
|
143
|
+
if (project.modificationDate && project.modificationDate > latestMod) {
|
|
144
|
+
latestMod = project.modificationDate;
|
|
145
|
+
}
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
// Create a simple checksum string
|
|
149
|
+
const checksum = taskCount + "-" + projectCount + "-" + latestMod.getTime();
|
|
150
|
+
|
|
151
|
+
return JSON.stringify({ checksum });
|
|
152
|
+
} catch (error) {
|
|
153
|
+
return JSON.stringify({ checksum: "error-" + Date.now() });
|
|
154
|
+
}
|
|
155
|
+
})();
|
|
156
|
+
`;
|
|
157
|
+
|
|
158
|
+
// Write to temp file and execute
|
|
159
|
+
const fs = await import('fs');
|
|
160
|
+
const tempFile = `/tmp/omnifocus_checksum_${Date.now()}.js`;
|
|
161
|
+
fs.writeFileSync(tempFile, script);
|
|
162
|
+
|
|
163
|
+
const result = await executeOmniFocusScript(tempFile);
|
|
164
|
+
fs.unlinkSync(tempFile);
|
|
165
|
+
|
|
166
|
+
return result.checksum || `fallback-${Date.now()}`;
|
|
167
|
+
} catch (error) {
|
|
168
|
+
console.error('Error getting database checksum:', error);
|
|
169
|
+
return `error-${Date.now()}`;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Evict oldest cache entries when size limit is reached
|
|
175
|
+
*/
|
|
176
|
+
private evictOldest(): void {
|
|
177
|
+
let oldestKey: string | null = null;
|
|
178
|
+
let oldestTime = new Date();
|
|
179
|
+
|
|
180
|
+
this.cache.forEach((entry, key) => {
|
|
181
|
+
if (entry.timestamp < oldestTime) {
|
|
182
|
+
oldestTime = entry.timestamp;
|
|
183
|
+
oldestKey = key;
|
|
184
|
+
}
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
if (oldestKey) {
|
|
188
|
+
this.cache.delete(oldestKey);
|
|
189
|
+
// Recalculate total size (simplified for now)
|
|
190
|
+
this.totalSize *= 0.9;
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// Tracking for hit rate calculation
|
|
195
|
+
private hits = 0;
|
|
196
|
+
private misses = 0;
|
|
197
|
+
|
|
198
|
+
private calculateHitRate(): number {
|
|
199
|
+
const total = this.hits + this.misses;
|
|
200
|
+
if (total === 0) return 0;
|
|
201
|
+
return (this.hits / total) * 100;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
trackHit(): void {
|
|
205
|
+
this.hits++;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
trackMiss(): void {
|
|
209
|
+
this.misses++;
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// Singleton instance
|
|
214
|
+
let cacheManager: OmniFocusCacheManager | null = null;
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* Get or create the cache manager instance
|
|
218
|
+
*/
|
|
219
|
+
export function getCacheManager(options?: CacheOptions): OmniFocusCacheManager {
|
|
220
|
+
if (!cacheManager) {
|
|
221
|
+
cacheManager = new OmniFocusCacheManager(options);
|
|
222
|
+
}
|
|
223
|
+
return cacheManager;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* Reset the cache manager (useful for testing)
|
|
228
|
+
*/
|
|
229
|
+
export function resetCacheManager(): void {
|
|
230
|
+
if (cacheManager) {
|
|
231
|
+
cacheManager.clear();
|
|
232
|
+
}
|
|
233
|
+
cacheManager = null;
|
|
234
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
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
|
+
/**
|
|
7
|
+
* Generate AppleScript to construct a date variable outside tell blocks
|
|
8
|
+
* @param isoDateString - ISO format date string
|
|
9
|
+
* @param varName - Name for the date variable
|
|
10
|
+
* @returns AppleScript code to construct the date
|
|
11
|
+
*/
|
|
12
|
+
export function createDateOutsideTellBlock(isoDateString: string, varName: string): string {
|
|
13
|
+
// Parse the ISO date string
|
|
14
|
+
const date = new Date(isoDateString);
|
|
15
|
+
|
|
16
|
+
// Check if the date is valid
|
|
17
|
+
if (isNaN(date.getTime())) {
|
|
18
|
+
throw new Error(`Invalid date string: ${isoDateString}`);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// Extract date components
|
|
22
|
+
const year = date.getFullYear();
|
|
23
|
+
const month = date.getMonth() + 1; // JavaScript months are 0-indexed
|
|
24
|
+
const day = date.getDate();
|
|
25
|
+
const hours = date.getHours();
|
|
26
|
+
const minutes = date.getMinutes();
|
|
27
|
+
const seconds = date.getSeconds();
|
|
28
|
+
|
|
29
|
+
// Generate AppleScript to construct date outside tell blocks
|
|
30
|
+
return `copy current date to ${varName}
|
|
31
|
+
set year of ${varName} to ${year}
|
|
32
|
+
set month of ${varName} to ${month}
|
|
33
|
+
set day of ${varName} to ${day}
|
|
34
|
+
set hours of ${varName} to ${hours}
|
|
35
|
+
set minutes of ${varName} to ${minutes}
|
|
36
|
+
set seconds of ${varName} to ${seconds}`;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Generate the complete AppleScript for date assignments
|
|
41
|
+
* Returns both the pre-tell block code and the in-tell block assignment
|
|
42
|
+
*/
|
|
43
|
+
export interface DateAssignmentParts {
|
|
44
|
+
preScript: string; // Code to run before tell blocks
|
|
45
|
+
assignmentScript: string; // Code to run inside tell blocks
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Generate date assignment that works with AppleScript restrictions
|
|
50
|
+
*/
|
|
51
|
+
export function generateDateAssignmentV2(
|
|
52
|
+
objectName: string,
|
|
53
|
+
propertyName: string,
|
|
54
|
+
isoDateString: string | undefined
|
|
55
|
+
): DateAssignmentParts | null {
|
|
56
|
+
if (isoDateString === undefined) {
|
|
57
|
+
return null; // No date change requested
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
if (isoDateString === '') {
|
|
61
|
+
// Clear the date
|
|
62
|
+
return {
|
|
63
|
+
preScript: '',
|
|
64
|
+
assignmentScript: `set ${propertyName} of ${objectName} to missing value`
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Generate unique variable name
|
|
69
|
+
const varName = `dateVar${Math.random().toString(36).substr(2, 9)}`;
|
|
70
|
+
|
|
71
|
+
// Generate the date construction (outside tell blocks)
|
|
72
|
+
const preScript = createDateOutsideTellBlock(isoDateString, varName);
|
|
73
|
+
|
|
74
|
+
// Generate the assignment (inside tell blocks)
|
|
75
|
+
const assignmentScript = `set ${propertyName} of ${objectName} to ${varName}`;
|
|
76
|
+
|
|
77
|
+
return {
|
|
78
|
+
preScript,
|
|
79
|
+
assignmentScript
|
|
80
|
+
};
|
|
81
|
+
}
|