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,169 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../../result"
|
|
4
|
+
require_relative "../../infrastructure/script_runner"
|
|
5
|
+
|
|
6
|
+
module OmnifocusMcp
|
|
7
|
+
module Tools
|
|
8
|
+
module Generators
|
|
9
|
+
# Debug variant of `QueryOmnifocus` that returns raw field information
|
|
10
|
+
# for a single sample item. Useful for understanding what fields are
|
|
11
|
+
# actually exposed by the OmniFocus JS API.
|
|
12
|
+
class QueryOmnifocusDebug
|
|
13
|
+
ENTITIES = %w[task project folder].freeze
|
|
14
|
+
private_constant :ENTITIES
|
|
15
|
+
|
|
16
|
+
# @param entity ['task'|'project'|'folder']
|
|
17
|
+
# @return [OmnifocusMcp::Result] +ok+ carries the parsed JSON Hash from the script
|
|
18
|
+
class << self
|
|
19
|
+
def call(entity)
|
|
20
|
+
require_relative "../operations/query_omnifocus_debug"
|
|
21
|
+
|
|
22
|
+
Operations::QueryOmnifocusDebug.call(entity)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# Build the OmniJS debug script for the given entity.
|
|
26
|
+
# @param entity [String] one of {ENTITIES}
|
|
27
|
+
def generate_debug_script(entity)
|
|
28
|
+
<<~JS
|
|
29
|
+
(() => {
|
|
30
|
+
try {
|
|
31
|
+
let item;
|
|
32
|
+
const entityType = "#{entity}";
|
|
33
|
+
|
|
34
|
+
if (entityType === "task") {
|
|
35
|
+
item = flattenedTasks[0];
|
|
36
|
+
} else if (entityType === "project") {
|
|
37
|
+
item = flattenedProjects[0];
|
|
38
|
+
} else if (entityType === "folder") {
|
|
39
|
+
item = flattenedFolders[0];
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
if (!item) {
|
|
43
|
+
return JSON.stringify({ error: "No items found" });
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const properties = {};
|
|
47
|
+
const skipProps = ['constructor', 'toString', 'valueOf'];
|
|
48
|
+
|
|
49
|
+
for (let prop in item) {
|
|
50
|
+
if (skipProps.includes(prop)) continue;
|
|
51
|
+
|
|
52
|
+
try {
|
|
53
|
+
const value = item[prop];
|
|
54
|
+
const valueType = typeof value;
|
|
55
|
+
|
|
56
|
+
if (value === null) {
|
|
57
|
+
properties[prop] = { type: 'null', value: null };
|
|
58
|
+
} else if (value === undefined) {
|
|
59
|
+
properties[prop] = { type: 'undefined', value: undefined };
|
|
60
|
+
} else if (valueType === 'function') {
|
|
61
|
+
properties[prop] = { type: 'function', value: '[Function]' };
|
|
62
|
+
} else if (value instanceof Date) {
|
|
63
|
+
properties[prop] = { type: 'Date', value: value.toISOString() };
|
|
64
|
+
} else if (Array.isArray(value)) {
|
|
65
|
+
properties[prop] = {
|
|
66
|
+
type: 'Array',
|
|
67
|
+
length: value.length,
|
|
68
|
+
sample: value.length > 0 ? value[0] : null
|
|
69
|
+
};
|
|
70
|
+
} else if (valueType === 'object') {
|
|
71
|
+
if (value.id && value.id.primaryKey) {
|
|
72
|
+
properties[prop] = {
|
|
73
|
+
type: 'OFObject',
|
|
74
|
+
id: value.id.primaryKey,
|
|
75
|
+
name: value.name || null
|
|
76
|
+
};
|
|
77
|
+
} else {
|
|
78
|
+
properties[prop] = { type: 'object', keys: Object.keys(value) };
|
|
79
|
+
}
|
|
80
|
+
} else {
|
|
81
|
+
properties[prop] = { type: valueType, value: value };
|
|
82
|
+
}
|
|
83
|
+
} catch (e) {
|
|
84
|
+
properties[prop] = { type: 'error', error: e.toString() };
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const checkProps = [
|
|
89
|
+
'id', 'name', 'note', 'flagged', 'dueDate', 'deferDate',
|
|
90
|
+
'estimatedMinutes', 'modificationDate', 'creationDate',
|
|
91
|
+
'completionDate', 'taskStatus', 'status', 'tasks', 'projects',
|
|
92
|
+
'containingProject', 'parentFolder', 'parent', 'children'
|
|
93
|
+
];
|
|
94
|
+
|
|
95
|
+
const expectedProps = {};
|
|
96
|
+
checkProps.forEach(prop => {
|
|
97
|
+
try {
|
|
98
|
+
const value = item[prop];
|
|
99
|
+
if (value !== undefined) {
|
|
100
|
+
if (value && value.id && value.id.primaryKey) {
|
|
101
|
+
expectedProps[prop] = {
|
|
102
|
+
exists: true,
|
|
103
|
+
type: 'OFObject',
|
|
104
|
+
id: value.id.primaryKey
|
|
105
|
+
};
|
|
106
|
+
} else if (value instanceof Date) {
|
|
107
|
+
expectedProps[prop] = {
|
|
108
|
+
exists: true,
|
|
109
|
+
type: 'Date',
|
|
110
|
+
value: value.toISOString()
|
|
111
|
+
};
|
|
112
|
+
} else if (Array.isArray(value)) {
|
|
113
|
+
expectedProps[prop] = {
|
|
114
|
+
exists: true,
|
|
115
|
+
type: 'Array',
|
|
116
|
+
length: value.length
|
|
117
|
+
};
|
|
118
|
+
} else {
|
|
119
|
+
expectedProps[prop] = {
|
|
120
|
+
exists: true,
|
|
121
|
+
type: typeof value,
|
|
122
|
+
value: value
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
} else {
|
|
126
|
+
expectedProps[prop] = { exists: false };
|
|
127
|
+
}
|
|
128
|
+
} catch (e) {
|
|
129
|
+
expectedProps[prop] = { exists: false, error: e.toString() };
|
|
130
|
+
}
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
return JSON.stringify({
|
|
134
|
+
entityType: entityType,
|
|
135
|
+
itemName: item.name || 'Unnamed',
|
|
136
|
+
allProperties: properties,
|
|
137
|
+
expectedProperties: expectedProps
|
|
138
|
+
}, null, 2);
|
|
139
|
+
|
|
140
|
+
} catch (error) {
|
|
141
|
+
return JSON.stringify({ error: error.toString() });
|
|
142
|
+
}
|
|
143
|
+
})();
|
|
144
|
+
JS
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
private
|
|
148
|
+
|
|
149
|
+
def classify_response(response)
|
|
150
|
+
shape = response.is_a?(Hash) ? response.transform_keys(&:to_sym) : nil
|
|
151
|
+
|
|
152
|
+
case shape
|
|
153
|
+
in { error: String => msg }
|
|
154
|
+
OmnifocusMcp::Result.error(msg)
|
|
155
|
+
in Hash
|
|
156
|
+
OmnifocusMcp::Result.ok(response)
|
|
157
|
+
in nil
|
|
158
|
+
OmnifocusMcp::Result.error("Unexpected response from query_omnifocus_debug: #{response.inspect}")
|
|
159
|
+
end
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
def unknown_entity_message(entity)
|
|
163
|
+
"Unknown entity: #{entity.inspect}. Must be one of #{ENTITIES.join(", ")}"
|
|
164
|
+
end
|
|
165
|
+
end
|
|
166
|
+
end
|
|
167
|
+
end
|
|
168
|
+
end
|
|
169
|
+
end
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../../infrastructure/apple_script"
|
|
4
|
+
require_relative "../../utils/blank"
|
|
5
|
+
require_relative "../params"
|
|
6
|
+
|
|
7
|
+
module OmnifocusMcp
|
|
8
|
+
module Tools
|
|
9
|
+
module Generators
|
|
10
|
+
class RemoveItem
|
|
11
|
+
class << self
|
|
12
|
+
def generate_apple_script(params = nil, **kwargs)
|
|
13
|
+
merge_params(params, kwargs).then do |params|
|
|
14
|
+
params = Params::McpBoundary.coerce(Params::RemoveItemParams, params)
|
|
15
|
+
return missing_identifier_error if Utils::Blank.blank?(params.id, params.name)
|
|
16
|
+
|
|
17
|
+
id = Infrastructure::AppleScript.escape(params.id.to_s)
|
|
18
|
+
name = Infrastructure::AppleScript.escape(params.name.to_s)
|
|
19
|
+
item_type = params.item_type.to_s
|
|
20
|
+
|
|
21
|
+
Infrastructure::AppleScript.tell_document(document_body(item_type:, id:, name:))
|
|
22
|
+
end
|
|
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
|
+
|
|
34
|
+
def missing_identifier_error
|
|
35
|
+
%(return "{\\"success\\":false,\\"error\\":\\"Either id or name must be provided\\"}")
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def document_body(item_type:, id:, name:)
|
|
39
|
+
<<~APPLESCRIPT.chomp
|
|
40
|
+
-- Find the item to remove
|
|
41
|
+
#{Infrastructure::AppleScript.find_item(var: "foundItem", item_type: item_type, id: id, name: name)}
|
|
42
|
+
-- If we found the item, remove it
|
|
43
|
+
if foundItem is not missing value then
|
|
44
|
+
set itemName to name of foundItem
|
|
45
|
+
set itemId to id of foundItem as string
|
|
46
|
+
|
|
47
|
+
-- Delete the item
|
|
48
|
+
delete foundItem
|
|
49
|
+
|
|
50
|
+
-- Return success
|
|
51
|
+
return "{\\"success\\":true,\\"id\\":\\"" & itemId & "\\",\\"name\\":\\"" & itemName & "\\"}"
|
|
52
|
+
else
|
|
53
|
+
return "{\\"success\\":false,\\"error\\":\\"Item not found\\"}"
|
|
54
|
+
end if
|
|
55
|
+
APPLESCRIPT
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../definitions/date_formatter"
|
|
4
|
+
|
|
5
|
+
module OmnifocusMcp
|
|
6
|
+
module Tools
|
|
7
|
+
module Messages
|
|
8
|
+
module AddOmniFocusTask
|
|
9
|
+
class << self
|
|
10
|
+
def success(args, result)
|
|
11
|
+
location_text = location_for(args, result.placement)
|
|
12
|
+
tag_text = tag_text_for(args[:tags])
|
|
13
|
+
due_text = if args[:dueDate]
|
|
14
|
+
" due on #{Definitions::DateFormatter.format_date(args[:dueDate],
|
|
15
|
+
style: :locale)}"
|
|
16
|
+
else
|
|
17
|
+
""
|
|
18
|
+
end
|
|
19
|
+
warning = placement_warning(args, result.placement)
|
|
20
|
+
|
|
21
|
+
%(✅ Task "#{args[:name]}" created successfully #{location_text}#{due_text}#{tag_text}.#{warning})
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def failure(error) = "Failed to create task: #{error}"
|
|
25
|
+
|
|
26
|
+
private
|
|
27
|
+
|
|
28
|
+
def location_for(args, placement)
|
|
29
|
+
case placement
|
|
30
|
+
when "parent" then "under the parent task"
|
|
31
|
+
when "project" then args[:projectName] ? %(in project "#{args[:projectName]}") : "in a project"
|
|
32
|
+
else "in your inbox"
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def tag_text_for(tags)
|
|
37
|
+
tags && !tags.empty? ? " with tags: #{tags.join(", ")}" : ""
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def placement_warning(args, placement)
|
|
41
|
+
return "" if placement.nil? || placement == "parent"
|
|
42
|
+
|
|
43
|
+
parent_requested = args[:parentTaskId] || args[:parentTaskName]
|
|
44
|
+
return "" unless parent_requested
|
|
45
|
+
|
|
46
|
+
location = placement == "project" ? "in project" : "in inbox"
|
|
47
|
+
"\n⚠️ Parent not found; task created #{location}."
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../definitions/date_formatter"
|
|
4
|
+
|
|
5
|
+
module OmnifocusMcp
|
|
6
|
+
module Tools
|
|
7
|
+
module Messages
|
|
8
|
+
module AddProject
|
|
9
|
+
class << self
|
|
10
|
+
def success(args)
|
|
11
|
+
location = args[:folderName] ? %(in folder "#{args[:folderName]}") : "at the root level"
|
|
12
|
+
tags = args[:tags] && !args[:tags].empty? ? " with tags: #{args[:tags].join(", ")}" : ""
|
|
13
|
+
due = if args[:dueDate]
|
|
14
|
+
" due on #{Definitions::DateFormatter.format_date(args[:dueDate], style: :locale)}"
|
|
15
|
+
else
|
|
16
|
+
""
|
|
17
|
+
end
|
|
18
|
+
sequential = args[:sequential] ? " (sequential)" : " (parallel)"
|
|
19
|
+
|
|
20
|
+
%(✅ Project "#{args[:name]}" created successfully #{location}#{due}#{tags}#{sequential}.)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def failure(error) = "Failed to create project: #{error}"
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module OmnifocusMcp
|
|
4
|
+
module Tools
|
|
5
|
+
module Messages
|
|
6
|
+
module EditItem
|
|
7
|
+
class << self
|
|
8
|
+
def missing_identifier = "Either id or name must be provided to edit an item."
|
|
9
|
+
|
|
10
|
+
def success(args, edited)
|
|
11
|
+
label = args[:itemType] == "task" ? "Task" : "Project"
|
|
12
|
+
changed = edited.changed_properties ? " (#{edited.changed_properties})" : ""
|
|
13
|
+
%(✅ #{label} "#{edited.name}" updated successfully#{changed}.)
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def failure(args, error)
|
|
17
|
+
base = "Failed to update #{args[:itemType]}"
|
|
18
|
+
return base unless error
|
|
19
|
+
|
|
20
|
+
if error.include?("Item not found")
|
|
21
|
+
not_found_message(args)
|
|
22
|
+
else
|
|
23
|
+
"#{base}: #{error}"
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
private
|
|
28
|
+
|
|
29
|
+
def not_found_message(args)
|
|
30
|
+
msg = "#{args[:itemType].capitalize} not found"
|
|
31
|
+
msg += %( with ID "#{args[:id]}") if args[:id]
|
|
32
|
+
msg += %(#{args[:id] ? " or" : " with"} name "#{args[:name]}") if args[:name]
|
|
33
|
+
"#{msg}."
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module OmnifocusMcp
|
|
4
|
+
module Tools
|
|
5
|
+
module Messages
|
|
6
|
+
module ListTools
|
|
7
|
+
class << self
|
|
8
|
+
def list_tags_failure(error) = "Failed to list tags: #{error}"
|
|
9
|
+
def list_perspectives_failure(error) = "Failed to list perspectives: #{error}"
|
|
10
|
+
def perspective_view_failure(error) = "Failed to get perspective view: #{error}"
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module OmnifocusMcp
|
|
4
|
+
module Tools
|
|
5
|
+
module Messages
|
|
6
|
+
module RemoveItem
|
|
7
|
+
class << self
|
|
8
|
+
def missing_identifier = "Either id or name must be provided to remove an item."
|
|
9
|
+
|
|
10
|
+
def invalid_item_type(item_type)
|
|
11
|
+
"Invalid item type: #{item_type}. Must be either 'task' or 'project'."
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def success(args, removed)
|
|
15
|
+
label = args[:itemType] == "task" ? "Task" : "Project"
|
|
16
|
+
%(✅ #{label} "#{removed.name}" removed successfully.)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def failure(args, error)
|
|
20
|
+
base = "Failed to remove #{args[:itemType]}"
|
|
21
|
+
return base unless error
|
|
22
|
+
|
|
23
|
+
if error.include?("Item not found")
|
|
24
|
+
not_found_message(args)
|
|
25
|
+
else
|
|
26
|
+
"#{base}: #{error}"
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
private
|
|
31
|
+
|
|
32
|
+
def not_found_message(args)
|
|
33
|
+
msg = "#{args[:itemType].capitalize} not found"
|
|
34
|
+
msg += %( with ID "#{args[:id]}") if args[:id]
|
|
35
|
+
msg += %(#{args[:id] ? " or" : " with"} name "#{args[:name]}") if args[:name]
|
|
36
|
+
"#{msg}."
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
@@ -0,0 +1,74 @@
|
|
|
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/add_omnifocus_task"
|
|
7
|
+
require_relative "../params"
|
|
8
|
+
|
|
9
|
+
module OmnifocusMcp
|
|
10
|
+
module Tools
|
|
11
|
+
module Operations
|
|
12
|
+
class AddOmniFocusTask
|
|
13
|
+
Created = Generators::AddOmniFocusTask::Created
|
|
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(...) = Generators::AddOmniFocusTask.generate_apple_script(...)
|
|
21
|
+
def generate_bulk_apple_script(...) = Generators::AddOmniFocusTask.generate_bulk_apple_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::AddOmniFocusTask)
|
|
34
|
+
@script_runner = script_runner
|
|
35
|
+
@generator = generator
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def call(params)
|
|
39
|
+
params = Params::McpBoundary.coerce(Params::AddTaskParams, params)
|
|
40
|
+
generator.generate_apple_script(params).then { |script| run_script(script) }
|
|
41
|
+
rescue StandardError => e
|
|
42
|
+
OmnifocusMcp.logger.warn("[add_omnifocus_task] Error: #{e}")
|
|
43
|
+
OmnifocusMcp::Result.error(e.message || "Unknown error in add_omnifocus_task")
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
private
|
|
47
|
+
|
|
48
|
+
attr_reader :script_runner, :generator
|
|
49
|
+
|
|
50
|
+
def run_script(script)
|
|
51
|
+
stdout, stderr, status = script_runner.execute_applescript(script)
|
|
52
|
+
|
|
53
|
+
OmnifocusMcp.logger.warn("[add_omnifocus_task] AppleScript stderr: #{stderr}") if stderr && !stderr.empty?
|
|
54
|
+
return OmnifocusMcp::Result.error(applescript_run_failure(stderr:, status:)) unless status.success?
|
|
55
|
+
|
|
56
|
+
parse_result(stdout)
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def parse_result(stdout)
|
|
60
|
+
Parsers::AppleScriptEnvelope.parse(stdout:, default_error: "Unknown error in add_omnifocus_task") do |hash|
|
|
61
|
+
OmnifocusMcp::Result.ok(Created.new(task_id: hash["taskId"], placement: hash["placement"]))
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def applescript_run_failure(stderr:, status:)
|
|
66
|
+
exit_code = status.respond_to?(:exitstatus) ? status.exitstatus : status
|
|
67
|
+
message = "osascript failed (exit #{exit_code})"
|
|
68
|
+
message += ": #{stderr.strip}" unless stderr.nil? || stderr.empty?
|
|
69
|
+
message
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
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/add_project"
|
|
7
|
+
require_relative "../params"
|
|
8
|
+
|
|
9
|
+
module OmnifocusMcp
|
|
10
|
+
module Tools
|
|
11
|
+
module Operations
|
|
12
|
+
class AddProject
|
|
13
|
+
Created = Data.define(:project_id)
|
|
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::AddProject.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::AddProject)
|
|
35
|
+
@script_runner = script_runner
|
|
36
|
+
@generator = generator
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def call(params)
|
|
40
|
+
params = Params::McpBoundary.coerce(Params::AddProjectParams, params)
|
|
41
|
+
generator.generate_apple_script(params).then { |script| run_script(script) }
|
|
42
|
+
rescue StandardError => e
|
|
43
|
+
OmnifocusMcp.logger.warn("[add_project] Error: #{e}")
|
|
44
|
+
OmnifocusMcp::Result.error(e.message || "Unknown error in add_project")
|
|
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("[add_project] 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 add_project") do |hash|
|
|
62
|
+
OmnifocusMcp::Result.ok(Created.new(project_id: hash["projectId"]))
|
|
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,38 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../../../result"
|
|
4
|
+
|
|
5
|
+
module OmnifocusMcp
|
|
6
|
+
module Tools
|
|
7
|
+
module Operations
|
|
8
|
+
class BatchAddItems
|
|
9
|
+
# In-flight bookkeeping for one item in a batch. The original payload
|
|
10
|
+
# and its position in the input array are read-only; status and result
|
|
11
|
+
# are mutated as the batch processes.
|
|
12
|
+
class BatchItem
|
|
13
|
+
attr_reader :payload, :index
|
|
14
|
+
attr_accessor :status, :result
|
|
15
|
+
|
|
16
|
+
def initialize(payload:, index:)
|
|
17
|
+
@payload = payload
|
|
18
|
+
@index = index
|
|
19
|
+
@status = :pending
|
|
20
|
+
@result = nil
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def pending? = @status == :pending
|
|
24
|
+
|
|
25
|
+
def fail!(message)
|
|
26
|
+
@status = :failed
|
|
27
|
+
@result = OmnifocusMcp::Result.error(message)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def succeed!(value)
|
|
31
|
+
@status = :succeeded
|
|
32
|
+
@result = OmnifocusMcp::Result.ok(value)
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|