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