omnifocus_mcp 1.0.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 (108) hide show
  1. checksums.yaml +7 -0
  2. data/AGENTS.md +15 -0
  3. data/CHANGELOG.md +7 -0
  4. data/CODE_OF_CONDUCT.md +16 -0
  5. data/LICENSE.txt +21 -0
  6. data/README.md +147 -0
  7. data/Rakefile +12 -0
  8. data/bin/omnifocus-mcp +7 -0
  9. data/lib/omnifocus_mcp/config.rb +18 -0
  10. data/lib/omnifocus_mcp/infrastructure/.keep +1 -0
  11. data/lib/omnifocus_mcp/infrastructure/apple_script.rb +263 -0
  12. data/lib/omnifocus_mcp/infrastructure/apple_script_date_builder.rb +65 -0
  13. data/lib/omnifocus_mcp/infrastructure/js_embed.rb +39 -0
  14. data/lib/omnifocus_mcp/infrastructure/script_runner.rb +254 -0
  15. data/lib/omnifocus_mcp/infrastructure.rb +6 -0
  16. data/lib/omnifocus_mcp/json_rpc_compat.rb +75 -0
  17. data/lib/omnifocus_mcp/logger.rb +34 -0
  18. data/lib/omnifocus_mcp/mcp.rb +74 -0
  19. data/lib/omnifocus_mcp/parsers/.keep +1 -0
  20. data/lib/omnifocus_mcp/parsers/apple_script_envelope.rb +44 -0
  21. data/lib/omnifocus_mcp/parsers.rb +6 -0
  22. data/lib/omnifocus_mcp/resources/base.rb +87 -0
  23. data/lib/omnifocus_mcp/resources/flagged_resource.rb +31 -0
  24. data/lib/omnifocus_mcp/resources/inbox_resource.rb +31 -0
  25. data/lib/omnifocus_mcp/resources/perspective_resource.rb +28 -0
  26. data/lib/omnifocus_mcp/resources/project_resource.rb +37 -0
  27. data/lib/omnifocus_mcp/resources/stats_resource.rb +22 -0
  28. data/lib/omnifocus_mcp/resources/today_resource.rb +37 -0
  29. data/lib/omnifocus_mcp/result.rb +108 -0
  30. data/lib/omnifocus_mcp/tools/batch_report.rb +9 -0
  31. data/lib/omnifocus_mcp/tools/database_stats.rb +184 -0
  32. data/lib/omnifocus_mcp/tools/definitions/add_omnifocus_task_tool.rb +61 -0
  33. data/lib/omnifocus_mcp/tools/definitions/add_project_tool.rb +54 -0
  34. data/lib/omnifocus_mcp/tools/definitions/batch_add_items_tool.rb +105 -0
  35. data/lib/omnifocus_mcp/tools/definitions/batch_remove_items_tool.rb +68 -0
  36. data/lib/omnifocus_mcp/tools/definitions/date_formatter.rb +45 -0
  37. data/lib/omnifocus_mcp/tools/definitions/edit_item_tool.rb +87 -0
  38. data/lib/omnifocus_mcp/tools/definitions/get_perspective_view_tool.rb +57 -0
  39. data/lib/omnifocus_mcp/tools/definitions/key_normalizer.rb +30 -0
  40. data/lib/omnifocus_mcp/tools/definitions/list_perspectives_tool.rb +47 -0
  41. data/lib/omnifocus_mcp/tools/definitions/list_tags_tool.rb +42 -0
  42. data/lib/omnifocus_mcp/tools/definitions/mcp_envelope.rb +31 -0
  43. data/lib/omnifocus_mcp/tools/definitions/operation_factory.rb +33 -0
  44. data/lib/omnifocus_mcp/tools/definitions/query_omnifocus_tool.rb +187 -0
  45. data/lib/omnifocus_mcp/tools/definitions/remove_item_tool.rb +55 -0
  46. data/lib/omnifocus_mcp/tools/generators/.keep +1 -0
  47. data/lib/omnifocus_mcp/tools/generators/add_omnifocus_task.rb +348 -0
  48. data/lib/omnifocus_mcp/tools/generators/add_project.rb +141 -0
  49. data/lib/omnifocus_mcp/tools/generators/database_stats.rb +16 -0
  50. data/lib/omnifocus_mcp/tools/generators/edit_item.rb +455 -0
  51. data/lib/omnifocus_mcp/tools/generators/list_perspectives.rb +13 -0
  52. data/lib/omnifocus_mcp/tools/generators/list_tags.rb +13 -0
  53. data/lib/omnifocus_mcp/tools/generators/perspective_view.rb +17 -0
  54. data/lib/omnifocus_mcp/tools/generators/query_omnifocus.rb +571 -0
  55. data/lib/omnifocus_mcp/tools/generators/query_omnifocus_debug.rb +169 -0
  56. data/lib/omnifocus_mcp/tools/generators/remove_item.rb +61 -0
  57. data/lib/omnifocus_mcp/tools/generators.rb +8 -0
  58. data/lib/omnifocus_mcp/tools/messages/add_omnifocus_task.rb +53 -0
  59. data/lib/omnifocus_mcp/tools/messages/add_project.rb +28 -0
  60. data/lib/omnifocus_mcp/tools/messages/batch_remove_items.rb +13 -0
  61. data/lib/omnifocus_mcp/tools/messages/edit_item.rb +39 -0
  62. data/lib/omnifocus_mcp/tools/messages/list_tools.rb +15 -0
  63. data/lib/omnifocus_mcp/tools/messages/remove_item.rb +42 -0
  64. data/lib/omnifocus_mcp/tools/messages.rb +8 -0
  65. data/lib/omnifocus_mcp/tools/operations/add_omnifocus_task.rb +74 -0
  66. data/lib/omnifocus_mcp/tools/operations/add_project.rb +75 -0
  67. data/lib/omnifocus_mcp/tools/operations/batch_add_items/batch_item.rb +38 -0
  68. data/lib/omnifocus_mcp/tools/operations/batch_add_items/bulk_executor.rb +94 -0
  69. data/lib/omnifocus_mcp/tools/operations/batch_add_items/cycle_detector.rb +74 -0
  70. data/lib/omnifocus_mcp/tools/operations/batch_add_items/param_builder.rb +47 -0
  71. data/lib/omnifocus_mcp/tools/operations/batch_add_items/planner.rb +111 -0
  72. data/lib/omnifocus_mcp/tools/operations/batch_add_items.rb +149 -0
  73. data/lib/omnifocus_mcp/tools/operations/batch_remove_items.rb +49 -0
  74. data/lib/omnifocus_mcp/tools/operations/database_stats.rb +52 -0
  75. data/lib/omnifocus_mcp/tools/operations/edit_item.rb +79 -0
  76. data/lib/omnifocus_mcp/tools/operations/get_perspective_view.rb +112 -0
  77. data/lib/omnifocus_mcp/tools/operations/list_perspectives.rb +85 -0
  78. data/lib/omnifocus_mcp/tools/operations/list_tags.rb +80 -0
  79. data/lib/omnifocus_mcp/tools/operations/query_omnifocus.rb +74 -0
  80. data/lib/omnifocus_mcp/tools/operations/query_omnifocus_debug.rb +63 -0
  81. data/lib/omnifocus_mcp/tools/operations/remove_item.rb +75 -0
  82. data/lib/omnifocus_mcp/tools/operations.rb +8 -0
  83. data/lib/omnifocus_mcp/tools/params/mcp_boundary.rb +41 -0
  84. data/lib/omnifocus_mcp/tools/params.rb +106 -0
  85. data/lib/omnifocus_mcp/tools/presenters/batch_report.rb +55 -0
  86. data/lib/omnifocus_mcp/tools/presenters/list_perspectives.rb +33 -0
  87. data/lib/omnifocus_mcp/tools/presenters/list_tags.rb +49 -0
  88. data/lib/omnifocus_mcp/tools/presenters/perspective_view.rb +81 -0
  89. data/lib/omnifocus_mcp/tools/presenters/query_reply.rb +52 -0
  90. data/lib/omnifocus_mcp/tools/presenters/query_results.rb +183 -0
  91. data/lib/omnifocus_mcp/tools/presenters.rb +8 -0
  92. data/lib/omnifocus_mcp/tools/query_omnifocus_formatter.rb +9 -0
  93. data/lib/omnifocus_mcp/tools/query_statuses.rb +22 -0
  94. data/lib/omnifocus_mcp/utils/apple_script.rb +9 -0
  95. data/lib/omnifocus_mcp/utils/apple_script_envelope.rb +9 -0
  96. data/lib/omnifocus_mcp/utils/apple_script_helpers.rb +9 -0
  97. data/lib/omnifocus_mcp/utils/blank.rb +26 -0
  98. data/lib/omnifocus_mcp/utils/date_filter.rb +76 -0
  99. data/lib/omnifocus_mcp/utils/date_formatting.rb +9 -0
  100. data/lib/omnifocus_mcp/utils/iso_date.rb +27 -0
  101. data/lib/omnifocus_mcp/utils/omnifocus_scripts/getPerspectiveView.js +472 -0
  102. data/lib/omnifocus_mcp/utils/omnifocus_scripts/listPerspectives.js +59 -0
  103. data/lib/omnifocus_mcp/utils/omnifocus_scripts/listTags.js +58 -0
  104. data/lib/omnifocus_mcp/utils/omnifocus_scripts/omnifocusDump.js +223 -0
  105. data/lib/omnifocus_mcp/utils/script_execution.rb +9 -0
  106. data/lib/omnifocus_mcp/version.rb +5 -0
  107. data/lib/omnifocus_mcp.rb +102 -0
  108. metadata +166 -0
