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,455 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../../infrastructure/apple_script"
|
|
4
|
+
require_relative "../../infrastructure/apple_script_date_builder"
|
|
5
|
+
require_relative "../../parsers/apple_script_envelope"
|
|
6
|
+
require_relative "../../utils/blank"
|
|
7
|
+
require_relative "../../result"
|
|
8
|
+
require_relative "../../infrastructure/script_runner"
|
|
9
|
+
require_relative "../params"
|
|
10
|
+
|
|
11
|
+
module OmnifocusMcp
|
|
12
|
+
module Tools
|
|
13
|
+
module Generators
|
|
14
|
+
# Edit a task or project in OmniFocus.
|
|
15
|
+
#
|
|
16
|
+
# Returns an {OmnifocusMcp::Result} whose +ok+ payload is an {Edited} carrying the
|
|
17
|
+
# item's id, name, and a +changed_properties+ string (comma-separated
|
|
18
|
+
# property names that the script reported as modified).
|
|
19
|
+
class EditItem
|
|
20
|
+
# Task statuses: 'incomplete' | 'completed' | 'dropped' | 'skipped'
|
|
21
|
+
# Project statuses: 'active' | 'completed' | 'dropped' | 'onHold'
|
|
22
|
+
|
|
23
|
+
Edited = Data.define(:id, :name, :changed_properties)
|
|
24
|
+
|
|
25
|
+
DATE_FIELDS = [
|
|
26
|
+
[:new_due_date, "due date"],
|
|
27
|
+
[:new_defer_date, "defer date"],
|
|
28
|
+
[:new_planned_date, "planned date"]
|
|
29
|
+
].freeze
|
|
30
|
+
private_constant :DATE_FIELDS
|
|
31
|
+
|
|
32
|
+
PROJECT_STATUS_MAP = {
|
|
33
|
+
"active" => "active status",
|
|
34
|
+
"completed" => "done status",
|
|
35
|
+
"dropped" => "dropped status"
|
|
36
|
+
}.freeze
|
|
37
|
+
private_constant :PROJECT_STATUS_MAP
|
|
38
|
+
|
|
39
|
+
# Generate pure AppleScript for item editing. Dates are constructed
|
|
40
|
+
# outside the `tell` block then referenced from within.
|
|
41
|
+
class << self
|
|
42
|
+
def generate_apple_script(params)
|
|
43
|
+
params = Params::McpBoundary.coerce(Params::EditItemParams, params)
|
|
44
|
+
return missing_identifier_error if Utils::Blank.blank?(params.id, params.name)
|
|
45
|
+
|
|
46
|
+
id = Infrastructure::AppleScript.escape(params.id.to_s)
|
|
47
|
+
name = Infrastructure::AppleScript.escape(params.name.to_s)
|
|
48
|
+
item_type = params.item_type.to_s
|
|
49
|
+
|
|
50
|
+
date_pre_scripts, date_assignments = collect_date_assignments(params)
|
|
51
|
+
|
|
52
|
+
[
|
|
53
|
+
date_pre_scripts.join("\n\n"),
|
|
54
|
+
Infrastructure::AppleScript.tell_document(document_body(item_type, id, name, params, date_assignments))
|
|
55
|
+
].reject(&:empty?).join("\n\n")
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# Run the generated AppleScript against OmniFocus and parse the JSON result.
|
|
59
|
+
def call(params)
|
|
60
|
+
require_relative "../operations/edit_item"
|
|
61
|
+
|
|
62
|
+
Operations::EditItem.call(params)
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
private
|
|
66
|
+
|
|
67
|
+
def run_script(script)
|
|
68
|
+
stdout, stderr, status = Infrastructure::ScriptRunner.execute_applescript(script)
|
|
69
|
+
|
|
70
|
+
OmnifocusMcp.logger.warn("[edit_item] AppleScript stderr: #{stderr}") if stderr && !stderr.empty?
|
|
71
|
+
return OmnifocusMcp::Result.error(applescript_run_failure(stderr:, status:)) unless status.success?
|
|
72
|
+
|
|
73
|
+
parse_result(stdout)
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def parse_result(stdout)
|
|
77
|
+
Parsers::AppleScriptEnvelope.parse(stdout:, default_error: "Unknown error in edit_item") do |hash|
|
|
78
|
+
OmnifocusMcp::Result.ok(
|
|
79
|
+
Edited.new(
|
|
80
|
+
id: hash["id"],
|
|
81
|
+
name: hash["name"],
|
|
82
|
+
changed_properties: hash["changedProperties"]
|
|
83
|
+
)
|
|
84
|
+
)
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def applescript_run_failure(stderr:, status:)
|
|
89
|
+
exit_code = status.respond_to?(:exitstatus) ? status.exitstatus : status
|
|
90
|
+
message = "osascript failed (exit #{exit_code})"
|
|
91
|
+
message += ": #{stderr.strip}" unless stderr.nil? || stderr.empty?
|
|
92
|
+
message
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def missing_identifier_error
|
|
96
|
+
%(return "{\\"success\\":false,\\"error\\":\\"Either id or name must be provided\\"}")
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
# Walk the three date params, returning [pre_scripts, assignments].
|
|
100
|
+
# `assignments` is a Hash from property name to AppleScript line that
|
|
101
|
+
# assigns the prepared date variable to that property.
|
|
102
|
+
def collect_date_assignments(params)
|
|
103
|
+
pre_scripts = []
|
|
104
|
+
assignments = {}
|
|
105
|
+
|
|
106
|
+
DATE_FIELDS.each do |param_key, property_name|
|
|
107
|
+
parts = Infrastructure::AppleScriptDateBuilder.generate_date_assignment(
|
|
108
|
+
"foundItem", property_name, params.public_send(param_key)
|
|
109
|
+
)
|
|
110
|
+
next if parts.nil?
|
|
111
|
+
|
|
112
|
+
pre_scripts << parts.pre_script unless Utils::Blank.blank?(parts.pre_script)
|
|
113
|
+
assignments[property_name] = parts.assignment_script
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
[pre_scripts, assignments]
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
# The interior of the `tell front document` block.
|
|
120
|
+
def document_body(item_type, id, name, params, date_assignments)
|
|
121
|
+
<<~APPLESCRIPT.chomp
|
|
122
|
+
-- Find the item to edit
|
|
123
|
+
#{Infrastructure::AppleScript.find_item(var: "foundItem", item_type: item_type, id: id, name: name)}
|
|
124
|
+
-- If we found the item, edit it
|
|
125
|
+
if foundItem is not missing value then
|
|
126
|
+
#{Infrastructure::AppleScript.indent(text: item_found_body(item_type, params, date_assignments).chomp, prefix: " ")}
|
|
127
|
+
else
|
|
128
|
+
return "{\\"success\\":false,\\"error\\":\\"Item not found\\"}"
|
|
129
|
+
end if
|
|
130
|
+
APPLESCRIPT
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
# Everything inside the `if foundItem is not missing value then`
|
|
134
|
+
# branch: collect each property update as a string, drop nils,
|
|
135
|
+
# finish with the changed-properties join and success envelope.
|
|
136
|
+
def item_found_body(item_type, params, date_assignments)
|
|
137
|
+
steps = [
|
|
138
|
+
"set itemName to name of foundItem",
|
|
139
|
+
"set itemId to id of foundItem as string",
|
|
140
|
+
"set changedProperties to {}"
|
|
141
|
+
]
|
|
142
|
+
steps.concat(property_update_steps(item_type, params, date_assignments).compact)
|
|
143
|
+
steps << finalize_and_return
|
|
144
|
+
|
|
145
|
+
steps.join("\n\n")
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
def property_update_steps(item_type, params, date_assignments)
|
|
149
|
+
[
|
|
150
|
+
update_string_property("name", params, :new_name),
|
|
151
|
+
update_string_property("note", params, :new_note),
|
|
152
|
+
update_date_step("due date", date_assignments["due date"]),
|
|
153
|
+
update_date_step("defer date", date_assignments["defer date"]),
|
|
154
|
+
update_date_step("planned date", date_assignments["planned date"]),
|
|
155
|
+
update_literal_property("flagged", params, :new_flagged),
|
|
156
|
+
update_literal_property("estimated minutes", params, :new_estimated_minutes),
|
|
157
|
+
*task_or_project_steps(item_type, params)
|
|
158
|
+
]
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
def task_or_project_steps(item_type, params)
|
|
162
|
+
case item_type
|
|
163
|
+
when "task"
|
|
164
|
+
[
|
|
165
|
+
apply_task_status(params),
|
|
166
|
+
apply_tag_operations(params),
|
|
167
|
+
apply_new_project_name(params)
|
|
168
|
+
]
|
|
169
|
+
when "project"
|
|
170
|
+
[
|
|
171
|
+
apply_sequential(params),
|
|
172
|
+
apply_project_status(params),
|
|
173
|
+
apply_new_folder(params)
|
|
174
|
+
]
|
|
175
|
+
else
|
|
176
|
+
[]
|
|
177
|
+
end
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
# `set foo of foundItem to "escaped string"` + changedProperties bump.
|
|
181
|
+
def update_string_property(label, params, key)
|
|
182
|
+
return nil unless param_provided?(params, key)
|
|
183
|
+
|
|
184
|
+
value = Infrastructure::AppleScript.escape(params.public_send(key).to_s)
|
|
185
|
+
<<~APPLESCRIPT.chomp
|
|
186
|
+
-- Update #{label}
|
|
187
|
+
set #{label} of foundItem to "#{value}"
|
|
188
|
+
set end of changedProperties to "#{label}"
|
|
189
|
+
APPLESCRIPT
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
# `set foo of foundItem to <raw value>` + changedProperties bump.
|
|
193
|
+
# For booleans / numbers (no AppleScript quotes).
|
|
194
|
+
def update_literal_property(label, params, key)
|
|
195
|
+
return nil unless param_provided?(params, key)
|
|
196
|
+
|
|
197
|
+
<<~APPLESCRIPT.chomp
|
|
198
|
+
-- Update #{label}
|
|
199
|
+
set #{label} of foundItem to #{params.public_send(key)}
|
|
200
|
+
set end of changedProperties to "#{label}"
|
|
201
|
+
APPLESCRIPT
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
def update_date_step(label, assignment_script)
|
|
205
|
+
return nil unless assignment_script
|
|
206
|
+
|
|
207
|
+
<<~APPLESCRIPT.chomp
|
|
208
|
+
-- Update #{label}
|
|
209
|
+
#{assignment_script}
|
|
210
|
+
set end of changedProperties to "#{label}"
|
|
211
|
+
APPLESCRIPT
|
|
212
|
+
end
|
|
213
|
+
|
|
214
|
+
def finalize_and_return
|
|
215
|
+
<<~APPLESCRIPT.chomp
|
|
216
|
+
-- Prepare the changed properties as a string
|
|
217
|
+
set changedPropsText to ""
|
|
218
|
+
repeat with i from 1 to count of changedProperties
|
|
219
|
+
set changedPropsText to changedPropsText & item i of changedProperties
|
|
220
|
+
if i < count of changedProperties then
|
|
221
|
+
set changedPropsText to changedPropsText & ", "
|
|
222
|
+
end if
|
|
223
|
+
end repeat
|
|
224
|
+
|
|
225
|
+
-- Return success with details
|
|
226
|
+
return "{\\"success\\":true,\\"id\\":\\"" & itemId & "\\",\\"name\\":\\"" & itemName & "\\",\\"changedProperties\\":\\"" & changedPropsText & "\\"}"
|
|
227
|
+
APPLESCRIPT
|
|
228
|
+
end
|
|
229
|
+
|
|
230
|
+
def apply_task_status(params)
|
|
231
|
+
return nil unless param_provided?(params, :new_status)
|
|
232
|
+
|
|
233
|
+
case params.new_status.to_s
|
|
234
|
+
when "completed" then task_status_completed
|
|
235
|
+
when "dropped" then task_status_dropped
|
|
236
|
+
when "skipped" then task_status_skipped
|
|
237
|
+
when "incomplete" then task_status_incomplete
|
|
238
|
+
end
|
|
239
|
+
end
|
|
240
|
+
|
|
241
|
+
def task_status_completed
|
|
242
|
+
<<~APPLESCRIPT.chomp
|
|
243
|
+
-- Mark task as completed (works reliably for all task types including inbox tasks)
|
|
244
|
+
mark complete foundItem
|
|
245
|
+
set end of changedProperties to "status (completed)"
|
|
246
|
+
APPLESCRIPT
|
|
247
|
+
end
|
|
248
|
+
|
|
249
|
+
def task_status_dropped
|
|
250
|
+
<<~APPLESCRIPT.chomp
|
|
251
|
+
-- Mark task as dropped
|
|
252
|
+
mark dropped foundItem
|
|
253
|
+
set end of changedProperties to "status (dropped)"
|
|
254
|
+
APPLESCRIPT
|
|
255
|
+
end
|
|
256
|
+
|
|
257
|
+
def task_status_incomplete
|
|
258
|
+
<<~APPLESCRIPT.chomp
|
|
259
|
+
-- Mark task as incomplete
|
|
260
|
+
mark incomplete foundItem
|
|
261
|
+
set end of changedProperties to "status (incomplete)"
|
|
262
|
+
APPLESCRIPT
|
|
263
|
+
end
|
|
264
|
+
|
|
265
|
+
def task_status_skipped
|
|
266
|
+
<<~APPLESCRIPT.chomp
|
|
267
|
+
-- Skip repeating task: complete it to fire the next repeat, then drop the completed instance
|
|
268
|
+
if repetition rule of foundItem is missing value then
|
|
269
|
+
return "{\\"success\\":false,\\"error\\":\\"Cannot skip a non-repeating task. The task must have a repetition rule.\\"}"
|
|
270
|
+
end if
|
|
271
|
+
|
|
272
|
+
-- Store the ID of the current instance before completing
|
|
273
|
+
set skippedTaskId to id of foundItem as string
|
|
274
|
+
|
|
275
|
+
-- Complete the task to trigger the next repetition
|
|
276
|
+
mark complete foundItem
|
|
277
|
+
|
|
278
|
+
-- Now find and drop the completed instance by its original ID
|
|
279
|
+
try
|
|
280
|
+
set completedTask to first flattened task whose id is skippedTaskId
|
|
281
|
+
set dropped of completedTask to true
|
|
282
|
+
set end of changedProperties to "status (skipped)"
|
|
283
|
+
on error
|
|
284
|
+
-- The completed instance may have moved; still report success since repeat was triggered
|
|
285
|
+
set end of changedProperties to "status (skipped - completed instance not found to drop)"
|
|
286
|
+
end try
|
|
287
|
+
APPLESCRIPT
|
|
288
|
+
end
|
|
289
|
+
|
|
290
|
+
def apply_tag_operations(params)
|
|
291
|
+
return tag_replace_block(params.replace_tags) if non_empty?(params.replace_tags)
|
|
292
|
+
|
|
293
|
+
blocks = [
|
|
294
|
+
non_empty?(params.add_tags) ? tag_add_block(params.add_tags) : nil,
|
|
295
|
+
non_empty?(params.remove_tags) ? tag_remove_block(params.remove_tags) : nil
|
|
296
|
+
].compact
|
|
297
|
+
|
|
298
|
+
blocks.empty? ? nil : blocks.join("\n\n")
|
|
299
|
+
end
|
|
300
|
+
|
|
301
|
+
def non_empty?(value) = value && !value.empty?
|
|
302
|
+
|
|
303
|
+
# rubocop:disable Metrics/MethodLength
|
|
304
|
+
def tag_replace_block(tags)
|
|
305
|
+
tags_list = tags.map { |t| %("#{Infrastructure::AppleScript.escape(t.to_s)}") }.join(", ")
|
|
306
|
+
<<~APPLESCRIPT.chomp
|
|
307
|
+
-- Replace all tags
|
|
308
|
+
set tagNames to {#{tags_list}}
|
|
309
|
+
set existingTags to tags of foundItem
|
|
310
|
+
|
|
311
|
+
-- First clear all existing tags
|
|
312
|
+
repeat with existingTag in existingTags
|
|
313
|
+
remove existingTag from tags of foundItem
|
|
314
|
+
end repeat
|
|
315
|
+
|
|
316
|
+
-- Then add new tags
|
|
317
|
+
repeat with tagName in tagNames
|
|
318
|
+
set tagObj to missing value
|
|
319
|
+
try
|
|
320
|
+
set tagObj to first flattened tag where name = (tagName as string)
|
|
321
|
+
on error
|
|
322
|
+
-- Tag doesn't exist, create it
|
|
323
|
+
set tagObj to make new tag with properties {name:(tagName as string)}
|
|
324
|
+
end try
|
|
325
|
+
if tagObj is not missing value then
|
|
326
|
+
add tagObj to tags of foundItem
|
|
327
|
+
end if
|
|
328
|
+
end repeat
|
|
329
|
+
set end of changedProperties to "tags (replaced)"
|
|
330
|
+
APPLESCRIPT
|
|
331
|
+
end
|
|
332
|
+
# rubocop:enable Metrics/MethodLength
|
|
333
|
+
|
|
334
|
+
def tag_add_block(tags)
|
|
335
|
+
tags_list = tags.map { |t| %("#{Infrastructure::AppleScript.escape(t.to_s)}") }.join(", ")
|
|
336
|
+
<<~APPLESCRIPT.chomp
|
|
337
|
+
-- Add tags
|
|
338
|
+
set tagNames to {#{tags_list}}
|
|
339
|
+
repeat with tagName in tagNames
|
|
340
|
+
set tagObj to missing value
|
|
341
|
+
try
|
|
342
|
+
set tagObj to first flattened tag where name = (tagName as string)
|
|
343
|
+
on error
|
|
344
|
+
-- Tag doesn't exist, create it
|
|
345
|
+
set tagObj to make new tag with properties {name:(tagName as string)}
|
|
346
|
+
end try
|
|
347
|
+
if tagObj is not missing value then
|
|
348
|
+
add tagObj to tags of foundItem
|
|
349
|
+
end if
|
|
350
|
+
end repeat
|
|
351
|
+
set end of changedProperties to "tags (added)"
|
|
352
|
+
APPLESCRIPT
|
|
353
|
+
end
|
|
354
|
+
|
|
355
|
+
def tag_remove_block(tags)
|
|
356
|
+
tags_list = tags.map { |t| %("#{Infrastructure::AppleScript.escape(t.to_s)}") }.join(", ")
|
|
357
|
+
<<~APPLESCRIPT.chomp
|
|
358
|
+
-- Remove tags
|
|
359
|
+
set tagNames to {#{tags_list}}
|
|
360
|
+
repeat with tagName in tagNames
|
|
361
|
+
try
|
|
362
|
+
set tagObj to first flattened tag where name = (tagName as string)
|
|
363
|
+
remove tagObj from tags of foundItem
|
|
364
|
+
end try
|
|
365
|
+
end repeat
|
|
366
|
+
set end of changedProperties to "tags (removed)"
|
|
367
|
+
APPLESCRIPT
|
|
368
|
+
end
|
|
369
|
+
|
|
370
|
+
def apply_new_project_name(params)
|
|
371
|
+
return nil unless param_provided?(params, :new_project_name)
|
|
372
|
+
|
|
373
|
+
project_name = params.new_project_name.to_s
|
|
374
|
+
|
|
375
|
+
if project_name.empty? || project_name.downcase == "inbox"
|
|
376
|
+
move_task_to_inbox_block
|
|
377
|
+
else
|
|
378
|
+
move_task_to_project_block(project_name)
|
|
379
|
+
end
|
|
380
|
+
end
|
|
381
|
+
|
|
382
|
+
def move_task_to_inbox_block
|
|
383
|
+
<<~APPLESCRIPT.chomp
|
|
384
|
+
-- Move task to inbox by clearing its assigned container
|
|
385
|
+
set assigned container of foundItem to missing value
|
|
386
|
+
set end of changedProperties to "project (moved to inbox)"
|
|
387
|
+
APPLESCRIPT
|
|
388
|
+
end
|
|
389
|
+
|
|
390
|
+
def move_task_to_project_block(project_name)
|
|
391
|
+
escaped = Infrastructure::AppleScript.escape(project_name)
|
|
392
|
+
error_json = %({\\"success\\":false,\\"error\\":\\"Project not found: #{escaped}\\"})
|
|
393
|
+
project_lookup = Infrastructure::AppleScript.generate_project_lookup_script(
|
|
394
|
+
raw_project_path: project_name, var_name: "destProject", error_return_json: error_json
|
|
395
|
+
)
|
|
396
|
+
<<~APPLESCRIPT.chomp
|
|
397
|
+
-- Find the destination project (supports folder paths like "Work/My Project")
|
|
398
|
+
#{project_lookup}
|
|
399
|
+
|
|
400
|
+
move foundItem to end of tasks of destProject
|
|
401
|
+
set end of changedProperties to "project (moved to #{escaped})"
|
|
402
|
+
APPLESCRIPT
|
|
403
|
+
end
|
|
404
|
+
|
|
405
|
+
def apply_sequential(params)
|
|
406
|
+
return nil unless param_provided?(params, :new_sequential)
|
|
407
|
+
|
|
408
|
+
<<~APPLESCRIPT.chomp
|
|
409
|
+
-- Update sequential status
|
|
410
|
+
set sequential of foundItem to #{params.new_sequential}
|
|
411
|
+
set end of changedProperties to "sequential"
|
|
412
|
+
APPLESCRIPT
|
|
413
|
+
end
|
|
414
|
+
|
|
415
|
+
def apply_project_status(params)
|
|
416
|
+
return nil unless param_provided?(params, :new_project_status)
|
|
417
|
+
|
|
418
|
+
status_value = PROJECT_STATUS_MAP.fetch(params.new_project_status.to_s, "on hold status")
|
|
419
|
+
|
|
420
|
+
<<~APPLESCRIPT.chomp
|
|
421
|
+
-- Update project status
|
|
422
|
+
set status of foundItem to #{status_value}
|
|
423
|
+
set end of changedProperties to "status"
|
|
424
|
+
APPLESCRIPT
|
|
425
|
+
end
|
|
426
|
+
|
|
427
|
+
def apply_new_folder(params)
|
|
428
|
+
return nil unless param_provided?(params, :new_folder_name)
|
|
429
|
+
return nil if Utils::Blank.blank?(params.new_folder_name)
|
|
430
|
+
|
|
431
|
+
folder_name = params.new_folder_name.to_s
|
|
432
|
+
escaped = Infrastructure::AppleScript.escape(folder_name)
|
|
433
|
+
error_json = %({\\"success\\":false,\\"error\\":\\"Folder not found: #{escaped}\\"})
|
|
434
|
+
folder_lookup = Infrastructure::AppleScript.generate_folder_lookup_script(
|
|
435
|
+
raw_folder_path: folder_name, var_name: "destFolder", error_return_json: error_json
|
|
436
|
+
)
|
|
437
|
+
|
|
438
|
+
<<~APPLESCRIPT.chomp
|
|
439
|
+
-- Find the destination folder
|
|
440
|
+
#{folder_lookup}
|
|
441
|
+
|
|
442
|
+
-- Move project to the folder
|
|
443
|
+
move {foundItem} to end of projects of destFolder
|
|
444
|
+
set end of changedProperties to "folder"
|
|
445
|
+
APPLESCRIPT
|
|
446
|
+
end
|
|
447
|
+
|
|
448
|
+
def param_provided?(params, key)
|
|
449
|
+
!params.public_send(key).nil?
|
|
450
|
+
end
|
|
451
|
+
end
|
|
452
|
+
end
|
|
453
|
+
end
|
|
454
|
+
end
|
|
455
|
+
end
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module OmnifocusMcp
|
|
4
|
+
module Tools
|
|
5
|
+
module Generators
|
|
6
|
+
class PerspectiveView
|
|
7
|
+
class << self
|
|
8
|
+
def script_path = "@getPerspectiveView.js"
|
|
9
|
+
|
|
10
|
+
def args(perspective_name:, limit:)
|
|
11
|
+
[perspective_name.to_s, limit.to_s]
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|