@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,507 @@
|
|
|
1
|
+
import { exec } from 'child_process';
|
|
2
|
+
import { promisify } from 'util';
|
|
3
|
+
import { writeFileSync, unlinkSync } from 'fs';
|
|
4
|
+
import { join } from 'path';
|
|
5
|
+
import { tmpdir } from 'os';
|
|
6
|
+
import { generateDateAssignmentV2 } from '../../utils/dateFormatting.js';
|
|
7
|
+
const execAsync = promisify(exec);
|
|
8
|
+
|
|
9
|
+
// Status options for tasks and projects
|
|
10
|
+
type TaskStatus = 'incomplete' | 'completed' | 'dropped';
|
|
11
|
+
type ProjectStatus = 'active' | 'completed' | 'dropped' | 'onHold';
|
|
12
|
+
|
|
13
|
+
// Interface for item edit parameters
|
|
14
|
+
export interface EditItemParams {
|
|
15
|
+
id?: string; // ID of the task or project to edit
|
|
16
|
+
name?: string; // Name of the task or project to edit (as fallback if ID not provided)
|
|
17
|
+
itemType: 'task' | 'project'; // Type of item to edit
|
|
18
|
+
|
|
19
|
+
// Common editable fields
|
|
20
|
+
newName?: string; // New name for the item
|
|
21
|
+
newNote?: string; // New note for the item
|
|
22
|
+
newDueDate?: string; // New due date in ISO format (empty string to clear)
|
|
23
|
+
newDeferDate?: string; // New defer date in ISO format (empty string to clear)
|
|
24
|
+
newFlagged?: boolean; // New flagged status (false to remove flag, true to add flag)
|
|
25
|
+
newEstimatedMinutes?: number; // New estimated minutes
|
|
26
|
+
|
|
27
|
+
// Task-specific fields
|
|
28
|
+
newStatus?: TaskStatus; // New status for tasks (incomplete, completed, dropped)
|
|
29
|
+
addTags?: string[]; // Tags to add to the task
|
|
30
|
+
removeTags?: string[]; // Tags to remove from the task
|
|
31
|
+
replaceTags?: string[]; // Tags to replace all existing tags with
|
|
32
|
+
|
|
33
|
+
// Project-specific fields
|
|
34
|
+
newSequential?: boolean; // Whether the project should be sequential
|
|
35
|
+
newFolderName?: string; // New folder to move the project to
|
|
36
|
+
newProjectStatus?: ProjectStatus; // New status for projects
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Generate pure AppleScript for item editing with dates constructed outside tell blocks
|
|
41
|
+
*/
|
|
42
|
+
function generateAppleScript(params: EditItemParams): string {
|
|
43
|
+
// Sanitize and prepare parameters for AppleScript
|
|
44
|
+
const id = params.id?.replace(/['"\\]/g, '\\$&') || ''; // Escape quotes and backslashes
|
|
45
|
+
const name = params.name?.replace(/['"\\]/g, '\\$&') || '';
|
|
46
|
+
const itemType = params.itemType;
|
|
47
|
+
|
|
48
|
+
// Verify we have at least one identifier
|
|
49
|
+
if (!id && !name) {
|
|
50
|
+
return `return "{\\\"success\\\":false,\\\"error\\\":\\\"Either id or name must be provided\\\"}"`;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Collect all date constructions that need to happen outside tell blocks
|
|
54
|
+
const datePreScripts: string[] = [];
|
|
55
|
+
const dateAssignments: { [key: string]: string } = {};
|
|
56
|
+
|
|
57
|
+
// Process due date if provided
|
|
58
|
+
const dueDateParts = generateDateAssignmentV2('foundItem', 'due date', params.newDueDate);
|
|
59
|
+
if (dueDateParts) {
|
|
60
|
+
if (dueDateParts.preScript) {
|
|
61
|
+
datePreScripts.push(dueDateParts.preScript);
|
|
62
|
+
}
|
|
63
|
+
dateAssignments['due date'] = dueDateParts.assignmentScript;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Process defer date if provided
|
|
67
|
+
const deferDateParts = generateDateAssignmentV2('foundItem', 'defer date', params.newDeferDate);
|
|
68
|
+
if (deferDateParts) {
|
|
69
|
+
if (deferDateParts.preScript) {
|
|
70
|
+
datePreScripts.push(deferDateParts.preScript);
|
|
71
|
+
}
|
|
72
|
+
dateAssignments['defer date'] = deferDateParts.assignmentScript;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Build the complete script
|
|
76
|
+
let script = '';
|
|
77
|
+
|
|
78
|
+
// Add date constructions outside tell blocks
|
|
79
|
+
if (datePreScripts.length > 0) {
|
|
80
|
+
script += datePreScripts.join('\n') + '\n\n';
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Start the main script
|
|
84
|
+
script += `try
|
|
85
|
+
tell application "OmniFocus"
|
|
86
|
+
tell front document
|
|
87
|
+
-- Find the item to edit
|
|
88
|
+
set foundItem to missing value
|
|
89
|
+
`;
|
|
90
|
+
|
|
91
|
+
// Add ID search if provided
|
|
92
|
+
if (id) {
|
|
93
|
+
if (itemType === 'task') {
|
|
94
|
+
script += `
|
|
95
|
+
-- Try to find task by ID
|
|
96
|
+
repeat with aTask in (flattened tasks)
|
|
97
|
+
if (id of aTask as string) = "${id}" then
|
|
98
|
+
set foundItem to aTask
|
|
99
|
+
exit repeat
|
|
100
|
+
end if
|
|
101
|
+
end repeat
|
|
102
|
+
|
|
103
|
+
-- If not found in projects, search in inbox
|
|
104
|
+
if foundItem is missing value then
|
|
105
|
+
repeat with aTask in (inbox tasks)
|
|
106
|
+
if (id of aTask as string) = "${id}" then
|
|
107
|
+
set foundItem to aTask
|
|
108
|
+
exit repeat
|
|
109
|
+
end if
|
|
110
|
+
end repeat
|
|
111
|
+
end if
|
|
112
|
+
`;
|
|
113
|
+
} else {
|
|
114
|
+
script += `
|
|
115
|
+
-- Try to find project by ID
|
|
116
|
+
repeat with aProject in (flattened projects)
|
|
117
|
+
if (id of aProject as string) = "${id}" then
|
|
118
|
+
set foundItem to aProject
|
|
119
|
+
exit repeat
|
|
120
|
+
end if
|
|
121
|
+
end repeat
|
|
122
|
+
`;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// Add name search if provided (and no ID or as fallback)
|
|
127
|
+
if (!id && name) {
|
|
128
|
+
if (itemType === 'task') {
|
|
129
|
+
script += `
|
|
130
|
+
-- Find task by name (search in projects first, then inbox)
|
|
131
|
+
repeat with aTask in (flattened tasks)
|
|
132
|
+
if (name of aTask) = "${name}" then
|
|
133
|
+
set foundItem to aTask
|
|
134
|
+
exit repeat
|
|
135
|
+
end if
|
|
136
|
+
end repeat
|
|
137
|
+
|
|
138
|
+
-- If not found in projects, search in inbox
|
|
139
|
+
if foundItem is missing value then
|
|
140
|
+
repeat with aTask in (inbox tasks)
|
|
141
|
+
if (name of aTask) = "${name}" then
|
|
142
|
+
set foundItem to aTask
|
|
143
|
+
exit repeat
|
|
144
|
+
end if
|
|
145
|
+
end repeat
|
|
146
|
+
end if
|
|
147
|
+
`;
|
|
148
|
+
} else {
|
|
149
|
+
script += `
|
|
150
|
+
-- Find project by name
|
|
151
|
+
repeat with aProject in (flattened projects)
|
|
152
|
+
if (name of aProject) = "${name}" then
|
|
153
|
+
set foundItem to aProject
|
|
154
|
+
exit repeat
|
|
155
|
+
end if
|
|
156
|
+
end repeat
|
|
157
|
+
`;
|
|
158
|
+
}
|
|
159
|
+
} else if (id && name) {
|
|
160
|
+
if (itemType === 'task') {
|
|
161
|
+
script += `
|
|
162
|
+
-- If ID search failed, try to find by name as fallback
|
|
163
|
+
if foundItem is missing value then
|
|
164
|
+
repeat with aTask in (flattened tasks)
|
|
165
|
+
if (name of aTask) = "${name}" then
|
|
166
|
+
set foundItem to aTask
|
|
167
|
+
exit repeat
|
|
168
|
+
end if
|
|
169
|
+
end repeat
|
|
170
|
+
end if
|
|
171
|
+
|
|
172
|
+
-- If still not found, search in inbox
|
|
173
|
+
if foundItem is missing value then
|
|
174
|
+
repeat with aTask in (inbox tasks)
|
|
175
|
+
if (name of aTask) = "${name}" then
|
|
176
|
+
set foundItem to aTask
|
|
177
|
+
exit repeat
|
|
178
|
+
end if
|
|
179
|
+
end repeat
|
|
180
|
+
end if
|
|
181
|
+
`;
|
|
182
|
+
} else {
|
|
183
|
+
script += `
|
|
184
|
+
-- If ID search failed, try to find project by name as fallback
|
|
185
|
+
if foundItem is missing value then
|
|
186
|
+
repeat with aProject in (flattened projects)
|
|
187
|
+
if (name of aProject) = "${name}" then
|
|
188
|
+
set foundItem to aProject
|
|
189
|
+
exit repeat
|
|
190
|
+
end if
|
|
191
|
+
end repeat
|
|
192
|
+
end if
|
|
193
|
+
`;
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// Add the item editing logic
|
|
198
|
+
script += `
|
|
199
|
+
-- If we found the item, edit it
|
|
200
|
+
if foundItem is not missing value then
|
|
201
|
+
set itemName to name of foundItem
|
|
202
|
+
set itemId to id of foundItem as string
|
|
203
|
+
set changedProperties to {}
|
|
204
|
+
`;
|
|
205
|
+
|
|
206
|
+
// Common property updates for both tasks and projects
|
|
207
|
+
if (params.newName !== undefined) {
|
|
208
|
+
script += `
|
|
209
|
+
-- Update name
|
|
210
|
+
set name of foundItem to "${params.newName.replace(/['"\\]/g, '\\$&')}"
|
|
211
|
+
set end of changedProperties to "name"
|
|
212
|
+
`;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
if (params.newNote !== undefined) {
|
|
216
|
+
script += `
|
|
217
|
+
-- Update note
|
|
218
|
+
set note of foundItem to "${params.newNote.replace(/['"\\]/g, '\\$&')}"
|
|
219
|
+
set end of changedProperties to "note"
|
|
220
|
+
`;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// Add date assignments (using pre-constructed dates)
|
|
224
|
+
if (dateAssignments['due date']) {
|
|
225
|
+
script += `
|
|
226
|
+
-- Update due date
|
|
227
|
+
${dateAssignments['due date']}
|
|
228
|
+
set end of changedProperties to "due date"
|
|
229
|
+
`;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
if (dateAssignments['defer date']) {
|
|
233
|
+
script += `
|
|
234
|
+
-- Update defer date
|
|
235
|
+
${dateAssignments['defer date']}
|
|
236
|
+
set end of changedProperties to "defer date"
|
|
237
|
+
`;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
if (params.newFlagged !== undefined) {
|
|
241
|
+
script += `
|
|
242
|
+
-- Update flagged status
|
|
243
|
+
set flagged of foundItem to ${params.newFlagged}
|
|
244
|
+
set end of changedProperties to "flagged"
|
|
245
|
+
`;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
if (params.newEstimatedMinutes !== undefined) {
|
|
249
|
+
script += `
|
|
250
|
+
-- Update estimated minutes
|
|
251
|
+
set estimated minutes of foundItem to ${params.newEstimatedMinutes}
|
|
252
|
+
set end of changedProperties to "estimated minutes"
|
|
253
|
+
`;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
// Task-specific updates
|
|
257
|
+
if (itemType === 'task') {
|
|
258
|
+
// Update task status
|
|
259
|
+
if (params.newStatus !== undefined) {
|
|
260
|
+
if (params.newStatus === 'completed') {
|
|
261
|
+
script += `
|
|
262
|
+
-- Mark task as completed
|
|
263
|
+
set completed of foundItem to true
|
|
264
|
+
set end of changedProperties to "status (completed)"
|
|
265
|
+
`;
|
|
266
|
+
} else if (params.newStatus === 'dropped') {
|
|
267
|
+
script += `
|
|
268
|
+
-- Mark task as dropped
|
|
269
|
+
set dropped of foundItem to true
|
|
270
|
+
set end of changedProperties to "status (dropped)"
|
|
271
|
+
`;
|
|
272
|
+
} else if (params.newStatus === 'incomplete') {
|
|
273
|
+
script += `
|
|
274
|
+
-- Mark task as incomplete
|
|
275
|
+
set completed of foundItem to false
|
|
276
|
+
set dropped of foundItem to false
|
|
277
|
+
set end of changedProperties to "status (incomplete)"
|
|
278
|
+
`;
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
// Handle tag operations
|
|
283
|
+
if (params.replaceTags && params.replaceTags.length > 0) {
|
|
284
|
+
const tagsList = params.replaceTags.map(tag => `"${tag.replace(/['"\\]/g, '\\$&')}"`).join(", ");
|
|
285
|
+
script += `
|
|
286
|
+
-- Replace all tags
|
|
287
|
+
set tagNames to {${tagsList}}
|
|
288
|
+
set existingTags to tags of foundItem
|
|
289
|
+
|
|
290
|
+
-- First clear all existing tags
|
|
291
|
+
repeat with existingTag in existingTags
|
|
292
|
+
remove existingTag from tags of foundItem
|
|
293
|
+
end repeat
|
|
294
|
+
|
|
295
|
+
-- Then add new tags
|
|
296
|
+
repeat with tagName in tagNames
|
|
297
|
+
set tagObj to missing value
|
|
298
|
+
try
|
|
299
|
+
set tagObj to first flattened tag where name = (tagName as string)
|
|
300
|
+
on error
|
|
301
|
+
-- Tag doesn't exist, create it
|
|
302
|
+
set tagObj to make new tag with properties {name:(tagName as string)}
|
|
303
|
+
end try
|
|
304
|
+
if tagObj is not missing value then
|
|
305
|
+
add tagObj to tags of foundItem
|
|
306
|
+
end if
|
|
307
|
+
end repeat
|
|
308
|
+
set end of changedProperties to "tags (replaced)"
|
|
309
|
+
`;
|
|
310
|
+
} else {
|
|
311
|
+
// Add tags if specified
|
|
312
|
+
if (params.addTags && params.addTags.length > 0) {
|
|
313
|
+
const tagsList = params.addTags.map(tag => `"${tag.replace(/['"\\]/g, '\\$&')}"`).join(", ");
|
|
314
|
+
script += `
|
|
315
|
+
-- Add tags
|
|
316
|
+
set tagNames to {${tagsList}}
|
|
317
|
+
repeat with tagName in tagNames
|
|
318
|
+
set tagObj to missing value
|
|
319
|
+
try
|
|
320
|
+
set tagObj to first flattened tag where name = (tagName as string)
|
|
321
|
+
on error
|
|
322
|
+
-- Tag doesn't exist, create it
|
|
323
|
+
set tagObj to make new tag with properties {name:(tagName as string)}
|
|
324
|
+
end try
|
|
325
|
+
if tagObj is not missing value then
|
|
326
|
+
add tagObj to tags of foundItem
|
|
327
|
+
end if
|
|
328
|
+
end repeat
|
|
329
|
+
set end of changedProperties to "tags (added)"
|
|
330
|
+
`;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
// Remove tags if specified
|
|
334
|
+
if (params.removeTags && params.removeTags.length > 0) {
|
|
335
|
+
const tagsList = params.removeTags.map(tag => `"${tag.replace(/['"\\]/g, '\\$&')}"`).join(", ");
|
|
336
|
+
script += `
|
|
337
|
+
-- Remove tags
|
|
338
|
+
set tagNames to {${tagsList}}
|
|
339
|
+
repeat with tagName in tagNames
|
|
340
|
+
try
|
|
341
|
+
set tagObj to first flattened tag where name = (tagName as string)
|
|
342
|
+
remove tagObj from tags of foundItem
|
|
343
|
+
end try
|
|
344
|
+
end repeat
|
|
345
|
+
set end of changedProperties to "tags (removed)"
|
|
346
|
+
`;
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
// Project-specific updates
|
|
352
|
+
if (itemType === 'project') {
|
|
353
|
+
// Update sequential status
|
|
354
|
+
if (params.newSequential !== undefined) {
|
|
355
|
+
script += `
|
|
356
|
+
-- Update sequential status
|
|
357
|
+
set sequential of foundItem to ${params.newSequential}
|
|
358
|
+
set end of changedProperties to "sequential"
|
|
359
|
+
`;
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
// Update project status
|
|
363
|
+
if (params.newProjectStatus !== undefined) {
|
|
364
|
+
const statusValue = params.newProjectStatus === 'active' ? 'active status' :
|
|
365
|
+
params.newProjectStatus === 'completed' ? 'done status' :
|
|
366
|
+
params.newProjectStatus === 'dropped' ? 'dropped status' :
|
|
367
|
+
'on hold status';
|
|
368
|
+
script += `
|
|
369
|
+
-- Update project status
|
|
370
|
+
set status of foundItem to ${statusValue}
|
|
371
|
+
set end of changedProperties to "status"
|
|
372
|
+
`;
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
// Move to a new folder
|
|
376
|
+
if (params.newFolderName !== undefined) {
|
|
377
|
+
const folderName = params.newFolderName.replace(/['"\\]/g, '\\$&');
|
|
378
|
+
script += `
|
|
379
|
+
-- Move to new folder
|
|
380
|
+
set destFolder to missing value
|
|
381
|
+
try
|
|
382
|
+
set destFolder to first flattened folder where name = "${folderName}"
|
|
383
|
+
end try
|
|
384
|
+
|
|
385
|
+
if destFolder is missing value then
|
|
386
|
+
-- Create the folder if it doesn't exist
|
|
387
|
+
set destFolder to make new folder with properties {name:"${folderName}"}
|
|
388
|
+
end if
|
|
389
|
+
|
|
390
|
+
-- Move project to the folder
|
|
391
|
+
move foundItem to destFolder
|
|
392
|
+
set end of changedProperties to "folder"
|
|
393
|
+
`;
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
script += `
|
|
398
|
+
-- Prepare the changed properties as a string
|
|
399
|
+
set changedPropsText to ""
|
|
400
|
+
repeat with i from 1 to count of changedProperties
|
|
401
|
+
set changedPropsText to changedPropsText & item i of changedProperties
|
|
402
|
+
if i < count of changedProperties then
|
|
403
|
+
set changedPropsText to changedPropsText & ", "
|
|
404
|
+
end if
|
|
405
|
+
end repeat
|
|
406
|
+
|
|
407
|
+
-- Return success with details
|
|
408
|
+
return "{\\\"success\\\":true,\\\"id\\\":\\"" & itemId & "\\",\\\"name\\\":\\"" & itemName & "\\",\\\"changedProperties\\\":\\"" & changedPropsText & "\\"}"
|
|
409
|
+
else
|
|
410
|
+
-- Item not found
|
|
411
|
+
return "{\\\"success\\\":false,\\\"error\\\":\\\"Item not found\\"}"
|
|
412
|
+
end if
|
|
413
|
+
end tell
|
|
414
|
+
end tell
|
|
415
|
+
on error errorMessage
|
|
416
|
+
return "{\\\"success\\\":false,\\\"error\\\":\\"" & errorMessage & "\\"}"
|
|
417
|
+
end try
|
|
418
|
+
`;
|
|
419
|
+
|
|
420
|
+
return script;
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
/**
|
|
424
|
+
* Edit a task or project in OmniFocus
|
|
425
|
+
*/
|
|
426
|
+
export async function editItem(params: EditItemParams): Promise<{
|
|
427
|
+
success: boolean,
|
|
428
|
+
id?: string,
|
|
429
|
+
name?: string,
|
|
430
|
+
changedProperties?: string,
|
|
431
|
+
error?: string
|
|
432
|
+
}> {
|
|
433
|
+
let tempFile: string | undefined;
|
|
434
|
+
|
|
435
|
+
try {
|
|
436
|
+
// Generate AppleScript
|
|
437
|
+
const script = generateAppleScript(params);
|
|
438
|
+
|
|
439
|
+
console.error("Executing AppleScript for editing (V2)...");
|
|
440
|
+
console.error(`Item type: ${params.itemType}, ID: ${params.id || 'not provided'}, Name: ${params.name || 'not provided'}`);
|
|
441
|
+
|
|
442
|
+
// Log a preview of the script for debugging (first few lines)
|
|
443
|
+
const scriptPreview = script.split('\n').slice(0, 10).join('\n') + '\n...';
|
|
444
|
+
console.error("AppleScript preview:\n", scriptPreview);
|
|
445
|
+
|
|
446
|
+
// Write script to temporary file to avoid shell escaping issues
|
|
447
|
+
tempFile = join(tmpdir(), `edit_omnifocus_${Date.now()}.applescript`);
|
|
448
|
+
writeFileSync(tempFile, script);
|
|
449
|
+
|
|
450
|
+
// Execute AppleScript from file
|
|
451
|
+
const { stdout, stderr } = await execAsync(`osascript ${tempFile}`);
|
|
452
|
+
|
|
453
|
+
// Clean up temp file
|
|
454
|
+
try {
|
|
455
|
+
unlinkSync(tempFile);
|
|
456
|
+
} catch (cleanupError) {
|
|
457
|
+
console.error("Failed to clean up temp file:", cleanupError);
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
if (stderr) {
|
|
461
|
+
console.error("AppleScript stderr:", stderr);
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
console.error("AppleScript stdout:", stdout);
|
|
465
|
+
|
|
466
|
+
// Parse the result
|
|
467
|
+
try {
|
|
468
|
+
const result = JSON.parse(stdout);
|
|
469
|
+
|
|
470
|
+
// Return the result
|
|
471
|
+
return {
|
|
472
|
+
success: result.success,
|
|
473
|
+
id: result.id,
|
|
474
|
+
name: result.name,
|
|
475
|
+
changedProperties: result.changedProperties,
|
|
476
|
+
error: result.error
|
|
477
|
+
};
|
|
478
|
+
} catch (parseError) {
|
|
479
|
+
console.error("Error parsing AppleScript result:", parseError);
|
|
480
|
+
return {
|
|
481
|
+
success: false,
|
|
482
|
+
error: `Failed to parse result: ${stdout}`
|
|
483
|
+
};
|
|
484
|
+
}
|
|
485
|
+
} catch (error: any) {
|
|
486
|
+
// Clean up temp file if it exists
|
|
487
|
+
if (tempFile) {
|
|
488
|
+
try {
|
|
489
|
+
unlinkSync(tempFile);
|
|
490
|
+
} catch (cleanupError) {
|
|
491
|
+
// Ignore cleanup errors
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
console.error("Error in editItem execution:", error);
|
|
496
|
+
|
|
497
|
+
// Include more detailed error information
|
|
498
|
+
if (error.message && error.message.includes('syntax error')) {
|
|
499
|
+
console.error("This appears to be an AppleScript syntax error. Review the script generation logic.");
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
return {
|
|
503
|
+
success: false,
|
|
504
|
+
error: error?.message || "Unknown error in editItem"
|
|
505
|
+
};
|
|
506
|
+
}
|
|
507
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { executeOmniFocusScript } from '../../utils/scriptExecution.js';
|
|
2
|
+
|
|
3
|
+
export interface GetPerspectiveViewParams {
|
|
4
|
+
perspectiveName: string;
|
|
5
|
+
limit?: number;
|
|
6
|
+
includeMetadata?: boolean;
|
|
7
|
+
fields?: string[];
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
interface PerspectiveViewResult {
|
|
11
|
+
success: boolean;
|
|
12
|
+
items?: any[];
|
|
13
|
+
error?: string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export async function getPerspectiveView(params: GetPerspectiveViewParams): Promise<PerspectiveViewResult> {
|
|
17
|
+
const { perspectiveName, limit = 100, includeMetadata = true, fields } = params;
|
|
18
|
+
|
|
19
|
+
try {
|
|
20
|
+
// Execute the OmniJS script to get perspective view
|
|
21
|
+
// Note: This gets the current perspective view, not a specific one
|
|
22
|
+
// OmniJS doesn't easily allow switching perspectives
|
|
23
|
+
const result = await executeOmniFocusScript('@getPerspectiveView.js');
|
|
24
|
+
|
|
25
|
+
if (result.error) {
|
|
26
|
+
return {
|
|
27
|
+
success: false,
|
|
28
|
+
error: result.error
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// Check if the current perspective matches what was requested
|
|
33
|
+
const currentPerspective = result.perspectiveName;
|
|
34
|
+
if (currentPerspective && currentPerspective.toLowerCase() !== perspectiveName.toLowerCase()) {
|
|
35
|
+
console.warn(`Note: Current perspective is "${currentPerspective}", not "${perspectiveName}". OmniJS cannot easily switch perspectives.`);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Filter and limit items
|
|
39
|
+
let items = result.items || [];
|
|
40
|
+
|
|
41
|
+
// Apply field filtering if specified
|
|
42
|
+
if (fields && fields.length > 0) {
|
|
43
|
+
items = items.map((item: any) => {
|
|
44
|
+
const filtered: any = {};
|
|
45
|
+
fields.forEach(field => {
|
|
46
|
+
if (item.hasOwnProperty(field)) {
|
|
47
|
+
filtered[field] = item[field];
|
|
48
|
+
}
|
|
49
|
+
});
|
|
50
|
+
return filtered;
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Apply limit
|
|
55
|
+
if (limit && items.length > limit) {
|
|
56
|
+
items = items.slice(0, limit);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return {
|
|
60
|
+
success: true,
|
|
61
|
+
items: items
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
} catch (error) {
|
|
65
|
+
console.error('Error getting perspective view:', error);
|
|
66
|
+
return {
|
|
67
|
+
success: false,
|
|
68
|
+
error: error instanceof Error ? error.message : 'Unknown error occurred'
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { executeOmniFocusScript } from '../../utils/scriptExecution.js';
|
|
2
|
+
import { OmnifocusPerspective } from '../../types.js';
|
|
3
|
+
|
|
4
|
+
export interface ListPerspectivesParams {
|
|
5
|
+
includeBuiltIn?: boolean;
|
|
6
|
+
includeCustom?: boolean;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
interface ListPerspectivesResult {
|
|
10
|
+
success: boolean;
|
|
11
|
+
perspectives?: OmnifocusPerspective[];
|
|
12
|
+
error?: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export async function listPerspectives(params: ListPerspectivesParams = {}): Promise<ListPerspectivesResult> {
|
|
16
|
+
const { includeBuiltIn = true, includeCustom = true } = params;
|
|
17
|
+
|
|
18
|
+
try {
|
|
19
|
+
// Execute the OmniJS script to list perspectives
|
|
20
|
+
// This uses the built-in OmniFocus JavaScript API
|
|
21
|
+
const result = await executeOmniFocusScript('@listPerspectives.js');
|
|
22
|
+
|
|
23
|
+
if (result.error) {
|
|
24
|
+
return {
|
|
25
|
+
success: false,
|
|
26
|
+
error: result.error
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// Filter perspectives based on parameters
|
|
31
|
+
let perspectives = result.perspectives || [];
|
|
32
|
+
|
|
33
|
+
if (!includeBuiltIn) {
|
|
34
|
+
perspectives = perspectives.filter((p: any) => p.type !== 'builtin');
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
if (!includeCustom) {
|
|
38
|
+
perspectives = perspectives.filter((p: any) => p.type !== 'custom');
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
return {
|
|
42
|
+
success: true,
|
|
43
|
+
perspectives: perspectives
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
} catch (error) {
|
|
47
|
+
console.error('Error listing perspectives:', error);
|
|
48
|
+
return {
|
|
49
|
+
success: false,
|
|
50
|
+
error: error instanceof Error ? error.message : 'Unknown error occurred'
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
}
|