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.
- checksums.yaml +7 -0
- data/AGENTS.md +15 -0
- data/CHANGELOG.md +7 -0
- data/CODE_OF_CONDUCT.md +16 -0
- data/LICENSE.txt +21 -0
- data/README.md +147 -0
- data/Rakefile +12 -0
- data/bin/omnifocus-mcp +7 -0
- data/lib/omnifocus_mcp/config.rb +18 -0
- data/lib/omnifocus_mcp/infrastructure/.keep +1 -0
- data/lib/omnifocus_mcp/infrastructure/apple_script.rb +263 -0
- data/lib/omnifocus_mcp/infrastructure/apple_script_date_builder.rb +65 -0
- data/lib/omnifocus_mcp/infrastructure/js_embed.rb +39 -0
- data/lib/omnifocus_mcp/infrastructure/script_runner.rb +254 -0
- data/lib/omnifocus_mcp/infrastructure.rb +6 -0
- data/lib/omnifocus_mcp/json_rpc_compat.rb +75 -0
- data/lib/omnifocus_mcp/logger.rb +34 -0
- data/lib/omnifocus_mcp/mcp.rb +74 -0
- data/lib/omnifocus_mcp/parsers/.keep +1 -0
- data/lib/omnifocus_mcp/parsers/apple_script_envelope.rb +44 -0
- data/lib/omnifocus_mcp/parsers.rb +6 -0
- data/lib/omnifocus_mcp/resources/base.rb +87 -0
- data/lib/omnifocus_mcp/resources/flagged_resource.rb +31 -0
- data/lib/omnifocus_mcp/resources/inbox_resource.rb +31 -0
- data/lib/omnifocus_mcp/resources/perspective_resource.rb +28 -0
- data/lib/omnifocus_mcp/resources/project_resource.rb +37 -0
- data/lib/omnifocus_mcp/resources/stats_resource.rb +22 -0
- data/lib/omnifocus_mcp/resources/today_resource.rb +37 -0
- data/lib/omnifocus_mcp/result.rb +108 -0
- data/lib/omnifocus_mcp/tools/batch_report.rb +9 -0
- data/lib/omnifocus_mcp/tools/database_stats.rb +184 -0
- data/lib/omnifocus_mcp/tools/definitions/add_omnifocus_task_tool.rb +61 -0
- data/lib/omnifocus_mcp/tools/definitions/add_project_tool.rb +54 -0
- data/lib/omnifocus_mcp/tools/definitions/batch_add_items_tool.rb +105 -0
- data/lib/omnifocus_mcp/tools/definitions/batch_remove_items_tool.rb +68 -0
- data/lib/omnifocus_mcp/tools/definitions/date_formatter.rb +45 -0
- data/lib/omnifocus_mcp/tools/definitions/edit_item_tool.rb +87 -0
- data/lib/omnifocus_mcp/tools/definitions/get_perspective_view_tool.rb +57 -0
- data/lib/omnifocus_mcp/tools/definitions/key_normalizer.rb +30 -0
- data/lib/omnifocus_mcp/tools/definitions/list_perspectives_tool.rb +47 -0
- data/lib/omnifocus_mcp/tools/definitions/list_tags_tool.rb +42 -0
- data/lib/omnifocus_mcp/tools/definitions/mcp_envelope.rb +31 -0
- data/lib/omnifocus_mcp/tools/definitions/operation_factory.rb +33 -0
- data/lib/omnifocus_mcp/tools/definitions/query_omnifocus_tool.rb +187 -0
- data/lib/omnifocus_mcp/tools/definitions/remove_item_tool.rb +55 -0
- data/lib/omnifocus_mcp/tools/generators/.keep +1 -0
- data/lib/omnifocus_mcp/tools/generators/add_omnifocus_task.rb +348 -0
- data/lib/omnifocus_mcp/tools/generators/add_project.rb +141 -0
- data/lib/omnifocus_mcp/tools/generators/database_stats.rb +16 -0
- data/lib/omnifocus_mcp/tools/generators/edit_item.rb +455 -0
- data/lib/omnifocus_mcp/tools/generators/list_perspectives.rb +13 -0
- data/lib/omnifocus_mcp/tools/generators/list_tags.rb +13 -0
- data/lib/omnifocus_mcp/tools/generators/perspective_view.rb +17 -0
- data/lib/omnifocus_mcp/tools/generators/query_omnifocus.rb +571 -0
- data/lib/omnifocus_mcp/tools/generators/query_omnifocus_debug.rb +169 -0
- data/lib/omnifocus_mcp/tools/generators/remove_item.rb +61 -0
- data/lib/omnifocus_mcp/tools/generators.rb +8 -0
- data/lib/omnifocus_mcp/tools/messages/add_omnifocus_task.rb +53 -0
- data/lib/omnifocus_mcp/tools/messages/add_project.rb +28 -0
- data/lib/omnifocus_mcp/tools/messages/batch_remove_items.rb +13 -0
- data/lib/omnifocus_mcp/tools/messages/edit_item.rb +39 -0
- data/lib/omnifocus_mcp/tools/messages/list_tools.rb +15 -0
- data/lib/omnifocus_mcp/tools/messages/remove_item.rb +42 -0
- data/lib/omnifocus_mcp/tools/messages.rb +8 -0
- data/lib/omnifocus_mcp/tools/operations/add_omnifocus_task.rb +74 -0
- data/lib/omnifocus_mcp/tools/operations/add_project.rb +75 -0
- data/lib/omnifocus_mcp/tools/operations/batch_add_items/batch_item.rb +38 -0
- data/lib/omnifocus_mcp/tools/operations/batch_add_items/bulk_executor.rb +94 -0
- data/lib/omnifocus_mcp/tools/operations/batch_add_items/cycle_detector.rb +74 -0
- data/lib/omnifocus_mcp/tools/operations/batch_add_items/param_builder.rb +47 -0
- data/lib/omnifocus_mcp/tools/operations/batch_add_items/planner.rb +111 -0
- data/lib/omnifocus_mcp/tools/operations/batch_add_items.rb +149 -0
- data/lib/omnifocus_mcp/tools/operations/batch_remove_items.rb +49 -0
- data/lib/omnifocus_mcp/tools/operations/database_stats.rb +52 -0
- data/lib/omnifocus_mcp/tools/operations/edit_item.rb +79 -0
- data/lib/omnifocus_mcp/tools/operations/get_perspective_view.rb +112 -0
- data/lib/omnifocus_mcp/tools/operations/list_perspectives.rb +85 -0
- data/lib/omnifocus_mcp/tools/operations/list_tags.rb +80 -0
- data/lib/omnifocus_mcp/tools/operations/query_omnifocus.rb +74 -0
- data/lib/omnifocus_mcp/tools/operations/query_omnifocus_debug.rb +63 -0
- data/lib/omnifocus_mcp/tools/operations/remove_item.rb +75 -0
- data/lib/omnifocus_mcp/tools/operations.rb +8 -0
- data/lib/omnifocus_mcp/tools/params/mcp_boundary.rb +41 -0
- data/lib/omnifocus_mcp/tools/params.rb +106 -0
- data/lib/omnifocus_mcp/tools/presenters/batch_report.rb +55 -0
- data/lib/omnifocus_mcp/tools/presenters/list_perspectives.rb +33 -0
- data/lib/omnifocus_mcp/tools/presenters/list_tags.rb +49 -0
- data/lib/omnifocus_mcp/tools/presenters/perspective_view.rb +81 -0
- data/lib/omnifocus_mcp/tools/presenters/query_reply.rb +52 -0
- data/lib/omnifocus_mcp/tools/presenters/query_results.rb +183 -0
- data/lib/omnifocus_mcp/tools/presenters.rb +8 -0
- data/lib/omnifocus_mcp/tools/query_omnifocus_formatter.rb +9 -0
- data/lib/omnifocus_mcp/tools/query_statuses.rb +22 -0
- data/lib/omnifocus_mcp/utils/apple_script.rb +9 -0
- data/lib/omnifocus_mcp/utils/apple_script_envelope.rb +9 -0
- data/lib/omnifocus_mcp/utils/apple_script_helpers.rb +9 -0
- data/lib/omnifocus_mcp/utils/blank.rb +26 -0
- data/lib/omnifocus_mcp/utils/date_filter.rb +76 -0
- data/lib/omnifocus_mcp/utils/date_formatting.rb +9 -0
- data/lib/omnifocus_mcp/utils/iso_date.rb +27 -0
- data/lib/omnifocus_mcp/utils/omnifocus_scripts/getPerspectiveView.js +472 -0
- data/lib/omnifocus_mcp/utils/omnifocus_scripts/listPerspectives.js +59 -0
- data/lib/omnifocus_mcp/utils/omnifocus_scripts/listTags.js +58 -0
- data/lib/omnifocus_mcp/utils/omnifocus_scripts/omnifocusDump.js +223 -0
- data/lib/omnifocus_mcp/utils/script_execution.rb +9 -0
- data/lib/omnifocus_mcp/version.rb +5 -0
- data/lib/omnifocus_mcp.rb +102 -0
- 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
|
+
})()
|