@stephendolan/omnifocus-cli 1.2.3 → 1.3.0

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.
Files changed (49) hide show
  1. package/dist/index.js +1302 -20
  2. package/dist/index.js.map +1 -1
  3. package/package.json +13 -9
  4. package/dist/commands/inbox.d.ts +0 -3
  5. package/dist/commands/inbox.d.ts.map +0 -1
  6. package/dist/commands/inbox.js +0 -22
  7. package/dist/commands/inbox.js.map +0 -1
  8. package/dist/commands/perspective.d.ts +0 -3
  9. package/dist/commands/perspective.d.ts.map +0 -1
  10. package/dist/commands/perspective.js +0 -22
  11. package/dist/commands/perspective.js.map +0 -1
  12. package/dist/commands/project.d.ts +0 -3
  13. package/dist/commands/project.d.ts.map +0 -1
  14. package/dist/commands/project.js +0 -87
  15. package/dist/commands/project.js.map +0 -1
  16. package/dist/commands/search.d.ts +0 -3
  17. package/dist/commands/search.d.ts.map +0 -1
  18. package/dist/commands/search.js +0 -13
  19. package/dist/commands/search.js.map +0 -1
  20. package/dist/commands/tag.d.ts +0 -3
  21. package/dist/commands/tag.d.ts.map +0 -1
  22. package/dist/commands/tag.js +0 -68
  23. package/dist/commands/tag.js.map +0 -1
  24. package/dist/commands/task.d.ts +0 -3
  25. package/dist/commands/task.d.ts.map +0 -1
  26. package/dist/commands/task.js +0 -101
  27. package/dist/commands/task.js.map +0 -1
  28. package/dist/index.d.ts +0 -3
  29. package/dist/index.d.ts.map +0 -1
  30. package/dist/lib/command-utils.d.ts +0 -4
  31. package/dist/lib/command-utils.d.ts.map +0 -1
  32. package/dist/lib/command-utils.js +0 -22
  33. package/dist/lib/command-utils.js.map +0 -1
  34. package/dist/lib/display.d.ts +0 -18
  35. package/dist/lib/display.d.ts.map +0 -1
  36. package/dist/lib/display.js +0 -294
  37. package/dist/lib/display.js.map +0 -1
  38. package/dist/lib/omnifocus.d.ts +0 -38
  39. package/dist/lib/omnifocus.d.ts.map +0 -1
  40. package/dist/lib/omnifocus.js +0 -860
  41. package/dist/lib/omnifocus.js.map +0 -1
  42. package/dist/lib/output.d.ts +0 -6
  43. package/dist/lib/output.d.ts.map +0 -1
  44. package/dist/lib/output.js +0 -12
  45. package/dist/lib/output.js.map +0 -1
  46. package/dist/types.d.ts +0 -164
  47. package/dist/types.d.ts.map +0 -1
  48. package/dist/types.js +0 -2
  49. package/dist/types.js.map +0 -1
package/dist/index.js CHANGED
@@ -1,23 +1,1304 @@
1
1
  #!/usr/bin/env node
