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,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,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ module OmnifocusMcp
4
+ module Tools
5
+ module Generators
6
+ class ListPerspectives
7
+ class << self
8
+ def script_path = "@listPerspectives.js"
9
+ end
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ module OmnifocusMcp
4
+ module Tools
5
+ module Generators
6
+ class ListTags
7
+ class << self
8
+ def script_path = "@listTags.js"
9
+ end
10
+ end
11
+ end
12
+ end
13
+ 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