@@ -0,0 +1,472 @@
1
+ // OmniJS script to get perspective view in OmniFocus using rule evaluation
2
+ // Usage: Call with perspective name and limit as parameters
3
+ // Example: getPerspectiveViewByName("Today", 100)
4
+
5
+ function getPerspectiveViewByName(perspectiveName, limit = 100) {
6
+ try {
7
+ let currentPerspective = null;
8
+
9
+ if (perspectiveName.toLowerCase() === "inbox") {
10
+ currentPerspective = Perspective.BuiltIn.Inbox;
11
+ } else if (perspectiveName.toLowerCase() === "projects") {
12
+ currentPerspective = Perspective.BuiltIn.Projects;
13
+ } else if (perspectiveName.toLowerCase() === "tags") {
14
+ currentPerspective = Perspective.BuiltIn.Tags;
15
+ } else if (perspectiveName.toLowerCase() === "forecast") {
16
+ currentPerspective = Perspective.BuiltIn.Forecast;
17
+ } else if (perspectiveName.toLowerCase() === "flagged") {
18
+ currentPerspective = Perspective.BuiltIn.Flagged;
19
+ } else if (perspectiveName.toLowerCase() === "review") {
20
+ currentPerspective = Perspective.BuiltIn.Review;
21
+ } else {
22
+ currentPerspective = Perspective.Custom.byName(perspectiveName);
23
+ }
24
+
25
+ if (!currentPerspective) {
26
+ return JSON.stringify({
27
+ success: false,
28
+ error: "Could not find perspective named '" + perspectiveName + "'",
29
+ });
30
+ }
31
+ let perspectiveDisplayName = "Unknown";
32
+
33
+ if (currentPerspective) {
34
+ if (currentPerspective === Perspective.BuiltIn.Inbox) {
35
+ perspectiveDisplayName = "Inbox";
36
+ } else if (currentPerspective === Perspective.BuiltIn.Projects) {
37
+ perspectiveDisplayName = "Projects";
38
+ } else if (currentPerspective === Perspective.BuiltIn.Tags) {
39
+ perspectiveDisplayName = "Tags";
40
+ } else if (currentPerspective === Perspective.BuiltIn.Forecast) {
41
+ perspectiveDisplayName = "Forecast";
42
+ } else if (currentPerspective === Perspective.BuiltIn.Flagged) {
43
+ perspectiveDisplayName = "Flagged";
44
+ } else if (currentPerspective === Perspective.BuiltIn.Review) {
45
+ perspectiveDisplayName = "Review";
46
+ } else if (currentPerspective.name) {
47
+ perspectiveDisplayName = currentPerspective.name;
48
+ }
49
+ }
50
+
51
+ var evaluateActionAvailability = (task, value) => {
52
+ let result;
53
+ if (value === "remaining") {
54
+ result = !task.completed && task.taskStatus !== Task.Status.Dropped;
55
+ } else if (value === "completed") {
56
+ result = task.completed;
57
+ } else if (value === "dropped") {
58
+ result = task.taskStatus === Task.Status.Dropped;
59
+ } else if (value === "available") {
60
+ // "available" is defined here: https://support.omnigroup.com/documentation/omnifocus/universal/4.3.3/en/glossary/#view-options
61
+ const isActive =
62
+ !task.completed && task.taskStatus !== Task.Status.Dropped;
63
+ const isAvailable =
64
+ task.taskStatus !== Task.Status.Blocked &&
65
+ (!task.deferDate || task.deferDate <= new Date());
66
+ result = isActive && isAvailable;
67
+ } else if (value === "firstAvailable") {
68
+ // "firstAvailable" specifically means the Available status
69
+ result = task.taskStatus === Task.Status.Available;
70
+ } else {
71
+ result = false;
72
+ }
73
+ return result;
74
+ };
75
+
76
+ var evaluateActionStatus = (task, value) => {
77
+ if (value === "due")
78
+ return (
79
+ task.taskStatus === Task.Status.DueSoon ||
80
+ task.taskStatus === Task.Status.Overdue
81
+ );
82
+ if (value === "flagged") return task.flagged;
83
+ return false;
84
+ };
85
+
86
+ var evaluateActionHasDueDate = (task, value) =>
87
+ (task.dueDate !== null) === value;
88
+ var evaluateActionHasDeferDate = (task, value) =>
89
+ (task.deferDate !== null) === value;
90
+ var evaluateActionHasDuration = (task, value) =>
91
+ (task.estimatedMinutes !== null) === value;
92
+ var evaluateActionWithinDuration = (task, value) =>
93
+ task.estimatedMinutes !== null && task.estimatedMinutes <= value;
94
+ var evaluateActionIsProject = (task, value) =>
95
+ (task.children &&
96
+ task.children.length > 0 &&
97
+ task.containingProject === null) === value;
98
+ var evaluateActionIsGroup = (task, value) =>
99
+ (task.children &&
100
+ task.children.length > 0 &&
101
+ task.containingProject !== null) === value;
102
+ var evaluateActionIsProjectOrGroup = (task, value) =>
103
+ (task.children && task.children.length > 0) === value;
104
+ var evaluateActionRepeats = (task, value) =>
105
+ (task.repetitionRule !== null) === value;
106
+ var evaluateActionIsUntagged = (task, value) =>
107
+ (task.tags.length === 0) === value;
108
+ var evaluateActionHasTagWithStatus = (task, value) => {
109
+ return task.tags.some((tag) => {
110
+ // Map OmniFocus tag status values
111
+ if (value === "remaining") return !tag.effectivelyDropped;
112
+ if (value === "active") return tag.effectivelyActive;
113
+ if (value === "onHold") return tag.effectivelyOnHold;
114
+ if (value === "dropped") return tag.effectivelyDropped;
115
+ return false;
116
+ });
117
+ };
118
+ var evaluateActionIsLeaf = (task, value) =>
119
+ (!task.children || task.children.length === 0) === value;
120
+ var evaluateActionHasNoProject = (task, value) =>
121
+ (task.containingProject === null) === value;
122
+ var evaluateActionIsInSingleActionsList = (task, value) => {
123
+ const project = task.containingProject;
124
+ if (!project) return false;
125
+ return (project.status === Project.Status.SingleActions) === value;
126
+ };
127
+ var evaluateActionHasProjectWithStatus = (task, value) => {
128
+ const project = task.containingProject;
129
+ if (!project) return false;
130
+ if (value === "remaining") {
131
+ return !project.effectivelyCompleted && !project.effectivelyDropped;
132
+ }
133
+ if (value === "completed") return project.effectivelyCompleted;
134
+ if (value === "dropped") return project.effectivelyDropped;
135
+ const statusMap = {
136
+ active: Project.Status.Active,
137
+ onHold: Project.Status.OnHold,
138
+ stalled: Project.Status.Stalled,
139
+ pending: Project.Status.Pending,
140
+ };
141
+ return project.status === statusMap[value];
142
+ };
143
+
144
+ var evaluateActionHasAnyOfTags = (task, value) => {
145
+ if (!Array.isArray(value) || task.tags.length === 0) return false;
146
+ const taskTagIds = task.tags.map((tag) => tag.id.primaryKey);
147
+ return value.some((tagId) => taskTagIds.includes(tagId));
148
+ };
149
+
150
+ var evaluateActionHasAllOfTags = (task, value) => {
151
+ if (!Array.isArray(value) || task.tags.length === 0) return false;
152
+ const taskTagIds = task.tags.map((tag) => tag.id.primaryKey);
153
+ return value.every((tagId) => taskTagIds.includes(tagId));
154
+ };
155
+
156
+ var evaluateActionWithinFocus = (task, value) => {
157
+ if (!Array.isArray(value)) return false;
158
+
159
+ function isWithinHierarchy(item) {
160
+ if (!item) return false;
161
+
162
+ if (value.includes(item.id.primaryKey)) {
163
+ return true;
164
+ }
165
+
166
+ if (item.parentFolder) {
167
+ return isWithinHierarchy(item.parentFolder);
168
+ }
169
+
170
+ if (item.parent && item.parent !== item) {
171
+ return isWithinHierarchy(item.parent);
172
+ }
173
+
174
+ return false;
175
+ }
176
+
177
+ if (task.containingProject) {
178
+ return isWithinHierarchy(task.containingProject);
179
+ }
180
+
181
+ // Task has no containing project — don't match the task's own ID
182
+ // against the focus list, as the list contains project/folder IDs
183
+ return false;
184
+ };
185
+
186
+ var evaluateActionMatchingSearch = (task, value) => {
187
+ if (!Array.isArray(value)) return false;
188
+ const searchText = (task.name + " " + (task.note || "")).toLowerCase();
189
+ return value.some((term) => searchText.includes(term.toLowerCase()));
190
+ };
191
+
192
+ var evaluateActionDateIsToday = (task, dateField) => {
193
+ const fieldDate = task[dateField + "Date"]; // e.g., dueDate, deferDate
194
+ if (!fieldDate) return false;
195
+ const today = new Date();
196
+ return fieldDate.toDateString() === today.toDateString();
197
+ };
198
+
199
+ var evaluateActionDateIsYesterday = (task, dateField) => {
200
+ const fieldDate = task[dateField + "Date"];
201
+ if (!fieldDate) return false;
202
+ const yesterday = new Date();
203
+ yesterday.setDate(yesterday.getDate() - 1);
204
+ return fieldDate.toDateString() === yesterday.toDateString();
205
+ };
206
+
207
+ var evaluateActionDateIsTomorrow = (task, dateField) => {
208
+ const fieldDate = task[dateField + "Date"];
209
+ if (!fieldDate) return false;
210
+ const tomorrow = new Date();
211
+ tomorrow.setDate(tomorrow.getDate() + 1);
212
+ return fieldDate.toDateString() === tomorrow.toDateString();
213
+ };
214
+
215
+ // filter rules and values are defined here: https://omni-automation.com/omnifocus/perspective.html
216
+ var possibleRuleTypes = {
217
+ actionAvailability: evaluateActionAvailability,
218
+ actionStatus: evaluateActionStatus,
219
+ actionHasDueDate: evaluateActionHasDueDate,
220
+ actionHasDeferDate: evaluateActionHasDeferDate,
221
+ actionHasDuration: evaluateActionHasDuration,
222
+ actionWithinDuration: evaluateActionWithinDuration,
223
+ actionIsProject: evaluateActionIsProject,
224
+ actionIsGroup: evaluateActionIsGroup,
225
+ actionIsProjectOrGroup: evaluateActionIsProjectOrGroup,
226
+ actionRepeats: evaluateActionRepeats,
227
+ actionIsUntagged: evaluateActionIsUntagged,
228
+ actionHasTagWithStatus: evaluateActionHasTagWithStatus,
229
+ actionHasAnyOfTags: evaluateActionHasAnyOfTags,
230
+ actionHasAllOfTags: evaluateActionHasAllOfTags,
231
+ actionIsLeaf: evaluateActionIsLeaf,
232
+ actionHasNoProject: evaluateActionHasNoProject,
233
+ actionIsInSingleActionsList: evaluateActionIsInSingleActionsList,
234
+ actionHasProjectWithStatus: evaluateActionHasProjectWithStatus,
235
+ actionWithinFocus: evaluateActionWithinFocus,
236
+ actionMatchingSearch: evaluateActionMatchingSearch,
237
+ };
238
+
239
+ function evaluateRule(task, rule) {
240
+ // Handle complex date field rules
241
+ if (rule.actionDateField) {
242
+ const dateField = rule.actionDateField;
243
+
244
+ // Check for date-specific conditions
245
+ if (rule.actionDateIsToday) {
246
+ return evaluateActionDateIsToday(task, dateField);
247
+ }
248
+ if (rule.actionDateIsYesterday) {
249
+ return evaluateActionDateIsYesterday(task, dateField);
250
+ }
251
+ if (rule.actionDateIsTomorrow) {
252
+ return evaluateActionDateIsTomorrow(task, dateField);
253
+ }
254
+
255
+ // Add other date conditions as needed
256
+ return false;
257
+ }
258
+
259
+ // Skip disabled rules — they are toggled off in the perspective editor
260
+ if (rule.disabledRule !== undefined) {
261
+ return true;
262
+ }
263
+
264
+ // Handle standard rules
265
+ for (const [key, value] of Object.entries(rule)) {
266
+ if (possibleRuleTypes[key]) {
267
+ return possibleRuleTypes[key](task, value);
268
+ }
269
+ }
270
+
271
+ return false;
272
+ }
273
+
274
+ function evaluateTask(task, filters, aggregationType = "all") {
275
+ if (!Array.isArray(filters) || filters.length === 0) return true;
276
+
277
+ const results = filters.map((filter) => {
278
+ if (filter.aggregateType && filter.aggregateRules) {
279
+ // Handle nested aggregate rules
280
+ return evaluateTask(
281
+ task,
282
+ filter.aggregateRules,
283
+ filter.aggregateType
284
+ );
285
+ } else {
286
+ // Handle single rule
287
+ return evaluateRule(task, filter);
288
+ }
289
+ });
290
+
291
+ switch (aggregationType) {
292
+ case "any":
293
+ return results.some((result) => result);
294
+ case "all":
295
+ return results.every((result) => result);
296
+ case "none":
297
+ return results.every((result) => !result);
298
+ default:
299
+ return results.every((result) => result);
300
+ }
301
+ }
302
+
303
+ // Helper functions
304
+ function formatDate(date) {
305
+ if (!date) return null;
306
+ return date.toISOString();
307
+ }
308
+
309
+ function getTaskDetails(task) {
310
+ // Task status mapping aligned with query_omnifocus
311
+ const taskStatusMap = {
312
+ [Task.Status.Available]: "Available",
313
+ [Task.Status.Blocked]: "Blocked",
314
+ [Task.Status.Completed]: "Completed",
315
+ [Task.Status.Dropped]: "Dropped",
316
+ [Task.Status.DueSoon]: "DueSoon",
317
+ [Task.Status.Next]: "Next",
318
+ [Task.Status.Overdue]: "Overdue",
319
+ };
320
+
321
+ return {
322
+ id: task.id.primaryKey,
323
+ name: task.name,
324
+ completed: Boolean(task.completed),
325
+ flagged: Boolean(task.flagged),
326
+ note: task.note || "",
327
+ dueDate: formatDate(task.dueDate),
328
+ deferDate: formatDate(task.deferDate),
329
+ completionDate: formatDate(task.completionDate),
330
+ estimatedMinutes: task.estimatedMinutes
331
+ ? Number(task.estimatedMinutes)
332
+ : null,
333
+ taskStatus: taskStatusMap[task.taskStatus] || "Unknown",
334
+ projectName: task.containingProject
335
+ ? task.containingProject.name
336
+ : null,
337
+ tagNames: (task.tags || [])
338
+ .map((tag) => tag.name)
339
+ .filter((name) => name),
340
+ };
341
+ }
342
+
343
+ let perspectiveRules = null;
344
+ let perspectiveAggregation = "all";
345
+ let isCustomPerspective = false;
346
+
347
+ try {
348
+ isCustomPerspective =
349
+ currentPerspective &&
350
+ currentPerspective !== Perspective.BuiltIn.Inbox &&
351
+ currentPerspective !== Perspective.BuiltIn.Projects &&
352
+ currentPerspective !== Perspective.BuiltIn.Tags &&
353
+ currentPerspective !== Perspective.BuiltIn.Forecast &&
354
+ currentPerspective !== Perspective.BuiltIn.Flagged &&
355
+ currentPerspective !== Perspective.BuiltIn.Review;
356
+
357
+ if (isCustomPerspective && currentPerspective.archivedFilterRules) {
358
+ if (typeof currentPerspective.archivedFilterRules === "string") {
359
+ perspectiveRules = JSON.parse(currentPerspective.archivedFilterRules);
360
+ } else {
361
+ perspectiveRules = currentPerspective.archivedFilterRules;
362
+ }
363
+ perspectiveAggregation =
364
+ currentPerspective.archivedTopLevelFilterAggregation || "all";
365
+ }
366
+ } catch (e) {
367
+ // If we can't parse the rules, fall back to getting all available tasks
368
+ perspectiveRules = null;
369
+ var ruleParseError = e.toString();
370
+ }
371
+
372
+ let filteredTasks = [];
373
+
374
+ if (isCustomPerspective && perspectiveRules) {
375
+ flattenedTasks.forEach((task) => {
376
+ if (evaluateTask(task, perspectiveRules, perspectiveAggregation)) {
377
+ filteredTasks.push(getTaskDetails(task));
378
+ }
379
+ });
380
+ } else {
381
+ // Use built-in perspective logic for default perspectives
382
+ if (perspectiveName === "Inbox") {
383
+ inbox.forEach((task) => {
384
+ filteredTasks.push(getTaskDetails(task));
385
+ });
386
+ } else if (perspectiveName === "Flagged") {
387
+ flattenedTasks.forEach((task) => {
388
+ if (task.flagged && !task.completed) {
389
+ filteredTasks.push(getTaskDetails(task));
390
+ }
391
+ });
392
+ } else if (perspectiveName === "Projects") {
393
+ flattenedProjects.forEach((project) => {
394
+ if (project.status === Project.Status.Active) {
395
+ const projectTask = project.task;
396
+ if (projectTask) {
397
+ filteredTasks.push(getTaskDetails(projectTask));
398
+ }
399
+ }
400
+ });
401
+ } else if (perspectiveName === "Tags") {
402
+ flattenedTags.forEach((tag) => {
403
+ tag.remainingTasks.forEach((task) => {
404
+ const taskDetail = getTaskDetails(task);
405
+ if (!filteredTasks.some((item) => item.id === taskDetail.id)) {
406
+ filteredTasks.push(taskDetail);
407
+ }
408
+ });
409
+ });
410
+ } else {
411
+ flattenedTasks.forEach((task) => {
412
+ if (task.taskStatus === Task.Status.Available && !task.completed) {
413
+ filteredTasks.push(getTaskDetails(task));
414
+ }
415
+ });
416
+ }
417
+ }
418
+
419
+ const response = {
420
+ success: true,
421
+ perspectiveName: perspectiveDisplayName,
422
+ isCustomPerspective: isCustomPerspective,
423
+ rulesUsed: perspectiveRules !== null,
424
+ aggregationType: perspectiveAggregation,
425
+ ruleParseError: typeof ruleParseError !== "undefined" ? ruleParseError : undefined,
426
+ items: filteredTasks.slice(0, limit),
427
+ };
428
+
429
+ try {
430
+ return JSON.stringify(response);
431
+ } catch (jsonError) {
432
+ return JSON.stringify({
433
+ success: false,
434
+ error: "JSON serialization error: " + jsonError.toString(),
435
+ itemCount: filteredTasks.length,
436
+ });
437
+ }
438
+ } catch (error) {
439
+ return JSON.stringify({
440
+ success: false,
441
+ error: error.toString(),
442
+ });
443
+ }
444
+ }
445
+
446
+ (() => {
447
+ // Check for command-line arguments passed via the wrapper
448
+ if (
449
+ typeof perspectiveName !== "undefined" &&
450
+ typeof requestedLimit !== "undefined"
451
+ ) {
452
+ return getPerspectiveViewByName(perspectiveName, requestedLimit);
453
+ }
454
+
455
+ // Check for arguments passed via osascript
456
+ if (typeof argv !== "undefined" && argv.length >= 2) {
457
+ const argPerspectiveName = argv[0];
458
+ const argLimit = parseInt(argv[1]) || 100;
459
+ return getPerspectiveViewByName(argPerspectiveName, argLimit);
460
+ }
461
+
462
+ // Fallback to current window perspective for backwards compatibility
463
+ const window = document.windows[0];
464
+ if (window && window.perspective) {
465
+ return getPerspectiveViewByName(window.perspective.name || "Unknown", 100);
466
+ } else {
467
+ return JSON.stringify({
468
+ success: false,
469
+ error: "No perspective specified and no active window found",
470
+ });
471
+ }
472
+ })();
@@ -0,0 +1,59 @@
1
+ // OmniJS script to list available perspectives in OmniFocus
2
+ (() => {
3
+ try {
4
+ const perspectives = [];
5
+
6
+ // Get all built-in perspectives
7
+ // According to the API: Perspective.BuiltIn has these properties
8
+ const builtInPerspectives = [
9
+ { obj: Perspective.BuiltIn.Inbox, name: 'Inbox' },
10
+ { obj: Perspective.BuiltIn.Projects, name: 'Projects' },
11
+ { obj: Perspective.BuiltIn.Tags, name: 'Tags' },
12
+ { obj: Perspective.BuiltIn.Forecast, name: 'Forecast' },
13
+ { obj: Perspective.BuiltIn.Flagged, name: 'Flagged' },
14
+ { obj: Perspective.BuiltIn.Review, name: 'Review' }
15
+ ];
16
+
17
+ // Add built-in perspectives
18
+ builtInPerspectives.forEach(p => {
19
+ perspectives.push({
20
+ id: 'builtin_' + p.name.toLowerCase(),
21
+ name: p.name,
22
+ type: 'builtin',
23
+ isBuiltIn: true,
24
+ canModify: false
25
+ });
26
+ });
27
+
28
+ // Get all custom perspectives
29
+ // According to the API: Perspective.Custom.all returns all custom perspectives
30
+ try {
31
+ const customPerspectives = Perspective.Custom.all;
32
+ if (customPerspectives && customPerspectives.length > 0) {
33
+ customPerspectives.forEach(p => {
34
+ perspectives.push({
35
+ id: p.identifier || 'custom_' + p.name.toLowerCase().replace(/\s+/g, '_'),
36
+ name: p.name,
37
+ type: 'custom',
38
+ isBuiltIn: false,
39
+ canModify: true
40
+ });
41
+ });
42
+ }
43
+ } catch (e) {
44
+ // Custom perspectives might not be available (Standard edition)
45
+ // This is not a fatal error
46
+ }
47
+
48
+ return JSON.stringify({
49
+ success: true,
50
+ perspectives: perspectives
51
+ });
52
+
53
+ } catch (error) {
54
+ return JSON.stringify({
55
+ success: false,
56
+ error: error.toString()
57
+ });
58
+ }
59
+ })()
@@ -0,0 +1,58 @@
1
+ // OmniJS script to list all tags in OmniFocus
2
+ (() => {
3
+ try {
4
+ const tags = [];
5
+
6
+ // Build a parent name lookup for resolving parentTagID → parentName
7
+ const parentNameMap = {};
8
+ flattenedTags.forEach(tag => {
9
+ parentNameMap[tag.id.primaryKey] = tag.name;
10
+ });
11
+
12
+ flattenedTags.forEach(tag => {
13
+ try {
14
+ const tagId = tag.id.primaryKey;
15
+ const parentTagID = tag.parent ? tag.parent.id.primaryKey : null;
16
+
17
+ // Count remaining (non-completed, non-dropped) tasks for this tag
18
+ let taskCount = 0;
19
+ try {
20
+ taskCount = tag.remainingTasks.length;
21
+ } catch (e) {
22
+ // Fallback: count tasks manually
23
+ try {
24
+ taskCount = tag.tasks.filter(t =>
25
+ t.taskStatus !== Task.Status.Completed &&
26
+ t.taskStatus !== Task.Status.Dropped
27
+ ).length;
28
+ } catch (e2) {
29
+ taskCount = 0;
30
+ }
31
+ }
32
+
33
+ tags.push({
34
+ id: tagId,
35
+ name: tag.name,
36
+ parentTagID: parentTagID,
37
+ parentName: parentTagID ? (parentNameMap[parentTagID] || null) : null,
38
+ active: tag.active,
39
+ allowsNextAction: tag.allowsNextAction,
40
+ taskCount: taskCount
41
+ });
42
+ } catch (tagError) {
43
+ // Skip tags that error during processing
44
+ }
45
+ });
46
+
47
+ return JSON.stringify({
48
+ success: true,
49
+ tags: tags
50
+ });
51
+
52
+ } catch (error) {
53
+ return JSON.stringify({
54
+ success: false,
55
+ error: error.toString()
56
+ });
57
+ }
58
+ })()