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,112 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../../infrastructure/script_runner"
|
|
4
|
+
require_relative "../../result"
|
|
5
|
+
require_relative "../../utils/blank"
|
|
6
|
+
require_relative "../generators/perspective_view"
|
|
7
|
+
require_relative "../params"
|
|
8
|
+
|
|
9
|
+
module OmnifocusMcp
|
|
10
|
+
module Tools
|
|
11
|
+
module Operations
|
|
12
|
+
class GetPerspectiveView
|
|
13
|
+
DEFAULT_LIMIT = 100
|
|
14
|
+
|
|
15
|
+
class << self
|
|
16
|
+
def call(params = nil, script_runner: Infrastructure::ScriptRunner, **kwargs)
|
|
17
|
+
params = merge_params(params, kwargs)
|
|
18
|
+
new(script_runner:).call(params)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def normalize_limit(value) = new.normalize_limit(value)
|
|
22
|
+
|
|
23
|
+
def classify_response(response:, fields:, limit:)
|
|
24
|
+
new.classify_response(response:, fields:, limit:)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def shape_items(items:, fields:, limit:)
|
|
28
|
+
new.shape_items(items:, fields:, limit:)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def project_fields(items:, fields:)
|
|
32
|
+
new.project_fields(items:, fields:)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
private
|
|
36
|
+
|
|
37
|
+
def merge_params(params, kwargs)
|
|
38
|
+
return params || {} if kwargs.empty?
|
|
39
|
+
|
|
40
|
+
base = params.respond_to?(:to_h) ? params.to_h : params || {}
|
|
41
|
+
base.merge(kwargs)
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def initialize(script_runner: Infrastructure::ScriptRunner, generator: Generators::PerspectiveView)
|
|
46
|
+
@script_runner = script_runner
|
|
47
|
+
@generator = generator
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def call(params)
|
|
51
|
+
Params::McpBoundary.coerce(Params::GetPerspectiveViewParams, params).then do |params|
|
|
52
|
+
perspective_name = params.perspective_name.to_s
|
|
53
|
+
return OmnifocusMcp::Result.error("Perspective name is required") if Utils::Blank.blank?(perspective_name)
|
|
54
|
+
|
|
55
|
+
limit = normalize_limit(params.limit)
|
|
56
|
+
fields = params.fields
|
|
57
|
+
args = generator.args(perspective_name:, limit:)
|
|
58
|
+
|
|
59
|
+
script_runner.execute_omnifocus_script(generator.script_path, args:)
|
|
60
|
+
.and_then { |response| classify_response(response:, fields:, limit:) }
|
|
61
|
+
end
|
|
62
|
+
rescue StandardError => e
|
|
63
|
+
OmnifocusMcp.logger.warn("[get_perspective_view] Error: #{e}")
|
|
64
|
+
OmnifocusMcp::Result.error(e.message || "Unknown error occurred")
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def normalize_limit(value)
|
|
68
|
+
return DEFAULT_LIMIT if value.nil?
|
|
69
|
+
return value if value.is_a?(Integer) && value.positive?
|
|
70
|
+
|
|
71
|
+
DEFAULT_LIMIT
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def classify_response(response:, fields:, limit:)
|
|
75
|
+
shape = response.is_a?(Hash) ? response.transform_keys(&:to_sym) : nil
|
|
76
|
+
|
|
77
|
+
case shape
|
|
78
|
+
in { error: String => msg }
|
|
79
|
+
OmnifocusMcp::Result.error(msg)
|
|
80
|
+
in { items: Array => items }
|
|
81
|
+
OmnifocusMcp::Result.ok(shape_items(items:, fields:, limit:))
|
|
82
|
+
in Hash
|
|
83
|
+
OmnifocusMcp::Result.ok([])
|
|
84
|
+
in nil
|
|
85
|
+
OmnifocusMcp::Result.error("Unexpected response from getPerspectiveView.js: #{response.inspect}")
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def shape_items(items:, fields:, limit:)
|
|
90
|
+
items = project_fields(items:, fields:) if fields && !fields.empty?
|
|
91
|
+
items = items.first(limit) if items.length > limit
|
|
92
|
+
items
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def project_fields(items:, fields:)
|
|
96
|
+
string_fields = fields.map(&:to_s)
|
|
97
|
+
items.map do |item|
|
|
98
|
+
next item unless item.is_a?(Hash)
|
|
99
|
+
|
|
100
|
+
string_fields.each_with_object({}) do |field, projected|
|
|
101
|
+
projected[field] = item[field] if item.key?(field)
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
private
|
|
107
|
+
|
|
108
|
+
attr_reader :script_runner, :generator
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
end
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../../infrastructure/script_runner"
|
|
4
|
+
require_relative "../../result"
|
|
5
|
+
require_relative "../generators/list_perspectives"
|
|
6
|
+
require_relative "../params"
|
|
7
|
+
|
|
8
|
+
module OmnifocusMcp
|
|
9
|
+
module Tools
|
|
10
|
+
module Operations
|
|
11
|
+
class ListPerspectives
|
|
12
|
+
class << self
|
|
13
|
+
def call(params = nil, script_runner: Infrastructure::ScriptRunner, **kwargs)
|
|
14
|
+
merge_params(params, kwargs).then { |params| new(script_runner:).call(params) }
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def classify_response(response:, include_built_in:, include_custom:)
|
|
18
|
+
new.classify_response(response:, include_built_in:, include_custom:)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def filter_perspectives(perspectives:, include_built_in:, include_custom:)
|
|
22
|
+
new.filter_perspectives(perspectives:, include_built_in:, include_custom:)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
private
|
|
26
|
+
|
|
27
|
+
def merge_params(params, kwargs)
|
|
28
|
+
return params || {} if kwargs.empty?
|
|
29
|
+
|
|
30
|
+
base = params.respond_to?(:to_h) ? params.to_h : params || {}
|
|
31
|
+
base.merge(kwargs)
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def initialize(script_runner: Infrastructure::ScriptRunner, generator: Generators::ListPerspectives)
|
|
36
|
+
@script_runner = script_runner
|
|
37
|
+
@generator = generator
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def call(params = {})
|
|
41
|
+
params = Params::McpBoundary.coerce(Params::ListPerspectivesParams, params)
|
|
42
|
+
|
|
43
|
+
script_runner.execute_omnifocus_script(generator.script_path)
|
|
44
|
+
.and_then do |response|
|
|
45
|
+
classify_response(
|
|
46
|
+
response:,
|
|
47
|
+
include_built_in: params.include_built_in,
|
|
48
|
+
include_custom: params.include_custom
|
|
49
|
+
)
|
|
50
|
+
end
|
|
51
|
+
rescue StandardError => e
|
|
52
|
+
OmnifocusMcp.logger.warn("[list_perspectives] Error: #{e}")
|
|
53
|
+
OmnifocusMcp::Result.error(e.message || "Unknown error occurred")
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def classify_response(response:, include_built_in:, include_custom:)
|
|
57
|
+
shape = response.is_a?(Hash) ? response.transform_keys(&:to_sym) : nil
|
|
58
|
+
|
|
59
|
+
case shape
|
|
60
|
+
in { error: String => msg }
|
|
61
|
+
OmnifocusMcp::Result.error(msg)
|
|
62
|
+
in { perspectives: Array => perspectives }
|
|
63
|
+
OmnifocusMcp::Result.ok(
|
|
64
|
+
filter_perspectives(perspectives:, include_built_in:, include_custom:)
|
|
65
|
+
)
|
|
66
|
+
in Hash
|
|
67
|
+
OmnifocusMcp::Result.ok([])
|
|
68
|
+
in nil
|
|
69
|
+
OmnifocusMcp::Result.error("Unexpected response from listPerspectives.js: #{response.inspect}")
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def filter_perspectives(perspectives:, include_built_in:, include_custom:)
|
|
74
|
+
perspectives = perspectives.reject { |perspective| perspective["type"] == "builtin" } unless include_built_in
|
|
75
|
+
perspectives = perspectives.reject { |perspective| perspective["type"] == "custom" } unless include_custom
|
|
76
|
+
perspectives
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
private
|
|
80
|
+
|
|
81
|
+
attr_reader :script_runner, :generator
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
end
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../../infrastructure/script_runner"
|
|
4
|
+
require_relative "../../result"
|
|
5
|
+
require_relative "../generators/list_tags"
|
|
6
|
+
require_relative "../params"
|
|
7
|
+
|
|
8
|
+
module OmnifocusMcp
|
|
9
|
+
module Tools
|
|
10
|
+
module Operations
|
|
11
|
+
class ListTags
|
|
12
|
+
class << self
|
|
13
|
+
def call(params = nil, script_runner: Infrastructure::ScriptRunner, **kwargs)
|
|
14
|
+
params = merge_params(params, kwargs)
|
|
15
|
+
new(script_runner:).call(params)
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def classify_response(response:, include_dropped:)
|
|
19
|
+
new.classify_response(response:, include_dropped:)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def filter_tags(tags:, include_dropped:)
|
|
23
|
+
new.filter_tags(tags:, include_dropped:)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
private
|
|
27
|
+
|
|
28
|
+
def merge_params(params, kwargs)
|
|
29
|
+
return params || {} if kwargs.empty?
|
|
30
|
+
|
|
31
|
+
base = params.respond_to?(:to_h) ? params.to_h : params || {}
|
|
32
|
+
base.merge(kwargs)
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def initialize(script_runner: Infrastructure::ScriptRunner, generator: Generators::ListTags)
|
|
37
|
+
@script_runner = script_runner
|
|
38
|
+
@generator = generator
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def call(params = {})
|
|
42
|
+
params = Params::McpBoundary.coerce(Params::ListTagsParams, params)
|
|
43
|
+
|
|
44
|
+
script_runner.execute_omnifocus_script(generator.script_path)
|
|
45
|
+
.and_then do |response|
|
|
46
|
+
classify_response(response:, include_dropped: params.include_dropped)
|
|
47
|
+
end
|
|
48
|
+
rescue StandardError => e
|
|
49
|
+
OmnifocusMcp.logger.warn("[list_tags] Error: #{e}")
|
|
50
|
+
OmnifocusMcp::Result.error(e.message || "Unknown error occurred")
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def classify_response(response:, include_dropped:)
|
|
54
|
+
shape = response.is_a?(Hash) ? response.transform_keys(&:to_sym) : nil
|
|
55
|
+
|
|
56
|
+
case shape
|
|
57
|
+
in { error: String => msg }
|
|
58
|
+
OmnifocusMcp::Result.error(msg)
|
|
59
|
+
in { tags: Array => tags }
|
|
60
|
+
OmnifocusMcp::Result.ok(filter_tags(tags:, include_dropped:))
|
|
61
|
+
in Hash
|
|
62
|
+
OmnifocusMcp::Result.ok([])
|
|
63
|
+
in nil
|
|
64
|
+
OmnifocusMcp::Result.error("Unexpected response from listTags.js: #{response.inspect}")
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def filter_tags(tags:, include_dropped:)
|
|
69
|
+
return tags if include_dropped
|
|
70
|
+
|
|
71
|
+
tags.select { |tag| tag["active"] }
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
private
|
|
75
|
+
|
|
76
|
+
attr_reader :script_runner, :generator
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
end
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../../infrastructure/script_runner"
|
|
4
|
+
require_relative "../../result"
|
|
5
|
+
require_relative "../generators/query_omnifocus"
|
|
6
|
+
require_relative "../params"
|
|
7
|
+
|
|
8
|
+
module OmnifocusMcp
|
|
9
|
+
module Tools
|
|
10
|
+
module Operations
|
|
11
|
+
class QueryOmnifocus
|
|
12
|
+
Match = Data.define(:items, :count)
|
|
13
|
+
|
|
14
|
+
class << self
|
|
15
|
+
def call(params = nil, script_runner: Infrastructure::ScriptRunner, **kwargs)
|
|
16
|
+
merge_params(params, kwargs).then { new(script_runner:).call(it) }
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def classify_response(...) = new.classify_response(...)
|
|
20
|
+
def build_match(...) = new.build_match(...)
|
|
21
|
+
def generate_query_script(...) = Generators::QueryOmnifocus.generate_query_script(...)
|
|
22
|
+
|
|
23
|
+
private
|
|
24
|
+
|
|
25
|
+
def merge_params(params, kwargs)
|
|
26
|
+
return params || {} if kwargs.empty?
|
|
27
|
+
|
|
28
|
+
base = params.respond_to?(:to_h) ? params.to_h : params || {}
|
|
29
|
+
base.merge(kwargs)
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def initialize(script_runner: Infrastructure::ScriptRunner, generator: Generators::QueryOmnifocus)
|
|
34
|
+
@script_runner = script_runner
|
|
35
|
+
@generator = generator
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def call(params)
|
|
39
|
+
params = Params::McpBoundary.coerce(Params::QueryOmnifocusParams, params)
|
|
40
|
+
generator.generate_query_script(params).then do |script|
|
|
41
|
+
script_runner.execute_omnifocus_source(script)
|
|
42
|
+
.and_then { |response| classify_response(response:, summary: params.summary == true) }
|
|
43
|
+
end
|
|
44
|
+
rescue StandardError => e
|
|
45
|
+
OmnifocusMcp.logger.warn("[query_omnifocus] Error: #{e}")
|
|
46
|
+
OmnifocusMcp::Result.error(e.message || "Unknown error occurred")
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def classify_response(response:, summary:)
|
|
50
|
+
shape = response.is_a?(Hash) ? response.transform_keys(&:to_sym) : nil
|
|
51
|
+
|
|
52
|
+
case shape
|
|
53
|
+
in { error: String => msg }
|
|
54
|
+
OmnifocusMcp::Result.error(msg)
|
|
55
|
+
in { items: Array => items, count: Integer => count }
|
|
56
|
+
OmnifocusMcp::Result.ok(build_match(items:, count:, summary: summary))
|
|
57
|
+
in { count: Integer => count }
|
|
58
|
+
OmnifocusMcp::Result.ok(Match.new(items: nil, count: count))
|
|
59
|
+
in Hash
|
|
60
|
+
OmnifocusMcp::Result.ok(Match.new(items: nil, count: nil))
|
|
61
|
+
in nil
|
|
62
|
+
OmnifocusMcp::Result.error("Unexpected response from queryOmnifocus: #{response.inspect}")
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def build_match(items:, count:, summary:) = Match.new(items: summary ? nil : items, count: count)
|
|
67
|
+
|
|
68
|
+
private
|
|
69
|
+
|
|
70
|
+
attr_reader :script_runner, :generator
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
end
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../../infrastructure/script_runner"
|
|
4
|
+
require_relative "../../result"
|
|
5
|
+
require_relative "../generators/query_omnifocus_debug"
|
|
6
|
+
|
|
7
|
+
module OmnifocusMcp
|
|
8
|
+
module Tools
|
|
9
|
+
module Operations
|
|
10
|
+
class QueryOmnifocusDebug
|
|
11
|
+
ENTITIES = %w[task project folder].freeze
|
|
12
|
+
private_constant :ENTITIES
|
|
13
|
+
|
|
14
|
+
class << self
|
|
15
|
+
def call(entity, script_runner: Infrastructure::ScriptRunner)
|
|
16
|
+
new(script_runner:).call(entity)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def generate_debug_script(...) = Generators::QueryOmnifocusDebug.generate_debug_script(...)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def initialize(script_runner: Infrastructure::ScriptRunner, generator: Generators::QueryOmnifocusDebug)
|
|
23
|
+
@script_runner = script_runner
|
|
24
|
+
@generator = generator
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def call(entity)
|
|
28
|
+
normalized = entity.to_s
|
|
29
|
+
return OmnifocusMcp::Result.error(unknown_entity_message(normalized)) unless ENTITIES.include?(normalized)
|
|
30
|
+
|
|
31
|
+
generator.generate_debug_script(normalized).then do |script|
|
|
32
|
+
script_runner.execute_omnifocus_source(script)
|
|
33
|
+
.and_then { |response| classify_response(response) }
|
|
34
|
+
end
|
|
35
|
+
rescue StandardError => e
|
|
36
|
+
OmnifocusMcp.logger.warn("[query_omnifocus_debug] Error: #{e}")
|
|
37
|
+
OmnifocusMcp::Result.error(e.message || "Unknown error in query_omnifocus_debug")
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
private
|
|
41
|
+
|
|
42
|
+
attr_reader :script_runner, :generator
|
|
43
|
+
|
|
44
|
+
def classify_response(response)
|
|
45
|
+
shape = response.is_a?(Hash) ? response.transform_keys(&:to_sym) : nil
|
|
46
|
+
|
|
47
|
+
case shape
|
|
48
|
+
in { error: String => msg }
|
|
49
|
+
OmnifocusMcp::Result.error(msg)
|
|
50
|
+
in Hash
|
|
51
|
+
OmnifocusMcp::Result.ok(response)
|
|
52
|
+
in nil
|
|
53
|
+
OmnifocusMcp::Result.error("Unexpected response from query_omnifocus_debug: #{response.inspect}")
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def unknown_entity_message(entity)
|
|
58
|
+
"Unknown entity: #{entity.inspect}. Must be one of #{ENTITIES.join(", ")}"
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../../infrastructure/script_runner"
|
|
4
|
+
require_relative "../../parsers/apple_script_envelope"
|
|
5
|
+
require_relative "../../result"
|
|
6
|
+
require_relative "../generators/remove_item"
|
|
7
|
+
require_relative "../params"
|
|
8
|
+
|
|
9
|
+
module OmnifocusMcp
|
|
10
|
+
module Tools
|
|
11
|
+
module Operations
|
|
12
|
+
class RemoveItem
|
|
13
|
+
Removed = Data.define(:id, :name)
|
|
14
|
+
|
|
15
|
+
class << self
|
|
16
|
+
def call(params = nil, script_runner: Infrastructure::ScriptRunner, **kwargs)
|
|
17
|
+
merge_params(params, kwargs).then { new(script_runner:).call(it) }
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def generate_apple_script(params = nil, **)
|
|
21
|
+
Generators::RemoveItem.generate_apple_script(params, **)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
private
|
|
25
|
+
|
|
26
|
+
def merge_params(params, kwargs)
|
|
27
|
+
return params || {} if kwargs.empty?
|
|
28
|
+
|
|
29
|
+
base = params.respond_to?(:to_h) ? params.to_h : params || {}
|
|
30
|
+
base.merge(kwargs)
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def initialize(script_runner: Infrastructure::ScriptRunner, generator: Generators::RemoveItem)
|
|
35
|
+
@script_runner = script_runner
|
|
36
|
+
@generator = generator
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def call(params)
|
|
40
|
+
params = Params::McpBoundary.coerce(Params::RemoveItemParams, params)
|
|
41
|
+
generator.generate_apple_script(params).then { |script| run_script(script) }
|
|
42
|
+
rescue StandardError => e
|
|
43
|
+
OmnifocusMcp.logger.warn("[remove_item] Error: #{e}")
|
|
44
|
+
OmnifocusMcp::Result.error(e.message || "Unknown error in remove_item")
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
private
|
|
48
|
+
|
|
49
|
+
attr_reader :script_runner, :generator
|
|
50
|
+
|
|
51
|
+
def run_script(script)
|
|
52
|
+
stdout, stderr, status = script_runner.execute_applescript(script)
|
|
53
|
+
|
|
54
|
+
OmnifocusMcp.logger.warn("[remove_item] AppleScript stderr: #{stderr}") if stderr && !stderr.empty?
|
|
55
|
+
return OmnifocusMcp::Result.error(applescript_run_failure(stderr:, status:)) unless status.success?
|
|
56
|
+
|
|
57
|
+
parse_result(stdout)
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def parse_result(stdout)
|
|
61
|
+
Parsers::AppleScriptEnvelope.parse(stdout:, default_error: "Unknown error in remove_item") do |hash|
|
|
62
|
+
OmnifocusMcp::Result.ok(Removed.new(id: hash["id"], name: hash["name"]))
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def applescript_run_failure(stderr:, status:)
|
|
67
|
+
exit_code = status.respond_to?(:exitstatus) ? status.exitstatus : status
|
|
68
|
+
message = "osascript failed (exit #{exit_code})"
|
|
69
|
+
message += ": #{stderr.strip}" unless stderr.nil? || stderr.empty?
|
|
70
|
+
message
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
end
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../definitions/key_normalizer"
|
|
4
|
+
|
|
5
|
+
module OmnifocusMcp
|
|
6
|
+
module Tools
|
|
7
|
+
module Params
|
|
8
|
+
# Builds {Data.define} param structs from MCP wire args or snake_case Hashes.
|
|
9
|
+
module McpBoundary
|
|
10
|
+
class << self
|
|
11
|
+
# @param klass [Data] struct class whose +members+ list snake_case fields
|
|
12
|
+
# @param args [Hash] raw MCP arguments (camelCase Symbol keys)
|
|
13
|
+
# @param deep [Boolean] recurse into nested Hashes and Arrays of Hashes
|
|
14
|
+
def build(klass, args, deep: false)
|
|
15
|
+
snake = Definitions::KeyNormalizer.snake_keys(args, deep: deep)
|
|
16
|
+
from_snake_hash(klass, snake)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
# @param klass [Data]
|
|
20
|
+
# @param hash [Hash] snake_case Symbol keys (e.g. from tests or resources)
|
|
21
|
+
def from_hash(klass, hash)
|
|
22
|
+
from_snake_hash(klass, hash || {})
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def from_snake_hash(klass, snake)
|
|
26
|
+
klass.new(**klass.members.to_h { |member| [member, snake[member]] })
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# Accept a param struct or a snake_case Hash (e.g. specs, script helpers).
|
|
30
|
+
def coerce(klass, value)
|
|
31
|
+
case value
|
|
32
|
+
when klass then value
|
|
33
|
+
when Hash then klass.from_hash(value)
|
|
34
|
+
else raise ArgumentError, "expected #{klass.name}, got #{value.class}"
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "params/mcp_boundary"
|
|
4
|
+
|
|
5
|
+
module OmnifocusMcp
|
|
6
|
+
module Tools
|
|
7
|
+
# Typed input objects for tool primitives. MCP tools build these via
|
|
8
|
+
# {.from_mcp}; other callers (resources, integration specs) use {.from_hash}.
|
|
9
|
+
module Params
|
|
10
|
+
AddTaskParams = Data.define(
|
|
11
|
+
:name, :note, :due_date, :defer_date, :planned_date,
|
|
12
|
+
:flagged, :estimated_minutes, :tags, :project_name,
|
|
13
|
+
:parent_task_id, :parent_task_name, :hierarchy_level
|
|
14
|
+
) do
|
|
15
|
+
def self.from_mcp(args) = McpBoundary.build(self, args)
|
|
16
|
+
def self.from_hash(hash) = McpBoundary.from_hash(self, hash)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
AddProjectParams = Data.define(
|
|
20
|
+
:name, :note, :due_date, :defer_date, :flagged,
|
|
21
|
+
:estimated_minutes, :tags, :folder_name, :sequential
|
|
22
|
+
) do
|
|
23
|
+
def self.from_mcp(args) = McpBoundary.build(self, args)
|
|
24
|
+
def self.from_hash(hash) = McpBoundary.from_hash(self, hash)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
EditItemParams = Data.define(
|
|
28
|
+
:id, :name, :item_type, :new_name, :new_note,
|
|
29
|
+
:new_due_date, :new_defer_date, :new_planned_date, :new_flagged,
|
|
30
|
+
:new_estimated_minutes, :new_status, :add_tags, :remove_tags,
|
|
31
|
+
:replace_tags, :new_project_name, :new_sequential, :new_folder_name,
|
|
32
|
+
:new_project_status
|
|
33
|
+
) do
|
|
34
|
+
def self.from_mcp(args) = McpBoundary.build(self, args)
|
|
35
|
+
def self.from_hash(hash) = McpBoundary.from_hash(self, hash)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
RemoveItemParams = Data.define(:id, :name, :item_type) do
|
|
39
|
+
def self.from_mcp(args) = McpBoundary.build(self, args)
|
|
40
|
+
def self.from_hash(hash) = McpBoundary.from_hash(self, hash)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
QueryOmnifocusParams = Data.define(
|
|
44
|
+
:entity, :filters, :fields, :limit, :sort_by, :sort_order,
|
|
45
|
+
:include_completed, :format, :summary
|
|
46
|
+
) do
|
|
47
|
+
def self.from_mcp(args) = McpBoundary.build(self, args, deep: true)
|
|
48
|
+
def self.from_hash(hash) = McpBoundary.from_hash(self, hash)
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
BatchAddItemParams = Data.define(
|
|
52
|
+
:type, :name, :note, :due_date, :defer_date, :planned_date,
|
|
53
|
+
:flagged, :estimated_minutes, :tags, :project_name, :parent_task_id,
|
|
54
|
+
:parent_task_name, :temp_id, :parent_temp_id, :hierarchy_level,
|
|
55
|
+
:folder_name, :sequential
|
|
56
|
+
) do
|
|
57
|
+
def self.from_mcp(args) = McpBoundary.build(self, args)
|
|
58
|
+
def self.from_hash(hash) = McpBoundary.from_hash(self, hash)
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
BatchRemoveItemParams = Data.define(:id, :name, :item_type) do
|
|
62
|
+
def self.from_mcp(args) = McpBoundary.build(self, args)
|
|
63
|
+
def self.from_hash(hash) = McpBoundary.from_hash(self, hash)
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
ListPerspectivesParams = Data.define(:include_built_in, :include_custom) do
|
|
67
|
+
def self.from_mcp(args)
|
|
68
|
+
new(
|
|
69
|
+
include_built_in: args.fetch(:includeBuiltIn, true),
|
|
70
|
+
include_custom: args.fetch(:includeCustom, true)
|
|
71
|
+
)
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def self.from_hash(hash)
|
|
75
|
+
new(
|
|
76
|
+
include_built_in: hash.fetch(:include_built_in, true),
|
|
77
|
+
include_custom: hash.fetch(:include_custom, true)
|
|
78
|
+
)
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
ListTagsParams = Data.define(:include_dropped) do
|
|
83
|
+
def self.from_mcp(args) = new(include_dropped: args[:includeDropped] || false)
|
|
84
|
+
def self.from_hash(hash) = new(include_dropped: hash.fetch(:include_dropped, false))
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
GetPerspectiveViewParams = Data.define(:perspective_name, :limit, :fields) do
|
|
88
|
+
def self.from_mcp(args)
|
|
89
|
+
new(
|
|
90
|
+
perspective_name: args[:perspectiveName],
|
|
91
|
+
limit: args[:limit],
|
|
92
|
+
fields: args[:fields]
|
|
93
|
+
)
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def self.from_hash(hash)
|
|
97
|
+
new(
|
|
98
|
+
perspective_name: hash[:perspective_name],
|
|
99
|
+
limit: hash[:limit],
|
|
100
|
+
fields: hash[:fields]
|
|
101
|
+
)
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
end
|