@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,231 @@
|
|
|
1
|
+
import { OmnifocusDatabase } from '../types.js';
|
|
2
|
+
import { executeOmniFocusScript } from '../utils/scriptExecution.js';
|
|
3
|
+
import { getCacheManager } from '../utils/cacheManager.js';
|
|
4
|
+
import { dumpDatabase as originalDumpDatabase } from './dumpDatabase.js';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Optimized version of dumpDatabase that uses caching
|
|
8
|
+
* Falls back to original implementation if caching fails
|
|
9
|
+
*/
|
|
10
|
+
export async function dumpDatabaseOptimized(options?: {
|
|
11
|
+
forceRefresh?: boolean;
|
|
12
|
+
cacheKey?: string;
|
|
13
|
+
}): Promise<OmnifocusDatabase> {
|
|
14
|
+
const cacheManager = getCacheManager({
|
|
15
|
+
ttlSeconds: 300, // 5 minute cache
|
|
16
|
+
useChecksum: true // Validate with database checksum
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
const cacheKey = options?.cacheKey || 'full-dump';
|
|
20
|
+
|
|
21
|
+
// Check if we should force a refresh
|
|
22
|
+
if (!options?.forceRefresh) {
|
|
23
|
+
const cached = await cacheManager.get(cacheKey);
|
|
24
|
+
if (cached) {
|
|
25
|
+
cacheManager.trackHit();
|
|
26
|
+
console.log(`Cache hit for ${cacheKey}. Stats:`, cacheManager.getStats());
|
|
27
|
+
return cached;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
cacheManager.trackMiss();
|
|
32
|
+
console.log(`Cache miss for ${cacheKey}. Fetching fresh data...`);
|
|
33
|
+
|
|
34
|
+
// Fetch fresh data using the original implementation
|
|
35
|
+
const freshData = await originalDumpDatabase();
|
|
36
|
+
|
|
37
|
+
// Store in cache for next time
|
|
38
|
+
await cacheManager.set(cacheKey, freshData);
|
|
39
|
+
|
|
40
|
+
return freshData;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Get just database statistics without full dump
|
|
45
|
+
* Much more efficient for overview information
|
|
46
|
+
*/
|
|
47
|
+
export async function getDatabaseStats(): Promise<{
|
|
48
|
+
taskCount: number;
|
|
49
|
+
activeTaskCount: number;
|
|
50
|
+
projectCount: number;
|
|
51
|
+
activeProjectCount: number;
|
|
52
|
+
folderCount: number;
|
|
53
|
+
tagCount: number;
|
|
54
|
+
overdueCount: number;
|
|
55
|
+
nextActionCount: number;
|
|
56
|
+
flaggedCount: number;
|
|
57
|
+
inboxCount: number;
|
|
58
|
+
lastModified: string;
|
|
59
|
+
}> {
|
|
60
|
+
const script = `
|
|
61
|
+
(() => {
|
|
62
|
+
try {
|
|
63
|
+
// Calculate statistics without fetching full data
|
|
64
|
+
const allTasks = flattenedTasks;
|
|
65
|
+
const activeTasks = allTasks.filter(task =>
|
|
66
|
+
task.taskStatus !== Task.Status.Completed &&
|
|
67
|
+
task.taskStatus !== Task.Status.Dropped
|
|
68
|
+
);
|
|
69
|
+
|
|
70
|
+
const allProjects = flattenedProjects;
|
|
71
|
+
const activeProjects = allProjects.filter(project =>
|
|
72
|
+
project.status === Project.Status.Active
|
|
73
|
+
);
|
|
74
|
+
|
|
75
|
+
// Count specific task statuses
|
|
76
|
+
const overdueCount = activeTasks.filter(task =>
|
|
77
|
+
task.taskStatus === Task.Status.Overdue
|
|
78
|
+
).length;
|
|
79
|
+
|
|
80
|
+
const nextActionCount = activeTasks.filter(task =>
|
|
81
|
+
task.taskStatus === Task.Status.Next
|
|
82
|
+
).length;
|
|
83
|
+
|
|
84
|
+
const flaggedCount = activeTasks.filter(task => task.flagged).length;
|
|
85
|
+
|
|
86
|
+
const inboxCount = activeTasks.filter(task => task.inInbox).length;
|
|
87
|
+
|
|
88
|
+
// Get latest modification time
|
|
89
|
+
let lastModified = new Date(0);
|
|
90
|
+
allTasks.forEach(task => {
|
|
91
|
+
if (task.modificationDate && task.modificationDate > lastModified) {
|
|
92
|
+
lastModified = task.modificationDate;
|
|
93
|
+
}
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
return JSON.stringify({
|
|
97
|
+
taskCount: allTasks.length,
|
|
98
|
+
activeTaskCount: activeTasks.length,
|
|
99
|
+
projectCount: allProjects.length,
|
|
100
|
+
activeProjectCount: activeProjects.length,
|
|
101
|
+
folderCount: flattenedFolders.length,
|
|
102
|
+
tagCount: flattenedTags.filter(tag => tag.active).length,
|
|
103
|
+
overdueCount: overdueCount,
|
|
104
|
+
nextActionCount: nextActionCount,
|
|
105
|
+
flaggedCount: flaggedCount,
|
|
106
|
+
inboxCount: inboxCount,
|
|
107
|
+
lastModified: lastModified.toISOString()
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
} catch (error) {
|
|
111
|
+
return JSON.stringify({
|
|
112
|
+
error: "Failed to get database stats: " + error.toString()
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
})();
|
|
116
|
+
`;
|
|
117
|
+
|
|
118
|
+
// Write script to temp file and execute
|
|
119
|
+
const fs = await import('fs');
|
|
120
|
+
const tempFile = `/tmp/omnifocus_stats_${Date.now()}.js`;
|
|
121
|
+
fs.writeFileSync(tempFile, script);
|
|
122
|
+
|
|
123
|
+
const result = await executeOmniFocusScript(tempFile);
|
|
124
|
+
fs.unlinkSync(tempFile);
|
|
125
|
+
|
|
126
|
+
if (result.error) {
|
|
127
|
+
throw new Error(result.error);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
return result;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Get incremental changes since a specific timestamp
|
|
135
|
+
* Much more efficient for periodic updates
|
|
136
|
+
*/
|
|
137
|
+
export async function getChangesSince(since: Date): Promise<{
|
|
138
|
+
newTasks: any[];
|
|
139
|
+
updatedTasks: any[];
|
|
140
|
+
completedTasks: any[];
|
|
141
|
+
newProjects: any[];
|
|
142
|
+
updatedProjects: any[];
|
|
143
|
+
}> {
|
|
144
|
+
const script = `
|
|
145
|
+
(() => {
|
|
146
|
+
try {
|
|
147
|
+
const sinceDate = new Date("${since.toISOString()}");
|
|
148
|
+
|
|
149
|
+
// Find tasks that changed since the given date
|
|
150
|
+
const allTasks = flattenedTasks;
|
|
151
|
+
|
|
152
|
+
const newTasks = allTasks.filter(task =>
|
|
153
|
+
task.creationDate && task.creationDate > sinceDate
|
|
154
|
+
).map(task => ({
|
|
155
|
+
id: task.id.primaryKey,
|
|
156
|
+
name: task.name,
|
|
157
|
+
creationDate: task.creationDate.toISOString()
|
|
158
|
+
}));
|
|
159
|
+
|
|
160
|
+
const updatedTasks = allTasks.filter(task =>
|
|
161
|
+
task.modificationDate &&
|
|
162
|
+
task.modificationDate > sinceDate &&
|
|
163
|
+
task.creationDate &&
|
|
164
|
+
task.creationDate <= sinceDate
|
|
165
|
+
).map(task => ({
|
|
166
|
+
id: task.id.primaryKey,
|
|
167
|
+
name: task.name,
|
|
168
|
+
modificationDate: task.modificationDate.toISOString()
|
|
169
|
+
}));
|
|
170
|
+
|
|
171
|
+
const completedTasks = allTasks.filter(task =>
|
|
172
|
+
task.completionDate &&
|
|
173
|
+
task.completionDate > sinceDate
|
|
174
|
+
).map(task => ({
|
|
175
|
+
id: task.id.primaryKey,
|
|
176
|
+
name: task.name,
|
|
177
|
+
completionDate: task.completionDate.toISOString()
|
|
178
|
+
}));
|
|
179
|
+
|
|
180
|
+
// Find projects that changed
|
|
181
|
+
const allProjects = flattenedProjects;
|
|
182
|
+
|
|
183
|
+
const newProjects = allProjects.filter(project =>
|
|
184
|
+
project.creationDate && project.creationDate > sinceDate
|
|
185
|
+
).map(project => ({
|
|
186
|
+
id: project.id.primaryKey,
|
|
187
|
+
name: project.name,
|
|
188
|
+
creationDate: project.creationDate.toISOString()
|
|
189
|
+
}));
|
|
190
|
+
|
|
191
|
+
const updatedProjects = allProjects.filter(project =>
|
|
192
|
+
project.modificationDate &&
|
|
193
|
+
project.modificationDate > sinceDate &&
|
|
194
|
+
project.creationDate &&
|
|
195
|
+
project.creationDate <= sinceDate
|
|
196
|
+
).map(project => ({
|
|
197
|
+
id: project.id.primaryKey,
|
|
198
|
+
name: project.name,
|
|
199
|
+
modificationDate: project.modificationDate.toISOString()
|
|
200
|
+
}));
|
|
201
|
+
|
|
202
|
+
return JSON.stringify({
|
|
203
|
+
newTasks: newTasks,
|
|
204
|
+
updatedTasks: updatedTasks,
|
|
205
|
+
completedTasks: completedTasks,
|
|
206
|
+
newProjects: newProjects,
|
|
207
|
+
updatedProjects: updatedProjects
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
} catch (error) {
|
|
211
|
+
return JSON.stringify({
|
|
212
|
+
error: "Failed to get changes: " + error.toString()
|
|
213
|
+
});
|
|
214
|
+
}
|
|
215
|
+
})();
|
|
216
|
+
`;
|
|
217
|
+
|
|
218
|
+
// Write script to temp file and execute
|
|
219
|
+
const fs = await import('fs');
|
|
220
|
+
const tempFile = `/tmp/omnifocus_changes_${Date.now()}.js`;
|
|
221
|
+
fs.writeFileSync(tempFile, script);
|
|
222
|
+
|
|
223
|
+
const result = await executeOmniFocusScript(tempFile);
|
|
224
|
+
fs.unlinkSync(tempFile);
|
|
225
|
+
|
|
226
|
+
if (result.error) {
|
|
227
|
+
throw new Error(result.error);
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
return result;
|
|
231
|
+
}
|
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
import { exec } from 'child_process';
|
|
2
|
+
import { promisify } from 'util';
|
|
3
|
+
import { writeFileSync, unlinkSync } from 'fs';
|
|
4
|
+
import { tmpdir } from 'os';
|
|
5
|
+
import { join } from 'path';
|
|
6
|
+
import { createDateOutsideTellBlock } from '../../utils/dateFormatting.js';
|
|
7
|
+
const execAsync = promisify(exec);
|
|
8
|
+
|
|
9
|
+
// Interface for task creation parameters
|
|
10
|
+
export interface AddOmniFocusTaskParams {
|
|
11
|
+
name: string;
|
|
12
|
+
note?: string;
|
|
13
|
+
dueDate?: string; // ISO date string
|
|
14
|
+
deferDate?: string; // ISO date string
|
|
15
|
+
flagged?: boolean;
|
|
16
|
+
estimatedMinutes?: number;
|
|
17
|
+
tags?: string[]; // Tag names
|
|
18
|
+
projectName?: string; // Project name to add task to
|
|
19
|
+
// Hierarchy support
|
|
20
|
+
parentTaskId?: string;
|
|
21
|
+
parentTaskName?: string;
|
|
22
|
+
hierarchyLevel?: number; // ignored for single add
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Generate pure AppleScript for task creation
|
|
27
|
+
*/
|
|
28
|
+
function generateAppleScript(params: AddOmniFocusTaskParams): string {
|
|
29
|
+
// Sanitize and prepare parameters for AppleScript
|
|
30
|
+
const name = params.name.replace(/['"\\]/g, '\\$&'); // Escape quotes and backslashes
|
|
31
|
+
const note = params.note?.replace(/['"\\]/g, '\\$&') || '';
|
|
32
|
+
const dueDate = params.dueDate || '';
|
|
33
|
+
const deferDate = params.deferDate || '';
|
|
34
|
+
const flagged = params.flagged === true;
|
|
35
|
+
const estimatedMinutes = params.estimatedMinutes?.toString() || '';
|
|
36
|
+
const tags = params.tags || [];
|
|
37
|
+
const projectName = params.projectName?.replace(/['"\\]/g, '\\$&') || '';
|
|
38
|
+
const parentTaskId = params.parentTaskId?.replace(/['"\\]/g, '\\$&') || '';
|
|
39
|
+
const parentTaskName = params.parentTaskName?.replace(/['"\\]/g, '\\$&') || '';
|
|
40
|
+
|
|
41
|
+
// Generate date constructions outside tell blocks
|
|
42
|
+
let datePreScript = '';
|
|
43
|
+
let dueDateVar = '';
|
|
44
|
+
let deferDateVar = '';
|
|
45
|
+
|
|
46
|
+
if (dueDate) {
|
|
47
|
+
dueDateVar = `dueDate${Math.random().toString(36).substr(2, 9)}`;
|
|
48
|
+
datePreScript += createDateOutsideTellBlock(dueDate, dueDateVar) + '\n\n';
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if (deferDate) {
|
|
52
|
+
deferDateVar = `deferDate${Math.random().toString(36).substr(2, 9)}`;
|
|
53
|
+
datePreScript += createDateOutsideTellBlock(deferDate, deferDateVar) + '\n\n';
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Construct AppleScript with error handling
|
|
57
|
+
let script = datePreScript + `
|
|
58
|
+
try
|
|
59
|
+
tell application "OmniFocus"
|
|
60
|
+
tell front document
|
|
61
|
+
-- Resolve parent task if provided
|
|
62
|
+
set newTask to missing value
|
|
63
|
+
set parentTask to missing value
|
|
64
|
+
set placement to ""
|
|
65
|
+
|
|
66
|
+
if "${parentTaskId}" is not "" then
|
|
67
|
+
try
|
|
68
|
+
set parentTask to first flattened task where id = "${parentTaskId}"
|
|
69
|
+
end try
|
|
70
|
+
if parentTask is missing value then
|
|
71
|
+
try
|
|
72
|
+
set parentTask to first inbox task where id = "${parentTaskId}"
|
|
73
|
+
end try
|
|
74
|
+
end if
|
|
75
|
+
-- If projectName provided, ensure parent is within that project
|
|
76
|
+
if parentTask is not missing value and "${projectName}" is not "" then
|
|
77
|
+
try
|
|
78
|
+
set pproj to containing project of parentTask
|
|
79
|
+
if pproj is missing value or name of pproj is not "${projectName}" then set parentTask to missing value
|
|
80
|
+
end try
|
|
81
|
+
end if
|
|
82
|
+
end if
|
|
83
|
+
|
|
84
|
+
if parentTask is missing value and "${parentTaskName}" is not "" then
|
|
85
|
+
if "${projectName}" is not "" then
|
|
86
|
+
-- Find by name but constrain to the specified project
|
|
87
|
+
try
|
|
88
|
+
set parentTask to first flattened task where name = "${parentTaskName}"
|
|
89
|
+
end try
|
|
90
|
+
if parentTask is not missing value then
|
|
91
|
+
try
|
|
92
|
+
set pproj to containing project of parentTask
|
|
93
|
+
if pproj is missing value or name of pproj is not "${projectName}" then set parentTask to missing value
|
|
94
|
+
end try
|
|
95
|
+
end if
|
|
96
|
+
else
|
|
97
|
+
-- No project specified; allow global or inbox match by name
|
|
98
|
+
try
|
|
99
|
+
set parentTask to first flattened task where name = "${parentTaskName}"
|
|
100
|
+
end try
|
|
101
|
+
if parentTask is missing value then
|
|
102
|
+
try
|
|
103
|
+
set parentTask to first inbox task where name = "${parentTaskName}"
|
|
104
|
+
end try
|
|
105
|
+
end if
|
|
106
|
+
end if
|
|
107
|
+
end if
|
|
108
|
+
|
|
109
|
+
if parentTask is not missing value then
|
|
110
|
+
-- Create task under parent task
|
|
111
|
+
set newTask to make new task with properties {name:"${name}"} at end of tasks of parentTask
|
|
112
|
+
else if "${projectName}" is not "" then
|
|
113
|
+
-- Create under specified project
|
|
114
|
+
try
|
|
115
|
+
set theProject to first flattened project where name = "${projectName}"
|
|
116
|
+
set newTask to make new task with properties {name:"${name}"} at end of tasks of theProject
|
|
117
|
+
on error
|
|
118
|
+
return "{\\\"success\\\":false,\\\"error\\\":\\\"Project not found: ${projectName}\\\"}"
|
|
119
|
+
end try
|
|
120
|
+
else
|
|
121
|
+
-- Fallback to inbox
|
|
122
|
+
set newTask to make new inbox task with properties {name:"${name}"}
|
|
123
|
+
end if
|
|
124
|
+
|
|
125
|
+
-- Set task properties
|
|
126
|
+
${note ? `set note of newTask to "${note}"` : ''}
|
|
127
|
+
${dueDate ? `
|
|
128
|
+
-- Set due date
|
|
129
|
+
set due date of newTask to ` + dueDateVar : ''}
|
|
130
|
+
${deferDate ? `
|
|
131
|
+
-- Set defer date
|
|
132
|
+
set defer date of newTask to ` + deferDateVar : ''}
|
|
133
|
+
${flagged ? `set flagged of newTask to true` : ''}
|
|
134
|
+
${estimatedMinutes ? `set estimated minutes of newTask to ${estimatedMinutes}` : ''}
|
|
135
|
+
|
|
136
|
+
-- Derive placement from container; distinguish real parent vs project root task
|
|
137
|
+
try
|
|
138
|
+
set placement to "inbox"
|
|
139
|
+
set ctr to container of newTask
|
|
140
|
+
set cclass to class of ctr as string
|
|
141
|
+
set ctrId to id of ctr as string
|
|
142
|
+
if cclass is "project" then
|
|
143
|
+
set placement to "project"
|
|
144
|
+
else if cclass is "task" then
|
|
145
|
+
if parentTask is not missing value then
|
|
146
|
+
set parentId to id of parentTask as string
|
|
147
|
+
if ctrId is equal to parentId then
|
|
148
|
+
set placement to "parent"
|
|
149
|
+
else
|
|
150
|
+
-- Likely the project's root task; treat as project
|
|
151
|
+
set placement to "project"
|
|
152
|
+
end if
|
|
153
|
+
else
|
|
154
|
+
-- No explicit parent requested; container is root task -> treat as project
|
|
155
|
+
set placement to "project"
|
|
156
|
+
end if
|
|
157
|
+
else
|
|
158
|
+
set placement to "inbox"
|
|
159
|
+
end if
|
|
160
|
+
on error
|
|
161
|
+
-- If container access fails (e.g., inbox), default based on projectName
|
|
162
|
+
if "${projectName}" is not "" then
|
|
163
|
+
set placement to "project"
|
|
164
|
+
else
|
|
165
|
+
set placement to "inbox"
|
|
166
|
+
end if
|
|
167
|
+
end try
|
|
168
|
+
|
|
169
|
+
-- Get the task ID
|
|
170
|
+
set taskId to id of newTask as string
|
|
171
|
+
|
|
172
|
+
-- Add tags if provided
|
|
173
|
+
${tags.length > 0 ? tags.map(tag => {
|
|
174
|
+
const sanitizedTag = tag.replace(/['"\\]/g, '\\$&');
|
|
175
|
+
return `
|
|
176
|
+
try
|
|
177
|
+
set theTag to first flattened tag where name = "${sanitizedTag}"
|
|
178
|
+
add theTag to tags of newTask
|
|
179
|
+
on error
|
|
180
|
+
-- Tag might not exist, try to create it
|
|
181
|
+
try
|
|
182
|
+
set theTag to make new tag with properties {name:"${sanitizedTag}"}
|
|
183
|
+
add theTag to tags of newTask
|
|
184
|
+
on error
|
|
185
|
+
-- Could not create or add tag
|
|
186
|
+
end try
|
|
187
|
+
end try`;
|
|
188
|
+
}).join('\n') : ''}
|
|
189
|
+
|
|
190
|
+
-- Return success with task ID and placement
|
|
191
|
+
return "{\\\"success\\\":true,\\\"taskId\\\":\\"" & taskId & "\\",\\\"name\\\":\\"${name}\\\",\\\"placement\\\":\\"" & placement & "\\"}"
|
|
192
|
+
end tell
|
|
193
|
+
end tell
|
|
194
|
+
on error errorMessage
|
|
195
|
+
return "{\\\"success\\\":false,\\\"error\\\":\\"" & errorMessage & "\\"}"
|
|
196
|
+
end try
|
|
197
|
+
`;
|
|
198
|
+
|
|
199
|
+
return script;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* Add a task to OmniFocus
|
|
204
|
+
*/
|
|
205
|
+
export async function addOmniFocusTask(params: AddOmniFocusTaskParams): Promise<{success: boolean, taskId?: string, error?: string, placement?: 'parent' | 'project' | 'inbox'}> {
|
|
206
|
+
try {
|
|
207
|
+
// Generate AppleScript
|
|
208
|
+
const script = generateAppleScript(params);
|
|
209
|
+
console.error("Executing AppleScript via temp file...");
|
|
210
|
+
|
|
211
|
+
// Write to a temporary AppleScript file to avoid shell escaping issues
|
|
212
|
+
const tempFile = join(tmpdir(), `omnifocus_add_${Date.now()}.applescript`);
|
|
213
|
+
writeFileSync(tempFile, script, { encoding: 'utf8' });
|
|
214
|
+
|
|
215
|
+
// Execute AppleScript from file
|
|
216
|
+
const { stdout, stderr } = await execAsync(`osascript ${tempFile}`);
|
|
217
|
+
|
|
218
|
+
if (stderr) {
|
|
219
|
+
console.error("AppleScript stderr:", stderr);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
console.error("AppleScript stdout:", stdout);
|
|
223
|
+
|
|
224
|
+
// Cleanup temp file
|
|
225
|
+
try { unlinkSync(tempFile); } catch {}
|
|
226
|
+
|
|
227
|
+
// Parse the result
|
|
228
|
+
try {
|
|
229
|
+
const result = JSON.parse(stdout);
|
|
230
|
+
|
|
231
|
+
// Return the result
|
|
232
|
+
return {
|
|
233
|
+
success: result.success,
|
|
234
|
+
taskId: result.taskId,
|
|
235
|
+
error: result.error,
|
|
236
|
+
placement: result.placement
|
|
237
|
+
};
|
|
238
|
+
} catch (parseError) {
|
|
239
|
+
console.error("Error parsing AppleScript result:", parseError);
|
|
240
|
+
return {
|
|
241
|
+
success: false,
|
|
242
|
+
error: `Failed to parse result: ${stdout}`
|
|
243
|
+
};
|
|
244
|
+
}
|
|
245
|
+
} catch (error: any) {
|
|
246
|
+
console.error("Error in addOmniFocusTask:", error);
|
|
247
|
+
return {
|
|
248
|
+
success: false,
|
|
249
|
+
error: error?.message || "Unknown error in addOmniFocusTask"
|
|
250
|
+
};
|
|
251
|
+
}
|
|
252
|
+
}
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
import { exec } from 'child_process';
|
|
2
|
+
import { promisify } from 'util';
|
|
3
|
+
import { createDateOutsideTellBlock } from '../../utils/dateFormatting.js';
|
|
4
|
+
const execAsync = promisify(exec);
|
|
5
|
+
|
|
6
|
+
// Interface for project creation parameters
|
|
7
|
+
export interface AddProjectParams {
|
|
8
|
+
name: string;
|
|
9
|
+
note?: string;
|
|
10
|
+
dueDate?: string; // ISO date string
|
|
11
|
+
deferDate?: string; // ISO date string
|
|
12
|
+
flagged?: boolean;
|
|
13
|
+
estimatedMinutes?: number;
|
|
14
|
+
tags?: string[]; // Tag names
|
|
15
|
+
folderName?: string; // Folder name to add project to
|
|
16
|
+
sequential?: boolean; // Whether tasks should be sequential or parallel
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Generate pure AppleScript for project creation
|
|
21
|
+
*/
|
|
22
|
+
function generateAppleScript(params: AddProjectParams): string {
|
|
23
|
+
// Sanitize and prepare parameters for AppleScript
|
|
24
|
+
const name = params.name.replace(/['"\\]/g, '\\$&'); // Escape quotes and backslashes
|
|
25
|
+
const note = params.note?.replace(/['"\\]/g, '\\$&') || '';
|
|
26
|
+
const dueDate = params.dueDate || '';
|
|
27
|
+
const deferDate = params.deferDate || '';
|
|
28
|
+
const flagged = params.flagged === true;
|
|
29
|
+
const estimatedMinutes = params.estimatedMinutes?.toString() || '';
|
|
30
|
+
const tags = params.tags || [];
|
|
31
|
+
const folderName = params.folderName?.replace(/['"\\]/g, '\\$&') || '';
|
|
32
|
+
const sequential = params.sequential === true;
|
|
33
|
+
|
|
34
|
+
// Generate date constructions outside tell blocks
|
|
35
|
+
let datePreScript = '';
|
|
36
|
+
let dueDateVar = '';
|
|
37
|
+
let deferDateVar = '';
|
|
38
|
+
|
|
39
|
+
if (dueDate) {
|
|
40
|
+
dueDateVar = `dueDate${Math.random().toString(36).substr(2, 9)}`;
|
|
41
|
+
datePreScript += createDateOutsideTellBlock(dueDate, dueDateVar) + '\n\n';
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
if (deferDate) {
|
|
45
|
+
deferDateVar = `deferDate${Math.random().toString(36).substr(2, 9)}`;
|
|
46
|
+
datePreScript += createDateOutsideTellBlock(deferDate, deferDateVar) + '\n\n';
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Construct AppleScript with error handling
|
|
50
|
+
let script = datePreScript + `
|
|
51
|
+
try
|
|
52
|
+
tell application "OmniFocus"
|
|
53
|
+
tell front document
|
|
54
|
+
-- Determine the container (root or folder)
|
|
55
|
+
if "${folderName}" is "" then
|
|
56
|
+
-- Create project at the root level
|
|
57
|
+
set newProject to make new project with properties {name:"${name}"}
|
|
58
|
+
else
|
|
59
|
+
-- Use specified folder
|
|
60
|
+
try
|
|
61
|
+
set theFolder to first flattened folder where name = "${folderName}"
|
|
62
|
+
set newProject to make new project with properties {name:"${name}"} at end of projects of theFolder
|
|
63
|
+
on error
|
|
64
|
+
return "{\\\"success\\\":false,\\\"error\\\":\\\"Folder not found: ${folderName}\\\"}"
|
|
65
|
+
end try
|
|
66
|
+
end if
|
|
67
|
+
|
|
68
|
+
-- Set project properties
|
|
69
|
+
${note ? `set note of newProject to "${note}"` : ''}
|
|
70
|
+
${dueDate ? `
|
|
71
|
+
-- Set due date
|
|
72
|
+
set due date of newProject to ` + dueDateVar : ''}
|
|
73
|
+
${deferDate ? `
|
|
74
|
+
-- Set defer date
|
|
75
|
+
set defer date of newProject to ` + deferDateVar : ''}
|
|
76
|
+
${flagged ? `set flagged of newProject to true` : ''}
|
|
77
|
+
${estimatedMinutes ? `set estimated minutes of newProject to ${estimatedMinutes}` : ''}
|
|
78
|
+
${`set sequential of newProject to ${sequential}`}
|
|
79
|
+
|
|
80
|
+
-- Get the project ID
|
|
81
|
+
set projectId to id of newProject as string
|
|
82
|
+
|
|
83
|
+
-- Add tags if provided
|
|
84
|
+
${tags.length > 0 ? tags.map(tag => {
|
|
85
|
+
const sanitizedTag = tag.replace(/['"\\]/g, '\\$&');
|
|
86
|
+
return `
|
|
87
|
+
try
|
|
88
|
+
set theTag to first flattened tag where name = "${sanitizedTag}"
|
|
89
|
+
add theTag to tags of newProject
|
|
90
|
+
on error
|
|
91
|
+
-- Tag might not exist, try to create it
|
|
92
|
+
try
|
|
93
|
+
set theTag to make new tag with properties {name:"${sanitizedTag}"}
|
|
94
|
+
add theTag to tags of newProject
|
|
95
|
+
on error
|
|
96
|
+
-- Could not create or add tag
|
|
97
|
+
end try
|
|
98
|
+
end try`;
|
|
99
|
+
}).join('\n') : ''}
|
|
100
|
+
|
|
101
|
+
-- Return success with project ID
|
|
102
|
+
return "{\\\"success\\\":true,\\\"projectId\\\":\\"" & projectId & "\\",\\\"name\\\":\\"${name}\\"}"
|
|
103
|
+
end tell
|
|
104
|
+
end tell
|
|
105
|
+
on error errorMessage
|
|
106
|
+
return "{\\\"success\\\":false,\\\"error\\\":\\"" & errorMessage & "\\"}"
|
|
107
|
+
end try
|
|
108
|
+
`;
|
|
109
|
+
|
|
110
|
+
return script;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Add a project to OmniFocus
|
|
115
|
+
*/
|
|
116
|
+
export async function addProject(params: AddProjectParams): Promise<{success: boolean, projectId?: string, error?: string}> {
|
|
117
|
+
try {
|
|
118
|
+
// Generate AppleScript
|
|
119
|
+
const script = generateAppleScript(params);
|
|
120
|
+
|
|
121
|
+
console.error("Executing AppleScript directly...");
|
|
122
|
+
|
|
123
|
+
// Execute AppleScript directly
|
|
124
|
+
const { stdout, stderr } = await execAsync(`osascript -e '${script}'`);
|
|
125
|
+
|
|
126
|
+
if (stderr) {
|
|
127
|
+
console.error("AppleScript stderr:", stderr);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
console.error("AppleScript stdout:", stdout);
|
|
131
|
+
|
|
132
|
+
// Parse the result
|
|
133
|
+
try {
|
|
134
|
+
const result = JSON.parse(stdout);
|
|
135
|
+
|
|
136
|
+
// Return the result
|
|
137
|
+
return {
|
|
138
|
+
success: result.success,
|
|
139
|
+
projectId: result.projectId,
|
|
140
|
+
error: result.error
|
|
141
|
+
};
|
|
142
|
+
} catch (parseError) {
|
|
143
|
+
console.error("Error parsing AppleScript result:", parseError);
|
|
144
|
+
return {
|
|
145
|
+
success: false,
|
|
146
|
+
error: `Failed to parse result: ${stdout}`
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
} catch (error: any) {
|
|
150
|
+
console.error("Error in addProject:", error);
|
|
151
|
+
return {
|
|
152
|
+
success: false,
|
|
153
|
+
error: error?.message || "Unknown error in addProject"
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
}
|