2
- import { Command } from 'commander';
3
- import { setOutputOptions } from './lib/output.js';
4
- import { createTaskCommand } from './commands/task.js';
5
- import { createProjectCommand } from './commands/project.js';
6
- import { createInboxCommand } from './commands/inbox.js';
7
- import { createSearchCommand } from './commands/search.js';
8
- import { createPerspectiveCommand } from './commands/perspective.js';
9
- import { createTagCommand } from './commands/tag.js';
10
- const program = new Command();
11
- program
12
- .name('of')
13
- .description('A command-line interface for OmniFocus on macOS')
14
- .version('1.0.0')
15
- .option('-c, --compact', 'Minified JSON output (single line)')
16
- .hook('preAction', (thisCommand) => {
17
- const options = thisCommand.opts();
18
- setOutputOptions({
19
- compact: options.compact,
20
- });
2
+
3
+ // src/index.ts
4
+ import { Command as Command8 } from "commander";
5
+
6
+ // src/lib/output.ts
7
+ var globalOutputOptions = {};
8
+ function setOutputOptions(options) {
9
+ globalOutputOptions = options;
10
+ }
11
+ function outputJson(data, options = {}) {
12
+ const mergedOptions = { ...globalOutputOptions, ...options };
13
+ const jsonString = mergedOptions.compact ? JSON.stringify(data) : JSON.stringify(data, null, 2);
14
+ console.log(jsonString);
15
+ }
16
+
17
+ // src/commands/task.ts
18
+ import { Command } from "commander";
19
+
20
+ // src/lib/command-utils.ts
21
+ import ora from "ora";
22
+ import chalk from "chalk";
23
+
24
+ // src/lib/omnifocus.ts
25
+ import { execFile } from "child_process";
26
+ import { writeFile, unlink } from "fs/promises";
27
+ import { tmpdir } from "os";
28
+ import { join } from "path";
29
+ import { promisify } from "util";
30
+ var execFileAsync = promisify(execFile);
31
+ var OmniFocus = class {
32
+ PROJECT_STATUS_MAP = {
33
+ active: "Active",
34
+ "on hold": "OnHold",
35
+ dropped: "Dropped"
36
+ };
37
+ OMNI_HELPERS = `
38
+ function serializeTask(task) {
39
+ const containingProject = task.containingProject;
40
+ const tagNames = task.tags.map(t => t.name);
41
+
42
+ return {
43
+ id: task.id.primaryKey,
44
+ name: task.name,
45
+ note: task.note || null,
46
+ completed: task.completed,
47
+ dropped: task.dropped,
48
+ effectivelyActive: task.effectiveActive,
49
+ flagged: task.flagged,
50
+ project: containingProject ? containingProject.name : null,
51
+ tags: tagNames,
52
+ defer: task.deferDate ? task.deferDate.toISOString() : null,
53
+ due: task.dueDate ? task.dueDate.toISOString() : null,
54
+ estimatedMinutes: task.estimatedMinutes || null,
55
+ completionDate: task.completionDate ? task.completionDate.toISOString() : null,
56
+ added: task.added ? task.added.toISOString() : null,
57
+ modified: task.modified ? task.modified.toISOString() : null
58
+ };
59
+ }
60
+
61
+ function serializeProject(project) {
62
+ const parentFolder = project.parentFolder;
63
+ const allTasks = project.flattenedTasks;
64
+ const remainingTasks = allTasks.filter(t => !t.completed);
65
+ const tagNames = project.tags.map(t => t.name);
66
+
67
+ return {
68
+ id: project.id.primaryKey,
69
+ name: project.name,
70
+ note: project.note || null,
71
+ status: projectStatusToString(project.status),
72
+ folder: parentFolder ? parentFolder.name : null,
73
+ sequential: project.sequential,
74
+ taskCount: allTasks.length,
75
+ remainingCount: remainingTasks.length,
76
+ tags: tagNames
77
+ };
78
+ }
79
+
80
+ function findTask(idOrName) {
81
+ for (const task of flattenedTasks) {
82
+ if (task.id.primaryKey === idOrName || task.name === idOrName) {
83
+ return task;
84
+ }
85
+ }
86
+ throw new Error("Task not found: " + idOrName);
87
+ }
88
+
89
+ function findProject(idOrName) {
90
+ for (const project of flattenedProjects) {
91
+ if (project.id.primaryKey === idOrName || project.name === idOrName) {
92
+ return project;
93
+ }
94
+ }
95
+ throw new Error("Project not found: " + idOrName);
96
+ }
97
+
98
+ function getTagPath(tag) {
99
+ const parts = [tag.name];
100
+ let current = tag.parent;
101
+ while (current) {
102
+ parts.unshift(current.name);
103
+ current = current.parent;
104
+ }
105
+ return parts.join('/');
106
+ }
107
+
108
+ function findTag(idOrName) {
109
+ for (const tag of flattenedTags) {
110
+ if (tag.id.primaryKey === idOrName) {
111
+ return tag;
112
+ }
113
+ }
114
+
115
+ if (idOrName.includes('/')) {
116
+ for (const tag of flattenedTags) {
117
+ if (getTagPath(tag) === idOrName) {
118
+ return tag;
119
+ }
120
+ }
121
+ throw new Error("Tag not found: " + idOrName);
122
+ }
123
+
124
+ const matches = flattenedTags.filter(tag => tag.name === idOrName);
125
+
126
+ if (matches.length === 0) {
127
+ throw new Error("Tag not found: " + idOrName);
128
+ }
129
+
130
+ if (matches.length > 1) {
131
+ const paths = matches.map(getTagPath);
132
+ throw new Error("Multiple tags found with name '" + idOrName + "'. Please use full path:\\n " + paths.join('\\n ') + "\\nOr use tag ID: " + matches.map(t => t.id.primaryKey).join(', '));
133
+ }
134
+
135
+ return matches[0];
136
+ }
137
+
138
+ function findByName(collection, name, typeName) {
139
+ for (const item of collection) {
140
+ if (item.name === name) {
141
+ return item;
142
+ }
143
+ }
144
+ throw new Error(typeName + " not found: " + name);
145
+ }
146
+
147
+ function assignTags(target, tagNames) {
148
+ for (const tagName of tagNames) {
149
+ const tag = findTag(tagName);
150
+ target.addTag(tag);
151
+ }
152
+ }
153
+
154
+ function replaceTagsOn(target, tagNames) {
155
+ target.clearTags();
156
+ assignTags(target, tagNames);
157
+ }
158
+
159
+ function statusToString(status, StatusEnum) {
160
+ if (status === StatusEnum.Active) return 'active';
161
+ if (status === StatusEnum.OnHold) return 'on hold';
162
+ if (status === StatusEnum.Dropped) return 'dropped';
163
+ if (status === StatusEnum.Done) return 'done';
164
+ return 'dropped';
165
+ }
166
+
167
+ function stringToStatus(str, StatusEnum) {
168
+ if (str === 'active') return StatusEnum.Active;
169
+ if (str === 'on hold') return StatusEnum.OnHold;
170
+ return StatusEnum.Dropped;
171
+ }
172
+
173
+ const projectStatusToString = (status) => statusToString(status, Project.Status);
174
+ const tagStatusToString = (status) => statusToString(status, Tag.Status);
175
+ const folderStatusToString = (status) => {
176
+ if (status === Folder.Status.Active) return 'active';
177
+ return 'dropped';
178
+ };
179
+ const stringToProjectStatus = (str) => stringToStatus(str, Project.Status);
180
+ const stringToTagStatus = (str) => stringToStatus(str, Tag.Status);
181
+
182
+ function serializeFolder(folder, includeDropped = false) {
183
+ let childFolders = folder.folders;
184
+ if (!includeDropped) {
185
+ childFolders = childFolders.filter(c => c.effectiveActive);
186
+ }
187
+
188
+ return {
189
+ id: folder.id.primaryKey,
190
+ name: folder.name,
191
+ status: folderStatusToString(folder.status),
192
+ effectivelyActive: folder.effectiveActive,
193
+ parent: folder.parent ? folder.parent.name : null,
194
+ projectCount: folder.projects.length,
195
+ remainingProjectCount: folder.projects.filter(p => p.effectiveActive).length,
196
+ folderCount: folder.folders.length,
197
+ children: childFolders.map(child => serializeFolder(child, includeDropped))
198
+ };
199
+ }
200
+
201
+ function computeTopItems(items, keyFn, topN = 5) {
202
+ return items
203
+ .sort((a, b) => b[keyFn] - a[keyFn])
204
+ .slice(0, topN)
205
+ .map(item => ({ name: item.name, [keyFn]: item[keyFn] }));
206
+ }
207
+
208
+ function computeAverage(total, count) {
209
+ return count > 0 ? Math.round((total / count) * 10) / 10 : 0;
210
+ }
211
+
212
+ function serializeTag(tag, activeOnly = false) {
213
+ const tasks = tag.tasks;
214
+ const remainingTasks = tag.remainingTasks;
215
+ const includedTasks = activeOnly ? remainingTasks : tasks;
216
+
217
+ const dates = [];
218
+ if (tag.added) dates.push(tag.added);
219
+ if (tag.modified) dates.push(tag.modified);
220
+
221
+ for (const task of includedTasks) {
222
+ if (task.added) dates.push(task.added);
223
+ if (task.modified) dates.push(task.modified);
224
+ if (!activeOnly && task.completionDate) dates.push(task.completionDate);
225
+ if (!activeOnly && task.effectiveCompletionDate) dates.push(task.effectiveCompletionDate);
226
+ }
227
+
228
+ const lastActivity = dates.length > 0
229
+ ? dates.reduce((latest, current) => current > latest ? current : latest)
230
+ : null;
231
+
232
+ return {
233
+ id: tag.id.primaryKey,
234
+ name: tag.name,
235
+ taskCount: includedTasks.length,
236
+ remainingTaskCount: remainingTasks.length,
237
+ added: tag.added ? tag.added.toISOString() : null,
238
+ modified: tag.modified ? tag.modified.toISOString() : null,
239
+ lastActivity: lastActivity ? lastActivity.toISOString() : null,
240
+ active: tag.active,
241
+ status: tagStatusToString(tag.status),
242
+ parent: tag.parent ? tag.parent.name : null,
243
+ children: tag.children.map(c => c.name),
244
+ allowsNextAction: tag.allowsNextAction
245
+ };
246
+ }
247
+ `;
248
+ async executeJXA(script, timeoutMs = 3e4) {
249
+ const tmpFile = join(tmpdir(), `omnifocus-${Date.now()}.js`);
250
+ try {
251
+ await writeFile(tmpFile, script, "utf-8");
252
+ const { stdout } = await execFileAsync("osascript", ["-l", "JavaScript", tmpFile], {
253
+ timeout: timeoutMs,
254
+ maxBuffer: 10 * 1024 * 1024
255
+ });
256
+ return stdout.trim();
257
+ } finally {
258
+ try {
259
+ await unlink(tmpFile);
260
+ } catch {
261
+ }
262
+ }
263
+ }
264
+ escapeString(str) {
265
+ return str.replace(/\\/g, "\\\\").replace(/"/g, '\\"').replace(/\n/g, "\\n").replace(/\r/g, "\\r").replace(/\t/g, "\\t");
266
+ }
267
+ wrapOmniScript(omniScript) {
268
+ return `
269
+ const app = Application('OmniFocus');
270
+ app.includeStandardAdditions = true;
271
+ const result = app.evaluateJavascript(${JSON.stringify(omniScript.trim())});
272
+ result;
273
+ `.trim();
274
+ }
275
+ buildTaskFilters(filters) {
276
+ const conditions = [];
277
+ if (!filters.includeCompleted) {
278
+ conditions.push("if (task.completed) continue;");
279
+ }
280
+ if (!filters.includeDropped) {
281
+ conditions.push("if (!task.effectiveActive) continue;");
282
+ }
283
+ if (filters.flagged) {
284
+ conditions.push("if (!task.flagged) continue;");
285
+ conditions.push("if (task.taskStatus !== Task.Status.Available) continue;");
286
+ }
287
+ if (filters.project) {
288
+ conditions.push(`
289
+ if (!task.containingProject || task.containingProject.name !== "${this.escapeString(filters.project)}") {
290
+ continue;
291
+ }
292
+ `);
293
+ }
294
+ if (filters.tag) {
295
+ conditions.push(`
296
+ if (!task.tags.some(t => t.name === "${this.escapeString(filters.tag)}")) {
297
+ continue;
298
+ }
299
+ `);
300
+ }
301
+ return conditions.join("\n ");
302
+ }
303
+ buildProjectFilters(filters) {
304
+ const conditions = [];
305
+ if (!filters.includeDropped) {
306
+ conditions.push(
307
+ "if (project.status === Project.Status.Dropped || project.status === Project.Status.Done) continue;"
308
+ );
309
+ conditions.push(
310
+ "if (project.parentFolder && !project.parentFolder.effectiveActive) continue;"
311
+ );
312
+ }
313
+ if (filters.status) {
314
+ const statusCheck = this.PROJECT_STATUS_MAP[filters.status];
315
+ conditions.push(`if (project.status !== Project.Status.${statusCheck}) continue;`);
316
+ }
317
+ if (filters.folder) {
318
+ conditions.push(
319
+ `if (!project.parentFolder || project.parentFolder.name !== "${this.escapeString(filters.folder)}") continue;`
320
+ );
321
+ }
322
+ return conditions.join("\n ");
323
+ }
324
+ buildTaskUpdates(options) {
325
+ const updates = [];
326
+ if (options.name !== void 0) {
327
+ updates.push(`task.name = "${this.escapeString(options.name)}";`);
328
+ }
329
+ if (options.note !== void 0) {
330
+ updates.push(`task.note = "${this.escapeString(options.note)}";`);
331
+ }
332
+ if (options.flagged !== void 0) {
333
+ updates.push(`task.flagged = ${options.flagged};`);
334
+ }
335
+ if (options.completed !== void 0) {
336
+ updates.push(`task.completed = ${options.completed};`);
337
+ }
338
+ if (options.estimatedMinutes !== void 0) {
339
+ updates.push(`task.estimatedMinutes = ${options.estimatedMinutes};`);
340
+ }
341
+ if (options.defer !== void 0) {
342
+ updates.push(
343
+ options.defer ? `task.deferDate = new Date(${JSON.stringify(options.defer)});` : "task.deferDate = null;"
344
+ );
345
+ }
346
+ if (options.due !== void 0) {
347
+ updates.push(
348
+ options.due ? `task.dueDate = new Date(${JSON.stringify(options.due)});` : "task.dueDate = null;"
349
+ );
350
+ }
351
+ if (options.project !== void 0 && options.project) {
352
+ updates.push(`
353
+ const targetProject = findByName(flattenedProjects, "${this.escapeString(options.project)}", "Project");
354
+ moveTasks([task], targetProject);
355
+ `);
356
+ }
357
+ if (options.tags !== void 0) {
358
+ updates.push(`replaceTagsOn(task, ${JSON.stringify(options.tags)});`);
359
+ }
360
+ return updates.join("\n ");
361
+ }
362
+ buildTagUpdates(options) {
363
+ const updates = [];
364
+ if (options.name !== void 0) {
365
+ updates.push(`tag.name = "${this.escapeString(options.name)}";`);
366
+ }
367
+ if (options.status !== void 0) {
368
+ updates.push(`tag.status = stringToTagStatus("${options.status}");`);
369
+ }
370
+ return updates.join("\n ");
371
+ }
372
+ buildProjectUpdates(options) {
373
+ const updates = [];
374
+ if (options.name !== void 0) {
375
+ updates.push(`project.name = "${this.escapeString(options.name)}";`);
376
+ }
377
+ if (options.note !== void 0) {
378
+ updates.push(`project.note = "${this.escapeString(options.note)}";`);
379
+ }
380
+ if (options.sequential !== void 0) {
381
+ updates.push(`project.sequential = ${options.sequential};`);
382
+ }
383
+ if (options.status !== void 0) {
384
+ updates.push(`project.status = stringToProjectStatus("${options.status}");`);
385
+ }
386
+ if (options.folder !== void 0 && options.folder) {
387
+ updates.push(`
388
+ const targetFolder = findByName(flattenedFolders, "${this.escapeString(options.folder)}", "Folder");
389
+ moveProjects([project], targetFolder);
390
+ `);
391
+ }
392
+ if (options.tags !== void 0) {
393
+ updates.push(`replaceTagsOn(project, ${JSON.stringify(options.tags)});`);
394
+ }
395
+ return updates.join("\n ");
396
+ }
397
+ async listTasks(filters = {}) {
398
+ const omniScript = `
399
+ ${this.OMNI_HELPERS}
400
+ (() => {
401
+ const results = [];
402
+ for (const task of flattenedTasks) {
403
+ ${this.buildTaskFilters(filters)}
404
+ results.push(serializeTask(task));
405
+ }
406
+ return JSON.stringify(results);
407
+ })();
408
+ `;
409
+ const output = await this.executeJXA(this.wrapOmniScript(omniScript));
410
+ return JSON.parse(output);
411
+ }
412
+ async createTask(options) {
413
+ const omniScript = `
414
+ ${this.OMNI_HELPERS}
415
+ (() => {
416
+ ${options.project ? `const targetProject = findByName(flattenedProjects, "${this.escapeString(options.project)}", "Project");
417
+ const task = new Task("${this.escapeString(options.name)}", targetProject);` : `const task = new Task("${this.escapeString(options.name)}");`}
418
+
419
+ ${options.note ? `task.note = "${this.escapeString(options.note)}";` : ""}
420
+ ${options.flagged ? "task.flagged = true;" : ""}
421
+ ${options.estimatedMinutes ? `task.estimatedMinutes = ${options.estimatedMinutes};` : ""}
422
+ ${options.defer ? `task.deferDate = new Date(${JSON.stringify(options.defer)});` : ""}
423
+ ${options.due ? `task.dueDate = new Date(${JSON.stringify(options.due)});` : ""}
424
+ ${options.tags && options.tags.length > 0 ? `assignTags(task, ${JSON.stringify(options.tags)});` : ""}
425
+
426
+ return JSON.stringify(serializeTask(task));
427
+ })();
428
+ `;
429
+ const output = await this.executeJXA(this.wrapOmniScript(omniScript));
430
+ return JSON.parse(output);
431
+ }
432
+ async updateTask(idOrName, options) {
433
+ const omniScript = `
434
+ ${this.OMNI_HELPERS}
435
+ (() => {
436
+ const task = findTask("${this.escapeString(idOrName)}");
437
+ ${this.buildTaskUpdates(options)}
438
+ return JSON.stringify(serializeTask(task));
439
+ })();
440
+ `;
441
+ const output = await this.executeJXA(this.wrapOmniScript(omniScript));
442
+ return JSON.parse(output);
443
+ }
444
+ async deleteTask(idOrName) {
445
+ const omniScript = `
446
+ ${this.OMNI_HELPERS}
447
+ (() => {
448
+ deleteObject(findTask("${this.escapeString(idOrName)}"));
449
+ })();
450
+ `;
451
+ await this.executeJXA(this.wrapOmniScript(omniScript));
452
+ }
453
+ async listProjects(filters = {}) {
454
+ const filterCode = this.buildProjectFilters(filters);
455
+ const omniScript = `
456
+ ${this.OMNI_HELPERS}
457
+ (() => {
458
+ const results = [];
459
+ for (const project of flattenedProjects) {
460
+ ${filterCode}
461
+ results.push(serializeProject(project));
462
+ }
463
+ return JSON.stringify(results);
464
+ })();
465
+ `;
466
+ const output = await this.executeJXA(this.wrapOmniScript(omniScript));
467
+ return JSON.parse(output);
468
+ }
469
+ async createProject(options) {
470
+ const omniScript = `
471
+ ${this.OMNI_HELPERS}
472
+ (() => {
473
+ ${options.folder ? `const targetFolder = findByName(flattenedFolders, "${this.escapeString(options.folder)}", "Folder");
474
+ const project = new Project("${this.escapeString(options.name)}", targetFolder);` : `const project = new Project("${this.escapeString(options.name)}");`}
475
+
476
+ ${options.note ? `project.note = "${this.escapeString(options.note)}";` : ""}
477
+ ${options.sequential !== void 0 ? `project.sequential = ${options.sequential};` : ""}
478
+ ${options.status ? `project.status = stringToProjectStatus("${options.status}");` : ""}
479
+ ${options.tags && options.tags.length > 0 ? `assignTags(project, ${JSON.stringify(options.tags)});` : ""}
480
+
481
+ return JSON.stringify(serializeProject(project));
482
+ })();
483
+ `;
484
+ const output = await this.executeJXA(this.wrapOmniScript(omniScript));
485
+ return JSON.parse(output);
486
+ }
487
+ async updateProject(idOrName, options) {
488
+ const omniScript = `
489
+ ${this.OMNI_HELPERS}
490
+ (() => {
491
+ const project = findProject("${this.escapeString(idOrName)}");
492
+ ${this.buildProjectUpdates(options)}
493
+ return JSON.stringify(serializeProject(project));
494
+ })();
495
+ `;
496
+ const output = await this.executeJXA(this.wrapOmniScript(omniScript));
497
+ return JSON.parse(output);
498
+ }
499
+ async deleteProject(idOrName) {
500
+ const omniScript = `
501
+ ${this.OMNI_HELPERS}
502
+ (() => {
503
+ deleteObject(findProject("${this.escapeString(idOrName)}"));
504
+ })();
505
+ `;
506
+ await this.executeJXA(this.wrapOmniScript(omniScript));
507
+ }
508
+ async listInboxTasks() {
509
+ return this.getPerspectiveTasks("Inbox");
510
+ }
511
+ async getInboxCount() {
512
+ const tasks = await this.getPerspectiveTasks("Inbox");
513
+ return tasks.length;
514
+ }
515
+ async searchTasks(query) {
516
+ const omniScript = `
517
+ ${this.OMNI_HELPERS}
518
+ (() => {
519
+ const results = [];
520
+ const searchQuery = "${this.escapeString(query)}".toLowerCase();
521
+
522
+ for (const task of flattenedTasks) {
523
+ if (task.completed) continue;
524
+ if (!task.effectiveActive) continue;
525
+
526
+ const name = task.name.toLowerCase();
527
+ const note = (task.note || '').toLowerCase();
528
+
529
+ if (name.includes(searchQuery) || note.includes(searchQuery)) {
530
+ results.push(serializeTask(task));
531
+ }
532
+ }
533
+
534
+ return JSON.stringify(results);
535
+ })();
536
+ `;
537
+ const output = await this.executeJXA(this.wrapOmniScript(omniScript));
538
+ return JSON.parse(output);
539
+ }
540
+ async getTask(idOrName) {
541
+ const omniScript = `
542
+ ${this.OMNI_HELPERS}
543
+ (() => {
544
+ const task = findTask("${this.escapeString(idOrName)}");
545
+ return JSON.stringify(serializeTask(task));
546
+ })();
547
+ `;
548
+ const output = await this.executeJXA(this.wrapOmniScript(omniScript));
549
+ return JSON.parse(output);
550
+ }
551
+ async getProject(idOrName) {
552
+ const omniScript = `
553
+ ${this.OMNI_HELPERS}
554
+ (() => {
555
+ const project = findProject("${this.escapeString(idOrName)}");
556
+ return JSON.stringify(serializeProject(project));
557
+ })();
558
+ `;
559
+ const output = await this.executeJXA(this.wrapOmniScript(omniScript));
560
+ return JSON.parse(output);
561
+ }
562
+ async listPerspectives() {
563
+ const omniScript = `
564
+ (() => {
565
+ const results = [];
566
+
567
+ const builtInNames = ['Inbox', 'Flagged', 'Forecast', 'Projects', 'Tags', 'Nearby', 'Review'];
568
+ for (const name of builtInNames) {
569
+ results.push({ id: name, name: name });
570
+ }
571
+
572
+ const customPerspectives = Perspective.Custom.all;
573
+ for (const perspective of customPerspectives) {
574
+ results.push({ id: perspective.name, name: perspective.name });
575
+ }
576
+
577
+ return JSON.stringify(results);
578
+ })();
579
+ `;
580
+ const output = await this.executeJXA(this.wrapOmniScript(omniScript));
581
+ return JSON.parse(output);
582
+ }
583
+ async getPerspectiveTasks(perspectiveName) {
584
+ const omniScript = `
585
+ ${this.OMNI_HELPERS}
586
+ (() => {
587
+ const doc = document;
588
+ const windows = doc.windows;
589
+
590
+ if (windows.length === 0) {
591
+ throw new Error("No OmniFocus window is open. Please open an OmniFocus window and try again.");
592
+ }
593
+
594
+ const win = windows[0];
595
+ const perspectiveName = "${this.escapeString(perspectiveName)}";
596
+
597
+ const builtInPerspectives = {
598
+ 'inbox': Perspective.BuiltIn.Inbox,
599
+ 'flagged': Perspective.BuiltIn.Flagged,
600
+ 'forecast': Perspective.BuiltIn.Forecast,
601
+ 'projects': Perspective.BuiltIn.Projects,
602
+ 'tags': Perspective.BuiltIn.Tags,
603
+ 'nearby': Perspective.BuiltIn.Nearby,
604
+ 'review': Perspective.BuiltIn.Review
605
+ };
606
+
607
+ const lowerName = perspectiveName.toLowerCase();
608
+ if (builtInPerspectives[lowerName]) {
609
+ win.perspective = builtInPerspectives[lowerName];
610
+ } else {
611
+ const customPerspective = Perspective.Custom.byName(perspectiveName);
612
+ if (customPerspective) {
613
+ win.perspective = customPerspective;
614
+ } else {
615
+ throw new Error("Perspective not found: " + perspectiveName);
616
+ }
617
+ }
618
+
619
+ const content = win.content;
620
+ if (!content) {
621
+ throw new Error("No content available in window");
622
+ }
623
+
624
+ const tasks = [];
625
+ content.rootNode.apply(node => {
626
+ const obj = node.object;
627
+ if (obj instanceof Task) {
628
+ tasks.push(serializeTask(obj));
629
+ }
630
+ });
631
+
632
+ return JSON.stringify(tasks);
633
+ })();
634
+ `;
635
+ const output = await this.executeJXA(this.wrapOmniScript(omniScript), 6e4);
636
+ return JSON.parse(output);
637
+ }
638
+ async listTags(options = {}) {
639
+ const omniScript = `
640
+ ${this.OMNI_HELPERS}
641
+ (() => {
642
+ const results = [];
643
+ const now = new Date();
644
+ const activeOnly = ${!!options.activeOnly};
645
+
646
+ for (const tag of flattenedTags) {
647
+ const serialized = serializeTag(tag, activeOnly);
648
+ results.push(serialized);
649
+ }
650
+
651
+ ${options.unusedDays ? `
652
+ const cutoffDate = new Date(now.getTime() - (${options.unusedDays} * 24 * 60 * 60 * 1000));
653
+ const filtered = results.filter(tag => {
654
+ if (!tag.lastActivity) return true;
655
+ return new Date(tag.lastActivity) < cutoffDate;
656
+ });
657
+ return JSON.stringify(filtered);
658
+ ` : "return JSON.stringify(results);"}
659
+ })();
660
+ `;
661
+ const output = await this.executeJXA(this.wrapOmniScript(omniScript));
662
+ const tags = JSON.parse(output);
663
+ return this.sortTags(tags, options.sortBy);
664
+ }
665
+ sortTags(tags, sortBy = "name") {
666
+ const sortFns = {
667
+ usage: (a, b) => b.taskCount - a.taskCount,
668
+ activity: (a, b) => {
669
+ if (!a.lastActivity && !b.lastActivity) return 0;
670
+ if (!a.lastActivity) return 1;
671
+ if (!b.lastActivity) return -1;
672
+ return new Date(b.lastActivity).getTime() - new Date(a.lastActivity).getTime();
673
+ },
674
+ name: (a, b) => a.name.localeCompare(b.name)
675
+ };
676
+ return tags.sort(sortFns[sortBy] || sortFns.name);
677
+ }
678
+ async getTagStats() {
679
+ const omniScript = `
680
+ ${this.OMNI_HELPERS}
681
+ (() => {
682
+ const allTags = [];
683
+ for (const tag of flattenedTags) {
684
+ allTags.push(serializeTag(tag));
685
+ }
686
+
687
+ const activeTags = allTags.filter(t => t.active);
688
+ const tagsWithTasks = allTags.filter(t => t.taskCount > 0);
689
+ const unusedTags = allTags.filter(t => t.taskCount === 0);
690
+
691
+ const totalTasks = tagsWithTasks.reduce((sum, t) => sum + t.taskCount, 0);
692
+ const avgTasksPerTag = computeAverage(totalTasks, tagsWithTasks.length);
693
+
694
+ const mostUsedTags = computeTopItems(allTags, 'taskCount');
695
+ const leastUsedTags = computeTopItems(
696
+ tagsWithTasks.map(t => ({ ...t, taskCount: -t.taskCount })),
697
+ 'taskCount'
698
+ ).map(t => ({ name: t.name, taskCount: -t.taskCount }));
699
+
700
+ const now = new Date();
701
+ const thirtyDaysAgo = new Date(now.getTime() - (30 * 24 * 60 * 60 * 1000));
702
+ const staleTags = allTags
703
+ .filter(t => t.lastActivity && new Date(t.lastActivity) < thirtyDaysAgo)
704
+ .map(t => ({
705
+ name: t.name,
706
+ daysSinceActivity: Math.floor((now - new Date(t.lastActivity)) / (24 * 60 * 60 * 1000))
707
+ }))
708
+ .sort((a, b) => b.daysSinceActivity - a.daysSinceActivity);
709
+
710
+ return JSON.stringify({
711
+ totalTags: allTags.length,
712
+ activeTags: activeTags.length,
713
+ tagsWithTasks: tagsWithTasks.length,
714
+ unusedTags: unusedTags.length,
715
+ avgTasksPerTag,
716
+ mostUsedTags,
717
+ leastUsedTags,
718
+ staleTags
719
+ });
720
+ })();
721
+ `;
722
+ const output = await this.executeJXA(this.wrapOmniScript(omniScript));
723
+ return JSON.parse(output);
724
+ }
725
+ async createTag(options) {
726
+ const omniScript = `
727
+ ${this.OMNI_HELPERS}
728
+ (() => {
729
+ ${options.parent ? `const parentTag = findTag("${this.escapeString(options.parent)}");
730
+ const tag = new Tag("${this.escapeString(options.name)}", parentTag);` : `const tag = new Tag("${this.escapeString(options.name)}", tags.beginning);`}
731
+
732
+ ${options.status ? `tag.status = stringToTagStatus("${options.status}");` : ""}
733
+
734
+ return JSON.stringify(serializeTag(tag));
735
+ })();
736
+ `;
737
+ const output = await this.executeJXA(this.wrapOmniScript(omniScript));
738
+ return JSON.parse(output);
739
+ }
740
+ async getTag(idOrName) {
741
+ const omniScript = `
742
+ ${this.OMNI_HELPERS}
743
+ (() => {
744
+ const tag = findTag("${this.escapeString(idOrName)}");
745
+ return JSON.stringify(serializeTag(tag));
746
+ })();
747
+ `;
748
+ const output = await this.executeJXA(this.wrapOmniScript(omniScript));
749
+ return JSON.parse(output);
750
+ }
751
+ async updateTag(idOrName, options) {
752
+ const omniScript = `
753
+ ${this.OMNI_HELPERS}
754
+ (() => {
755
+ const tag = findTag("${this.escapeString(idOrName)}");
756
+ ${this.buildTagUpdates(options)}
757
+ return JSON.stringify(serializeTag(tag));
758
+ })();
759
+ `;
760
+ const output = await this.executeJXA(this.wrapOmniScript(omniScript));
761
+ return JSON.parse(output);
762
+ }
763
+ async deleteTag(idOrName) {
764
+ const omniScript = `
765
+ ${this.OMNI_HELPERS}
766
+ (() => {
767
+ deleteObject(findTag("${this.escapeString(idOrName)}"));
768
+ })();
769
+ `;
770
+ await this.executeJXA(this.wrapOmniScript(omniScript));
771
+ }
772
+ async getTaskStats() {
773
+ const omniScript = `
774
+ ${this.OMNI_HELPERS}
775
+ (() => {
776
+ const allTasks = Array.from(flattenedTasks);
777
+ const now = new Date();
778
+
779
+ const activeTasks = allTasks.filter(t => !t.completed && t.effectiveActive);
780
+ const completedTasks = allTasks.filter(t => t.completed);
781
+ const flaggedTasks = activeTasks.filter(t => t.flagged);
782
+ const overdueActiveTasks = activeTasks.filter(t => t.dueDate && t.dueDate < now);
783
+
784
+ const tasksWithEstimates = allTasks.filter(t => t.estimatedMinutes && t.estimatedMinutes > 0);
785
+ const totalEstimatedMinutes = tasksWithEstimates.reduce((sum, t) => sum + (t.estimatedMinutes || 0), 0);
786
+ const avgEstimatedMinutes = tasksWithEstimates.length > 0
787
+ ? Math.round(totalEstimatedMinutes / tasksWithEstimates.length)
788
+ : null;
789
+
790
+ const totalNonDropped = allTasks.filter(t => t.effectiveActive || t.completed).length;
791
+ const completionRate = totalNonDropped > 0
792
+ ? Math.round((completedTasks.length / totalNonDropped) * 100)
793
+ : 0;
794
+
795
+ const projectCounts = {};
796
+ for (const task of allTasks) {
797
+ if (!task.effectiveActive && !task.completed) continue;
798
+ const projectName = task.containingProject ? task.containingProject.name : 'Inbox';
799
+ projectCounts[projectName] = (projectCounts[projectName] || 0) + 1;
800
+ }
801
+ const tasksByProject = computeTopItems(
802
+ Object.entries(projectCounts).map(([name, count]) => ({ name, taskCount: count })),
803
+ 'taskCount'
804
+ );
805
+
806
+ const tagCounts = {};
807
+ for (const task of allTasks) {
808
+ if (!task.effectiveActive && !task.completed) continue;
809
+ for (const tag of task.tags) {
810
+ tagCounts[tag.name] = (tagCounts[tag.name] || 0) + 1;
811
+ }
812
+ }
813
+ const tasksByTag = computeTopItems(
814
+ Object.entries(tagCounts).map(([name, count]) => ({ name, taskCount: count })),
815
+ 'taskCount'
816
+ );
817
+
818
+ return JSON.stringify({
819
+ totalTasks: allTasks.length,
820
+ activeTasks: activeTasks.length,
821
+ completedTasks: completedTasks.length,
822
+ flaggedTasks: flaggedTasks.length,
823
+ overdueActiveTasks: overdueActiveTasks.length,
824
+ avgEstimatedMinutes,
825
+ tasksWithEstimates: tasksWithEstimates.length,
826
+ completionRate,
827
+ tasksByProject,
828
+ tasksByTag
829
+ });
830
+ })();
831
+ `;
832
+ const output = await this.executeJXA(this.wrapOmniScript(omniScript));
833
+ return JSON.parse(output);
834
+ }
835
+ async getProjectStats() {
836
+ const omniScript = `
837
+ ${this.OMNI_HELPERS}
838
+ (() => {
839
+ const allProjects = Array.from(flattenedProjects);
840
+
841
+ function isProjectEffectivelyActive(p) {
842
+ if (p.status === Project.Status.Dropped || p.status === Project.Status.Done) return false;
843
+ if (p.parentFolder && !p.parentFolder.effectiveActive) return false;
844
+ return true;
845
+ }
846
+
847
+ const effectivelyActiveProjects = allProjects.filter(isProjectEffectivelyActive);
848
+ const activeProjects = effectivelyActiveProjects.filter(p => p.status === Project.Status.Active);
849
+ const onHoldProjects = effectivelyActiveProjects.filter(p => p.status === Project.Status.OnHold);
850
+ const droppedProjects = allProjects.filter(p => p.status === Project.Status.Dropped);
851
+ const doneProjects = allProjects.filter(p => p.status === Project.Status.Done);
852
+ const sequentialProjects = effectivelyActiveProjects.filter(p => p.sequential);
853
+ const parallelProjects = effectivelyActiveProjects.filter(p => !p.sequential);
854
+
855
+ const totalTasks = effectivelyActiveProjects.reduce((sum, p) => sum + p.flattenedTasks.length, 0);
856
+ const totalRemaining = effectivelyActiveProjects.reduce((sum, p) => {
857
+ return sum + p.flattenedTasks.filter(t => !t.completed).length;
858
+ }, 0);
859
+
860
+ const avgTasksPerProject = computeAverage(totalTasks, effectivelyActiveProjects.length);
861
+ const avgRemainingPerProject = computeAverage(totalRemaining, effectivelyActiveProjects.length);
862
+
863
+ const completionRates = effectivelyActiveProjects
864
+ .filter(p => p.flattenedTasks.length > 0)
865
+ .map(p => {
866
+ const total = p.flattenedTasks.length;
867
+ const completed = p.flattenedTasks.filter(t => t.completed).length;
868
+ return (completed / total) * 100;
869
+ });
870
+
871
+ const avgCompletionRate = completionRates.length > 0
872
+ ? Math.round(completionRates.reduce((sum, rate) => sum + rate, 0) / completionRates.length)
873
+ : 0;
874
+
875
+ const projectsWithMostTasks = computeTopItems(
876
+ effectivelyActiveProjects.map(p => ({ name: p.name, taskCount: p.flattenedTasks.length })),
877
+ 'taskCount'
878
+ );
879
+
880
+ const projectsWithMostRemaining = computeTopItems(
881
+ effectivelyActiveProjects
882
+ .map(p => ({ name: p.name, remainingCount: p.flattenedTasks.filter(t => !t.completed).length }))
883
+ .filter(p => p.remainingCount > 0),
884
+ 'remainingCount'
885
+ );
886
+
887
+ return JSON.stringify({
888
+ totalProjects: allProjects.length,
889
+ activeProjects: activeProjects.length,
890
+ onHoldProjects: onHoldProjects.length,
891
+ droppedProjects: droppedProjects.length,
892
+ doneProjects: doneProjects.length,
893
+ sequentialProjects: sequentialProjects.length,
894
+ parallelProjects: parallelProjects.length,
895
+ avgTasksPerProject,
896
+ avgRemainingPerProject,
897
+ avgCompletionRate,
898
+ projectsWithMostTasks,
899
+ projectsWithMostRemaining
900
+ });
901
+ })();
902
+ `;
903
+ const output = await this.executeJXA(this.wrapOmniScript(omniScript));
904
+ return JSON.parse(output);
905
+ }
906
+ async listFolders(filters = {}) {
907
+ const includeDropped = filters.includeDropped ?? false;
908
+ const omniScript = `
909
+ ${this.OMNI_HELPERS}
910
+ (() => {
911
+ const includeDropped = ${includeDropped};
912
+ const results = [];
913
+ for (const folder of folders) {
914
+ if (!includeDropped && !folder.effectiveActive) continue;
915
+ results.push(serializeFolder(folder, includeDropped));
916
+ }
917
+ return JSON.stringify(results);
918
+ })();
919
+ `;
920
+ const output = await this.executeJXA(this.wrapOmniScript(omniScript));
921
+ return JSON.parse(output);
922
+ }
923
+ async getFolder(idOrName, filters = {}) {
924
+ const includeDropped = filters.includeDropped ?? false;
925
+ const omniScript = `
926
+ ${this.OMNI_HELPERS}
927
+ (() => {
928
+ const includeDropped = ${includeDropped};
929
+
930
+ function findFolder(idOrName) {
931
+ for (const folder of flattenedFolders) {
932
+ if (folder.id.primaryKey === idOrName || folder.name === idOrName) {
933
+ return folder;
934
+ }
935
+ }
936
+ throw new Error("Folder not found: " + idOrName);
937
+ }
938
+
939
+ const folder = findFolder("${this.escapeString(idOrName)}");
940
+ return JSON.stringify(serializeFolder(folder, includeDropped));
941
+ })();
942
+ `;
943
+ const output = await this.executeJXA(this.wrapOmniScript(omniScript));
944
+ return JSON.parse(output);
945
+ }
946
+ };
947
+
948
+ // src/lib/command-utils.ts
949
+ async function executeCommand(loadingMessage, action, onSuccess, failureMessage) {
950
+ const spinner = ora(loadingMessage).start();
951
+ try {
952
+ const result = await action();
953
+ spinner.stop();
954
+ onSuccess?.(result);
955
+ return result;
956
+ } catch (error) {
957
+ spinner.fail(failureMessage || "Command failed");
958
+ console.error(chalk.red(error.message));
959
+ throw error;
960
+ }
961
+ }
962
+ async function executeOmniFocusCommand(loadingMessage, action, onSuccess, failureMessage) {
963
+ const of = new OmniFocus();
964
+ return executeCommand(loadingMessage, () => action(of), onSuccess, failureMessage);
965
+ }
966
+
967
+ // src/commands/task.ts
968
+ function createTaskCommand() {
969
+ const command = new Command("task");
970
+ command.description("Manage OmniFocus tasks");
971
+ command.command("list").alias("ls").description("List tasks").option("-f, --flagged", "Show only flagged tasks").option("-p, --project <name>", "Filter by project").option("-t, --tag <name>", "Filter by tag").option("-c, --completed", "Include completed tasks").action(async (options) => {
972
+ await executeOmniFocusCommand(
973
+ "Loading tasks...",
974
+ (of) => {
975
+ const filters = {
976
+ includeCompleted: options.completed,
977
+ ...options.flagged && { flagged: true },
978
+ ...options.project && { project: options.project },
979
+ ...options.tag && { tag: options.tag }
980
+ };
981
+ return of.listTasks(filters);
982
+ },
983
+ (tasks) => outputJson(tasks),
984
+ "Failed to load tasks"
985
+ );
986
+ });
987
+ command.command("create <name>").description("Create a new task").option("-p, --project <name>", "Assign to project").option("--note <text>", "Add note").option("-t, --tag <tags...>", "Add tags").option("-d, --due <date>", "Set due date (ISO format)").option("-D, --defer <date>", "Set defer date (ISO format)").option("-f, --flagged", "Flag the task").option("-e, --estimate <minutes>", "Estimated time in minutes", parseInt).action(async (name, options) => {
988
+ await executeOmniFocusCommand(
989
+ "Creating task...",
990
+ (of) => of.createTask({
991
+ name,
992
+ note: options.note,
993
+ project: options.project,
994
+ tags: options.tag,
995
+ due: options.due,
996
+ defer: options.defer,
997
+ flagged: options.flagged,
998
+ estimatedMinutes: options.estimate
999
+ }),
1000
+ (task) => outputJson(task),
1001
+ "Failed to create task"
1002
+ );
1003
+ });
1004
+ command.command("update <idOrName>").description("Update an existing task").option("-n, --name <name>", "New name").option("--note <text>", "New note").option("-p, --project <name>", "Move to project").option("-t, --tag <tags...>", "Replace tags").option("-d, --due <date>", "Set due date (ISO format)").option("-D, --defer <date>", "Set defer date (ISO format)").option("-f, --flag", "Flag the task").option("-F, --unflag", "Unflag the task").option("-c, --complete", "Mark as completed").option("-C, --incomplete", "Mark as incomplete").option("-e, --estimate <minutes>", "Estimated time in minutes", parseInt).action(async (idOrName, options) => {
1005
+ await executeOmniFocusCommand(
1006
+ "Updating task...",
1007
+ (of) => {
1008
+ const updates = {
1009
+ ...options.name && { name: options.name },
1010
+ ...options.note !== void 0 && { note: options.note },
1011
+ ...options.project && { project: options.project },
1012
+ ...options.tag && { tags: options.tag },
1013
+ ...options.due !== void 0 && { due: options.due },
1014
+ ...options.defer !== void 0 && { defer: options.defer },
1015
+ ...options.flag && { flagged: true },
1016
+ ...options.unflag && { flagged: false },
1017
+ ...options.complete && { completed: true },
1018
+ ...options.incomplete && { completed: false },
1019
+ ...options.estimate !== void 0 && { estimatedMinutes: options.estimate }
1020
+ };
1021
+ return of.updateTask(idOrName, updates);
1022
+ },
1023
+ (task) => outputJson(task),
1024
+ "Failed to update task"
1025
+ );
1026
+ });
1027
+ command.command("delete <idOrName>").alias("rm").description("Delete a task").action(async (idOrName) => {
1028
+ await executeOmniFocusCommand(
1029
+ "Deleting task...",
1030
+ (of) => of.deleteTask(idOrName),
1031
+ () => outputJson({ message: "Task deleted successfully" }),
1032
+ "Failed to delete task"
1033
+ );
1034
+ });
1035
+ command.command("view <idOrName>").description("View task details").action(async (idOrName) => {
1036
+ await executeOmniFocusCommand(
1037
+ "Loading task...",
1038
+ (of) => of.getTask(idOrName),
1039
+ (task) => outputJson(task),
1040
+ "Failed to load task"
1041
+ );
1042
+ });
1043
+ command.command("stats").description("Show task statistics").action(async () => {
1044
+ await executeOmniFocusCommand(
1045
+ "Analyzing tasks...",
1046
+ (of) => of.getTaskStats(),
1047
+ (stats) => outputJson(stats),
1048
+ "Failed to analyze tasks"
1049
+ );
1050
+ });
1051
+ return command;
1052
+ }
1053
+
1054
+ // src/commands/project.ts
1055
+ import { Command as Command2 } from "commander";
1056
+ function createProjectCommand() {
1057
+ const command = new Command2("project");
1058
+ command.description("Manage OmniFocus projects");
1059
+ command.command("list").alias("ls").description("List projects").option("-f, --folder <name>", "Filter by folder").option("-s, --status <status>", "Filter by status (active, on hold, dropped)").option("-d, --dropped", "Include dropped projects").action(async (options) => {
1060
+ await executeOmniFocusCommand(
1061
+ "Loading projects...",
1062
+ (of) => {
1063
+ const filters = {
1064
+ includeDropped: options.dropped,
1065
+ ...options.folder && { folder: options.folder },
1066
+ ...options.status && { status: options.status }
1067
+ };
1068
+ return of.listProjects(filters);
1069
+ },
1070
+ (projects) => outputJson(projects),
1071
+ "Failed to load projects"
1072
+ );
1073
+ });
1074
+ command.command("create <name>").description("Create a new project").option("-f, --folder <name>", "Assign to folder").option("--note <text>", "Add note").option("-t, --tag <tags...>", "Add tags").option("-s, --sequential", "Make it a sequential project").option("--status <status>", "Set status (active, on hold, dropped)").action(async (name, options) => {
1075
+ await executeOmniFocusCommand(
1076
+ "Creating project...",
1077
+ (of) => of.createProject({
1078
+ name,
1079
+ note: options.note,
1080
+ folder: options.folder,
1081
+ tags: options.tag,
1082
+ sequential: options.sequential,
1083
+ status: options.status
1084
+ }),
1085
+ (project) => outputJson(project),
1086
+ "Failed to create project"
1087
+ );
1088
+ });
1089
+ command.command("update <idOrName>").description("Update an existing project").option("-n, --name <name>", "Rename project").option("--note <text>", "New note").option("-f, --folder <name>", "Move to folder").option("-t, --tag <tags...>", "Replace tags").option("-s, --sequential", "Make it sequential").option("-p, --parallel", "Make it parallel").option("--status <status>", "Set status (active, on hold, dropped)").action(async (idOrName, options) => {
1090
+ await executeOmniFocusCommand(
1091
+ "Updating project...",
1092
+ (of) => {
1093
+ const updates = {
1094
+ ...options.name && { name: options.name },
1095
+ ...options.note !== void 0 && { note: options.note },
1096
+ ...options.folder && { folder: options.folder },
1097
+ ...options.tag && { tags: options.tag },
1098
+ ...options.sequential && { sequential: true },
1099
+ ...options.parallel && { sequential: false },
1100
+ ...options.status && { status: options.status }
1101
+ };
1102
+ return of.updateProject(idOrName, updates);
1103
+ },
1104
+ (project) => outputJson(project),
1105
+ "Failed to update project"
1106
+ );
1107
+ });
1108
+ command.command("delete <idOrName>").alias("rm").description("Delete a project").action(async (idOrName) => {
1109
+ await executeOmniFocusCommand(
1110
+ "Deleting project...",
1111
+ (of) => of.deleteProject(idOrName),
1112
+ () => outputJson({ message: "Project deleted successfully" }),
1113
+ "Failed to delete project"
1114
+ );
1115
+ });
1116
+ command.command("view <idOrName>").description("View project details").action(async (idOrName) => {
1117
+ await executeOmniFocusCommand(
1118
+ "Loading project...",
1119
+ (of) => of.getProject(idOrName),
1120
+ (project) => outputJson(project),
1121
+ "Failed to load project"
1122
+ );
1123
+ });
1124
+ command.command("stats").description("Show project statistics").action(async () => {
1125
+ await executeOmniFocusCommand(
1126
+ "Analyzing projects...",
1127
+ (of) => of.getProjectStats(),
1128
+ (stats) => outputJson(stats),
1129
+ "Failed to analyze projects"
1130
+ );
1131
+ });
1132
+ return command;
1133
+ }
1134
+
1135
+ // src/commands/inbox.ts
1136
+ import { Command as Command3 } from "commander";
1137
+ function createInboxCommand() {
1138
+ const command = new Command3("inbox");
1139
+ command.description("Manage OmniFocus inbox");
1140
+ command.command("list").alias("ls").description("List inbox tasks").action(async () => {
1141
+ await executeOmniFocusCommand(
1142
+ "Loading inbox...",
1143
+ (of) => of.listInboxTasks(),
1144
+ (tasks) => outputJson(tasks),
1145
+ "Failed to load inbox"
1146
+ );
1147
+ });
1148
+ command.command("count").description("Get inbox count").action(async () => {
1149
+ await executeOmniFocusCommand(
1150
+ "Counting inbox items...",
1151
+ (of) => of.getInboxCount(),
1152
+ (count) => outputJson({ count }),
1153
+ "Failed to get inbox count"
1154
+ );
1155
+ });
1156
+ return command;
1157
+ }
1158
+
1159
+ // src/commands/search.ts
1160
+ import { Command as Command4 } from "commander";
1161
+ function createSearchCommand() {
1162
+ const command = new Command4("search");
1163
+ command.description("Search tasks by name or note");
1164
+ command.argument("<query>", "Search query");
1165
+ command.action(async (query) => {
1166
+ await executeOmniFocusCommand(
1167
+ "Searching...",
1168
+ (of) => of.searchTasks(query),
1169
+ (tasks) => outputJson(tasks),
1170
+ "Search failed"
1171
+ );
1172
+ });
1173
+ return command;
1174
+ }
1175
+
1176
+ // src/commands/perspective.ts
1177
+ import { Command as Command5 } from "commander";
1178
+ function createPerspectiveCommand() {
1179
+ const command = new Command5("perspective");
1180
+ command.description("Manage OmniFocus perspectives");
1181
+ command.command("list").alias("ls").description("List all perspectives").action(async () => {
1182
+ await executeOmniFocusCommand(
1183
+ "Loading perspectives...",
1184
+ (of) => of.listPerspectives(),
1185
+ (perspectives) => outputJson(perspectives),
1186
+ "Failed to load perspectives"
1187
+ );
1188
+ });
1189
+ command.command("view <name>").description("View tasks in a perspective").action(async (name) => {
1190
+ await executeOmniFocusCommand(
1191
+ `Loading perspective "${name}"...`,
1192
+ (of) => of.getPerspectiveTasks(name),
1193
+ (tasks) => outputJson(tasks),
1194
+ "Failed to load perspective"
1195
+ );
1196
+ });
1197
+ return command;
1198
+ }
1199
+
1200
+ // src/commands/tag.ts
1201
+ import { Command as Command6 } from "commander";
1202
+ function createTagCommand() {
1203
+ const command = new Command6("tag");
1204
+ command.description("Manage and analyze OmniFocus tags");
1205
+ command.command("list").alias("ls").description("List tags with usage information").option("-u, --unused-days <days>", "Show tags unused for N days", parseInt).option("-s, --sort <field>", "Sort by: name, usage, activity (default: name)", "name").option("-a, --active-only", "Only count active (incomplete) tasks").action(async (options) => {
1206
+ await executeOmniFocusCommand(
1207
+ "Loading tags...",
1208
+ (of) => of.listTags({
1209
+ unusedDays: options.unusedDays,
1210
+ sortBy: options.sort,
1211
+ activeOnly: options.activeOnly
1212
+ }),
1213
+ (tags) => outputJson(tags),
1214
+ "Failed to load tags"
1215
+ );
1216
+ });
1217
+ command.command("create <name>").description("Create a new tag").option("-p, --parent <name>", "Create as child of parent tag").option("-s, --status <status>", "Set status (active, on hold, dropped)").action(async (name, options) => {
1218
+ await executeOmniFocusCommand(
1219
+ "Creating tag...",
1220
+ (of) => of.createTag({
1221
+ name,
1222
+ parent: options.parent,
1223
+ status: options.status
1224
+ }),
1225
+ (tag) => outputJson(tag),
1226
+ "Failed to create tag"
1227
+ );
1228
+ });
1229
+ command.command("view <idOrName>").description("View tag details").action(async (idOrName) => {
1230
+ await executeOmniFocusCommand(
1231
+ "Loading tag...",
1232
+ (of) => of.getTag(idOrName),
1233
+ (tag) => outputJson(tag),
1234
+ "Failed to load tag"
1235
+ );
1236
+ });
1237
+ command.command("update <idOrName>").description("Update an existing tag").option("-n, --name <name>", "Rename tag").option("-s, --status <status>", "Set status (active, on hold, dropped)").action(async (idOrName, options) => {
1238
+ await executeOmniFocusCommand(
1239
+ "Updating tag...",
1240
+ (of) => {
1241
+ const updates = {
1242
+ ...options.name && { name: options.name },
1243
+ ...options.status && { status: options.status }
1244
+ };
1245
+ return of.updateTag(idOrName, updates);
1246
+ },
1247
+ (tag) => outputJson(tag),
1248
+ "Failed to update tag"
1249
+ );
1250
+ });
1251
+ command.command("delete <idOrName>").alias("rm").description("Delete a tag").action(async (idOrName) => {
1252
+ await executeOmniFocusCommand(
1253
+ "Deleting tag...",
1254
+ (of) => of.deleteTag(idOrName),
1255
+ () => outputJson({ message: "Tag deleted successfully" }),
1256
+ "Failed to delete tag"
1257
+ );
1258
+ });
1259
+ command.command("stats").description("Show tag usage statistics").action(async () => {
1260
+ await executeOmniFocusCommand(
1261
+ "Analyzing tags...",
1262
+ (of) => of.getTagStats(),
1263
+ (stats) => outputJson(stats),
1264
+ "Failed to analyze tags"
1265
+ );
1266
+ });
1267
+ return command;
1268
+ }
1269
+
1270
+ // src/commands/folder.ts
1271
+ import { Command as Command7 } from "commander";
1272
+ function createFolderCommand() {
1273
+ const command = new Command7("folder");
1274
+ command.description("View OmniFocus folder hierarchy");
1275
+ command.command("list").alias("ls").description("List top-level folders with nested children").option("-d, --dropped", "Include dropped folders").action(async (options) => {
1276
+ await executeOmniFocusCommand(
1277
+ "Loading folders...",
1278
+ (of) => of.listFolders({ includeDropped: options.dropped }),
1279
+ (folders) => outputJson(folders),
1280
+ "Failed to load folders"
1281
+ );
1282
+ });
1283
+ command.command("view <idOrName>").description("View folder details and children").option("-d, --dropped", "Include dropped child folders").action(async (idOrName, options) => {
1284
+ const filters = { includeDropped: options.dropped };
1285
+ await executeOmniFocusCommand(
1286
+ "Loading folder...",
1287
+ (of) => of.getFolder(idOrName, filters),
1288
+ (folder) => outputJson(folder),
1289
+ "Failed to load folder"
1290
+ );
1291
+ });
1292
+ return command;
1293
+ }
1294
+
1295
+ // src/index.ts
1296
+ var program = new Command8();
1297
+ program.name("of").description("A command-line interface for OmniFocus on macOS").version("1.3.0").option("-c, --compact", "Minified JSON output (single line)").hook("preAction", (thisCommand) => {
1298
+ const options = thisCommand.opts();
1299
+ setOutputOptions({
1300
+ compact: options.compact
1301
+ });
21
1302
  });
22
1303
  program.addCommand(createTaskCommand());
23
1304
  program.addCommand(createProjectCommand());
@@ -25,7 +1306,8 @@ program.addCommand(createInboxCommand());
25
1306
  program.addCommand(createSearchCommand());
26
1307
  program.addCommand(createPerspectiveCommand());
27
1308
  program.addCommand(createTagCommand());
1309
+ program.addCommand(createFolderCommand());
28
1310
  program.parseAsync().catch(() => {
29
- process.exit(1);
1311
+ process.exit(1);
30
1312
  });
31
1313
  //# sourceMappingURL=index.js.map