omnifocus_mcp 1.0.1 → 1.0.2

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 (26) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +13 -1
  3. data/README.md +7 -16
  4. data/bin/omnifocus-mcp +0 -1
  5. data/lib/omnifocus_mcp/infrastructure/script_runner.rb +1 -4
  6. data/lib/omnifocus_mcp/tools/database_stats.rb +0 -154
  7. data/lib/omnifocus_mcp/tools/definitions/{add_omnifocus_task_tool.rb → add_omni_focus_task_tool.rb} +3 -3
  8. data/lib/omnifocus_mcp/tools/definitions/date_formatter.rb +2 -4
  9. data/lib/omnifocus_mcp/tools/generators/database_stats.rb +134 -3
  10. data/lib/omnifocus_mcp/tools/generators/edit_item.rb +1 -3
  11. data/lib/omnifocus_mcp/tools/generators/query_omnifocus.rb +22 -20
  12. data/lib/omnifocus_mcp/tools/operations/{add_omnifocus_task.rb → add_omni_focus_task.rb} +1 -1
  13. data/lib/omnifocus_mcp/tools/operations/batch_add_items/bulk_executor.rb +1 -1
  14. data/lib/omnifocus_mcp/tools/operations/batch_add_items/planner.rb +1 -0
  15. data/lib/omnifocus_mcp/tools/operations/batch_add_items.rb +2 -2
  16. data/lib/omnifocus_mcp/tools/operations/batch_remove_items.rb +4 -2
  17. data/lib/omnifocus_mcp/tools/presenters/query_results.rb +2 -0
  18. data/lib/omnifocus_mcp/version.rb +1 -1
  19. data/lib/omnifocus_mcp.rb +6 -7
  20. metadata +5 -9
  21. data/lib/omnifocus_mcp/tools/batch_report.rb +0 -9
  22. data/lib/omnifocus_mcp/tools/generators/.keep +0 -1
  23. data/lib/omnifocus_mcp/tools/query_omnifocus_formatter.rb +0 -9
  24. data/lib/omnifocus_mcp/utils/date_formatting.rb +0 -9
  25. /data/lib/omnifocus_mcp/tools/generators/{add_omnifocus_task.rb → add_omni_focus_task.rb} +0 -0
  26. /data/lib/omnifocus_mcp/tools/messages/{add_omnifocus_task.rb → add_omni_focus_task.rb} +0 -0
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: e41a69180f0d9273204d7c1768eebccad3d62c0378608e2d5f8d7af2a1e0efc9
4
- data.tar.gz: a1ead1aa0c0a8c7080c9701a89507d662ab58d76311445f1c53d90a0446578b1
3
+ metadata.gz: b8c375f2413b7dd77c677c5abed0ccb49b4efebd2ed7e8fc36179f65cd763154
4
+ data.tar.gz: ca70f41c0e96bbb3a5048d1c673a950fe91f252c3b658eb09c99d0462452a35c
5
5
  SHA512:
6
- metadata.gz: d07d104a5f1a0d49cef8668099f785fee962ec0cdcf6cf2a23a6899c3bddba4fb184d04cf45094aeb2500f5a32a4cf783ec751b79ca86e1888f1dcd76a99dea6
7
- data.tar.gz: e99de7a835a14187683b719ac5b876684ebda26cb2f0334ac05c04d20ee5b7d2a404d0f7c9e589de8756d2c9b53803f8635eafb327f55f4d0615bf0048ae1dce
6
+ metadata.gz: e2887652751a36fec3f1fbadfbc9feca9c64a9063317f25a1c13a8f0817236c4f2fa0fdf022b4930d8cce7493a8a0b217c72ed100755b55a44a9733888f122a8
7
+ data.tar.gz: fd235093c277e4bd78d5886512d66489f2524c39c3af875877502bdd7fba3064de3b19287660e7582009eab69a9e7646ca98e0ab3c7b3cab451124f3a4940556
data/CHANGELOG.md CHANGED
@@ -1,6 +1,18 @@
1
1
  # Changelog
2
2
 
3
- ## [Unreleased]
3
+ ## [1.0.2]
4
+
5
+ - Remove `bundler/setup` from the executable so the gem runs correctly when
6
+ installed outside a Bundler-managed environment
7
+ - Refactor database stats generator to eliminate circular logic
8
+ - Remove unused `BatchReport` and `QueryOmnifocusFormatter` classes
9
+ - Reorganise spec suite into per-class files
10
+
11
+ ## [1.0.1] - 2026-06-27
12
+
13
+ - Fix UTF-8/US-ASCII encoding on stdio so MCP clients receive valid JSON-RPC messages
14
+ - Add `--version` / `-v` CLI flags and log server version on startup
15
+ - Omit empty date and numeric filters from query result formatting
4
16
 
5
17
  ## [1.0.0] - 2026-05-31
6
18
 
data/README.md CHANGED
@@ -1,7 +1,7 @@
1
1
  # omnifocus-mcp (Ruby)
2
2
 
3
- `omnifocus-mcp` is a Ruby MCP server that lets LLM clients (Claude Desktop, MCP
4
- Inspector, etc.) work with OmniFocus on macOS over stdio. It exposes tools and
3
+ `omnifocus-mcp` is a Ruby MCP server that lets LLM clients (Claude Code / Desktop,
4
+ Cursor, Zed, etc.) work with OmniFocus on macOS over stdio. It exposes tools and
5
5
  resources for creating, editing, removing, querying, and reporting on OmniFocus
6
6
  tasks, projects, perspectives, and tags.
7
7
 
@@ -60,21 +60,9 @@ This tool was heavily inspired by [OmniFocus MCP Server](https://github.com/them
60
60
  gem install omnifocus-mcp
61
61
  ```
62
62
 
63
- ## Run
64
-
65
- ```sh
66
- omnifocus-mcp
67
- ```
68
-
69
- The server speaks MCP over stdio. Test it with the MCP inspector:
70
-
71
- ```sh
72
- npx @modelcontextprotocol/inspector omnifocus-mcp
73
- ```
74
-
75
63
  ## Configure an MCP Client
76
64
 
77
- After installing the executable, add this server to any MCP client that supports
65
+ After installing the executable, add this config to any MCP client that supports
78
66
  stdio servers:
79
67
 
80
68
  ```json
@@ -93,7 +81,7 @@ stdio servers:
93
81
  This server uses [fast-mcp](https://github.com/yjacquin/fast-mcp) 1.6, which
94
82
  does not currently expose MCP server instructions during client initialization.
95
83
  To give an MCP client better guidance, copy the instructions below into a skill,
96
- rule, your project's `AGENTS.md`, or another client-specific instruction file.
84
+ rule, your project's `AGENTS.md`, or your client-specific instruction file.
97
85
 
98
86
  ```text
99
87
  OmniFocus MCP server for macOS task management.
@@ -145,3 +133,6 @@ If a run is killed mid-flight, you can sweep leftover items with:
145
133
  ```sh
146
134
  bundle exec ruby spec/integration/cleanup.rb
147
135
  ```
136
+
137
+ You should backup your OmniFocus database before using this tool. Refer to the
138
+ warranty information in the LICENSE.
data/bin/omnifocus-mcp CHANGED
@@ -10,6 +10,5 @@ if ARGV.intersect?(VERSION_ARGS)
10
10
  exit 0
11
11
  end
12
12
 
13
- require "bundler/setup"
14
13
  require "omnifocus_mcp"
15
14
  OmnifocusMcp::Cli.run
@@ -24,6 +24,7 @@ module OmnifocusMcp
24
24
 
25
25
  class << self
26
26
  def default = @default ||= new
27
+ def reset! = @default = new
27
28
 
28
29
  def runner = default.runner
29
30
 
@@ -31,10 +32,6 @@ module OmnifocusMcp
31
32
  default.runner = runner
32
33
  end
33
34
 
34
- def reset!
35
- @default = new
36
- end
37
-
38
35
  def execute_jxa(script) = default.execute_jxa(script)
39
36
  def execute_omnifocus_source(source, args: nil) = default.execute_omnifocus_source(source, args: args)
40
37
  def execute_omnifocus_script(script_path, args: nil) = default.execute_omnifocus_script(script_path, args: args)
@@ -1,9 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative "../result"
4
- require_relative "../infrastructure/js_embed"
5
- require_relative "../infrastructure/script_runner"
6
-
7
3
  module OmnifocusMcp
8
4
  module Tools
9
5
  # Lightweight database overview helpers that don't require pulling the
@@ -12,11 +8,7 @@ module OmnifocusMcp
12
8
  # Provides:
13
9
  # * {.get_database_stats} — counts + last-modified timestamp
14
10
  # * {.get_changes_since} — incremental change feed since a timestamp
15
- # rubocop:disable Metrics/ModuleLength
16
11
  module DatabaseStats
17
- # Lightweight database statistics: counts + last-modified timestamp.
18
- #
19
- # @return [OmnifocusMcp::Result] +ok+ carries the stats Hash; +error+ carries a user-facing message.
20
12
  class << self
21
13
  def get_database_stats
22
14
  require_relative "operations/database_stats"
@@ -32,153 +24,7 @@ module OmnifocusMcp
32
24
 
33
25
  Operations::DatabaseStats.get_changes_since(since)
34
26
  end
35
-
36
- STATS_SCRIPT = <<~JS
37
- (() => {
38
- try {
39
- const allTasks = flattenedTasks;
40
- const activeTasks = allTasks.filter(task =>
41
- task.taskStatus !== Task.Status.Completed &&
42
- task.taskStatus !== Task.Status.Dropped
43
- );
44
-
45
- const allProjects = flattenedProjects;
46
- const activeProjects = allProjects.filter(project =>
47
- project.status === Project.Status.Active
48
- );
49
-
50
- const overdueCount = activeTasks.filter(task =>
51
- task.taskStatus === Task.Status.Overdue
52
- ).length;
53
-
54
- const nextActionCount = activeTasks.filter(task =>
55
- task.taskStatus === Task.Status.Next
56
- ).length;
57
-
58
- const flaggedCount = activeTasks.filter(task => task.flagged).length;
59
- const inboxCount = activeTasks.filter(task => task.inInbox).length;
60
-
61
- let lastModified = new Date(0);
62
- allTasks.forEach(task => {
63
- if (task.modificationDate && task.modificationDate > lastModified) {
64
- lastModified = task.modificationDate;
65
- }
66
- });
67
-
68
- return JSON.stringify({
69
- taskCount: allTasks.length,
70
- activeTaskCount: activeTasks.length,
71
- projectCount: allProjects.length,
72
- activeProjectCount: activeProjects.length,
73
- folderCount: flattenedFolders.length,
74
- tagCount: flattenedTags.filter(tag => tag.active).length,
75
- overdueCount: overdueCount,
76
- nextActionCount: nextActionCount,
77
- flaggedCount: flaggedCount,
78
- inboxCount: inboxCount,
79
- lastModified: lastModified.toISOString()
80
- });
81
-
82
- } catch (error) {
83
- return JSON.stringify({
84
- error: "Failed to get database stats: " + error.toString()
85
- });
86
- }
87
- })();
88
- JS
89
-
90
- # rubocop:disable Metrics/MethodLength
91
- def changes_script(since_iso)
92
- escaped_since = Infrastructure::JsEmbed.double_quoted_string(since_iso)
93
-
94
- <<~JS
95
- (() => {
96
- try {
97
- const sinceDate = new Date("#{escaped_since}");
98
-
99
- const allTasks = flattenedTasks;
100
-
101
- const newTasks = allTasks.filter(task =>
102
- task.creationDate && task.creationDate > sinceDate
103
- ).map(task => ({
104
- id: task.id.primaryKey,
105
- name: task.name,
106
- creationDate: task.creationDate.toISOString()
107
- }));
108
-
109
- const updatedTasks = allTasks.filter(task =>
110
- task.modificationDate &&
111
- task.modificationDate > sinceDate &&
112
- task.creationDate &&
113
- task.creationDate <= sinceDate
114
- ).map(task => ({
115
- id: task.id.primaryKey,
116
- name: task.name,
117
- modificationDate: task.modificationDate.toISOString()
118
- }));
119
-
120
- const completedTasks = allTasks.filter(task =>
121
- task.completionDate &&
122
- task.completionDate > sinceDate
123
- ).map(task => ({
124
- id: task.id.primaryKey,
125
- name: task.name,
126
- completionDate: task.completionDate.toISOString()
127
- }));
128
-
129
- const allProjects = flattenedProjects;
130
-
131
- const newProjects = allProjects.filter(project =>
132
- project.creationDate && project.creationDate > sinceDate
133
- ).map(project => ({
134
- id: project.id.primaryKey,
135
- name: project.name,
136
- creationDate: project.creationDate.toISOString()
137
- }));
138
-
139
- const updatedProjects = allProjects.filter(project =>
140
- project.modificationDate &&
141
- project.modificationDate > sinceDate &&
142
- project.creationDate &&
143
- project.creationDate <= sinceDate
144
- ).map(project => ({
145
- id: project.id.primaryKey,
146
- name: project.name,
147
- modificationDate: project.modificationDate.toISOString()
148
- }));
149
-
150
- return JSON.stringify({
151
- newTasks: newTasks,
152
- updatedTasks: updatedTasks,
153
- completedTasks: completedTasks,
154
- newProjects: newProjects,
155
- updatedProjects: updatedProjects
156
- });
157
-
158
- } catch (error) {
159
- return JSON.stringify({
160
- error: "Failed to get changes: " + error.toString()
161
- });
162
- }
163
- })();
164
- JS
165
- end
166
- # rubocop:enable Metrics/MethodLength
167
-
168
- private
169
-
170
- # Collapse a {Infrastructure::ScriptRunner} {Result} into a {Result} over the parsed Hash payload.
171
- def script_payload_result(execution)
172
- execution.and_then do |payload|
173
- if payload.is_a?(Hash) && payload["error"]
174
- Result.error(payload["error"])
175
- else
176
- Result.ok(payload)
177
- end
178
- end
179
- end
180
27
  end
181
28
  end
182
- # rubocop:enable Metrics/ModuleLength
183
29
  end
184
30
  end
@@ -4,14 +4,14 @@ require "fast_mcp"
4
4
 
5
5
  require_relative "mcp_envelope"
6
6
  require_relative "operation_factory"
7
- require_relative "../messages/add_omnifocus_task"
8
- require_relative "../operations/add_omnifocus_task"
7
+ require_relative "../messages/add_omni_focus_task"
8
+ require_relative "../operations/add_omni_focus_task"
9
9
  require_relative "../params"
10
10
 
11
11
  module OmnifocusMcp
12
12
  module Tools
13
13
  module Definitions
14
- # `FastMcp::Tool` for `add_omnifocus_task`.
14
+ # `FastMcp::Tool` for `add_omni_focus_task`.
15
15
  class AddOmniFocusTaskTool < FastMcp::Tool
16
16
  tool_name "add_omnifocus_task"
17
17
  description "Add a new task to OmniFocus"
@@ -24,13 +24,11 @@ module OmnifocusMcp
24
24
  private
25
25
 
26
26
  def us_short_date(date)
27
- # TODO: Fix the formatting. Make Rubocop happy
28
- format("%d/%d/%d", date.month, date.day, date.year)
27
+ format("%<month>d/%<day>d/%<year>d", month: date.month, day: date.day, year: date.year)
29
28
  end
30
29
 
31
30
  def us_compact_date(date)
32
- # TODO: Fix the formatting. Make Rubocop happy
33
- format("%d/%d", date.month, date.day)
31
+ format("%<month>d/%<day>d", month: date.month, day: date.day)
34
32
  end
35
33
 
36
34
  def format_parsed(iso)
@@ -1,14 +1,145 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative "../database_stats"
3
+ require_relative "../../infrastructure/js_embed"
4
4
 
5
5
  module OmnifocusMcp
6
6
  module Tools
7
7
  module Generators
8
8
  class DatabaseStats
9
9
  class << self
10
- def stats_script = Tools::DatabaseStats.singleton_class.const_get(:STATS_SCRIPT)
11
- def changes_script(...) = Tools::DatabaseStats.changes_script(...)
10
+ STATS_SCRIPT = <<~JS
11
+ (() => {
12
+ try {
13
+ const allTasks = flattenedTasks;
14
+ const activeTasks = allTasks.filter(task =>
15
+ task.taskStatus !== Task.Status.Completed &&
16
+ task.taskStatus !== Task.Status.Dropped
17
+ );
18
+
19
+ const allProjects = flattenedProjects;
20
+ const activeProjects = allProjects.filter(project =>
21
+ project.status === Project.Status.Active
22
+ );
23
+
24
+ const overdueCount = activeTasks.filter(task =>
25
+ task.taskStatus === Task.Status.Overdue
26
+ ).length;
27
+
28
+ const nextActionCount = activeTasks.filter(task =>
29
+ task.taskStatus === Task.Status.Next
30
+ ).length;
31
+
32
+ const flaggedCount = activeTasks.filter(task => task.flagged).length;
33
+ const inboxCount = activeTasks.filter(task => task.inInbox).length;
34
+
35
+ let lastModified = new Date(0);
36
+ allTasks.forEach(task => {
37
+ if (task.modificationDate && task.modificationDate > lastModified) {
38
+ lastModified = task.modificationDate;
39
+ }
40
+ });
41
+
42
+ return JSON.stringify({
43
+ taskCount: allTasks.length,
44
+ activeTaskCount: activeTasks.length,
45
+ projectCount: allProjects.length,
46
+ activeProjectCount: activeProjects.length,
47
+ folderCount: flattenedFolders.length,
48
+ tagCount: flattenedTags.filter(tag => tag.active).length,
49
+ overdueCount: overdueCount,
50
+ nextActionCount: nextActionCount,
51
+ flaggedCount: flaggedCount,
52
+ inboxCount: inboxCount,
53
+ lastModified: lastModified.toISOString()
54
+ });
55
+
56
+ } catch (error) {
57
+ return JSON.stringify({
58
+ error: "Failed to get database stats: " + error.toString()
59
+ });
60
+ }
61
+ })();
62
+ JS
63
+
64
+ def stats_script = STATS_SCRIPT
65
+
66
+ # rubocop:disable Metrics/MethodLength
67
+ def changes_script(since_iso)
68
+ escaped_since = Infrastructure::JsEmbed.double_quoted_string(since_iso)
69
+
70
+ <<~JS
71
+ (() => {
72
+ try {
73
+ const sinceDate = new Date("#{escaped_since}");
74
+
75
+ const allTasks = flattenedTasks;
76
+
77
+ const newTasks = allTasks.filter(task =>
78
+ task.creationDate && task.creationDate > sinceDate
79
+ ).map(task => ({
80
+ id: task.id.primaryKey,
81
+ name: task.name,
82
+ creationDate: task.creationDate.toISOString()
83
+ }));
84
+
85
+ const updatedTasks = allTasks.filter(task =>
86
+ task.modificationDate &&
87
+ task.modificationDate > sinceDate &&
88
+ task.creationDate &&
89
+ task.creationDate <= sinceDate
90
+ ).map(task => ({
91
+ id: task.id.primaryKey,
92
+ name: task.name,
93
+ modificationDate: task.modificationDate.toISOString()
94
+ }));
95
+
96
+ const completedTasks = allTasks.filter(task =>
97
+ task.completionDate &&
98
+ task.completionDate > sinceDate
99
+ ).map(task => ({
100
+ id: task.id.primaryKey,
101
+ name: task.name,
102
+ completionDate: task.completionDate.toISOString()
103
+ }));
104
+
105
+ const allProjects = flattenedProjects;
106
+
107
+ const newProjects = allProjects.filter(project =>
108
+ project.creationDate && project.creationDate > sinceDate
109
+ ).map(project => ({
110
+ id: project.id.primaryKey,
111
+ name: project.name,
112
+ creationDate: project.creationDate.toISOString()
113
+ }));
114
+
115
+ const updatedProjects = allProjects.filter(project =>
116
+ project.modificationDate &&
117
+ project.modificationDate > sinceDate &&
118
+ project.creationDate &&
119
+ project.creationDate <= sinceDate
120
+ ).map(project => ({
121
+ id: project.id.primaryKey,
122
+ name: project.name,
123
+ modificationDate: project.modificationDate.toISOString()
124
+ }));
125
+
126
+ return JSON.stringify({
127
+ newTasks: newTasks,
128
+ updatedTasks: updatedTasks,
129
+ completedTasks: completedTasks,
130
+ newProjects: newProjects,
131
+ updatedProjects: updatedProjects
132
+ });
133
+
134
+ } catch (error) {
135
+ return JSON.stringify({
136
+ error: "Failed to get changes: " + error.toString()
137
+ });
138
+ }
139
+ })();
140
+ JS
141
+ end
142
+ # rubocop:enable Metrics/MethodLength
12
143
  end
13
144
  end
14
145
  end
@@ -43,12 +43,10 @@ module OmnifocusMcp
43
43
  params = Params::McpBoundary.coerce(Params::EditItemParams, params)
44
44
  return missing_identifier_error if Utils::Blank.blank?(params.id, params.name)
45
45
 
46
- id = Infrastructure::AppleScript.escape(params.id.to_s)
47
46
  name = Infrastructure::AppleScript.escape(params.name.to_s)
48
47
  item_type = params.item_type.to_s
49
-
48
+ id = Infrastructure::AppleScript.escape(params.id.to_s)
50
49
  date_pre_scripts, date_assignments = collect_date_assignments(params)
51
-
52
50
  [
53
51
  date_pre_scripts.join("\n\n"),
54
52
  Infrastructure::AppleScript.tell_document(document_body(item_type, id, name, params, date_assignments))
@@ -84,25 +84,24 @@ module OmnifocusMcp
84
84
  # Build the full JXA query script for the given params.
85
85
  def generate_query_script(params)
86
86
  params = Params::McpBoundary.coerce(Params::QueryOmnifocusParams, params)
87
+
87
88
  entity = params.entity.to_s
88
- filters = params.filters || {}
89
- fields = params.fields
90
- limit = params.limit
91
- sort_by = params.sort_by
92
- sort_order = params.sort_order
93
- include_completed = params.include_completed == true
94
- summary = params.summary == true
95
89
 
90
+ filters = params.filters || {}
96
91
  filter_conditions = generate_filter_conditions(entity:, filters:)
97
- field_mapping = generate_field_mapping(entity, fields:)
98
- sort_property = resolve_sort_field(sort_by)
99
- sort_logic = sort_property ? generate_sort_logic(sort_property, sort_order:) : ""
92
+
93
+ field_mapping = generate_field_mapping(entity:, fields: params.fields)
94
+
95
+ sort_property = resolve_sort_field(params.sort_by)
96
+ sort_logic = sort_property ? generate_sort_logic(sort_property, sort_order: params.sort_order) : ""
97
+
98
+ limit = params.limit
100
99
  limit_logic = limit.is_a?(Integer) && limit.positive? ? "filtered = filtered.slice(0, #{limit});" : ""
101
100
 
102
101
  build_query_script(
103
102
  entity: entity,
104
- include_completed: include_completed,
105
- summary: summary,
103
+ include_completed: params.include_completed == true,
104
+ summary: params.summary == true,
106
105
  filter_conditions: filter_conditions,
107
106
  field_mapping: field_mapping,
108
107
  sort_logic: sort_logic,
@@ -159,7 +158,7 @@ module OmnifocusMcp
159
158
  # Build the per-item field-projection block. With no `fields` array
160
159
  # (nil or empty), returns the default field set for the entity.
161
160
  # Otherwise builds explicit mappings for each requested field.
162
- def generate_field_mapping(entity, fields: nil)
161
+ def generate_field_mapping(entity:, fields: nil)
163
162
  if fields.nil? || fields.empty?
164
163
  return default_task_mapping if entity == "tasks"
165
164
  return default_project_mapping if entity == "projects"
@@ -233,10 +232,7 @@ module OmnifocusMcp
233
232
 
234
233
  def apply_tag_status_filters(filters:, conditions:, entity:)
235
234
  if entity == "tasks" && filters[:tags] && !filters[:tags].empty?
236
- tag_condition = filters[:tags].map do |tag|
237
- %(item.tags.some(t => t.name === "#{escape_jxa(tag)}"))
238
- end.join(" || ")
239
- conditions << "if (!(#{tag_condition})) return false;"
235
+ conditions << tag_filter_condition(filters[:tags])
240
236
  end
241
237
 
242
238
  return unless filters[:status] && !filters[:status].empty?
@@ -250,6 +246,13 @@ module OmnifocusMcp
250
246
  conditions << "if (!(#{status_condition})) return false;"
251
247
  end
252
248
 
249
+ def tag_filter_condition(tags)
250
+ tag_condition = tags.map do |tag|
251
+ %(item.tags.some(t => t.name === "#{escape_jxa(tag)}"))
252
+ end.join(" || ")
253
+ "if (!(#{tag_condition})) return false;"
254
+ end
255
+
253
256
  def apply_task_date_filters(filters:, conditions:)
254
257
  conditions << "if (item.flagged !== #{filters[:flagged]}) return false;" unless filters[:flagged].nil?
255
258
 
@@ -260,12 +263,11 @@ module OmnifocusMcp
260
263
  push_same_day(conditions:, field: "dueDate", value: filters[:due_on])
261
264
  push_same_day(conditions:, field: "deferDate", value: filters[:defer_on])
262
265
  push_same_day(conditions:, field: "plannedDate", value: filters[:planned_on])
263
-
264
- push_within_past(conditions:, field: "added", value: filters[:added_within])
265
266
  push_same_day(conditions:, field: "added", value: filters[:added_on])
267
+ push_same_day(conditions:, field: "completionDate", value: filters[:completed_on])
266
268
 
269
+ push_within_past(conditions:, field: "added", value: filters[:added_within])
267
270
  push_within_past(conditions:, field: "completionDate", value: filters[:completed_within])
268
- push_same_day(conditions:, field: "completionDate", value: filters[:completed_on])
269
271
  end
270
272
 
271
273
  def apply_task_misc_filters(filters:, conditions:)
@@ -3,7 +3,7 @@
3
3
  require_relative "../../infrastructure/script_runner"
4
4
  require_relative "../../parsers/apple_script_envelope"
5
5
  require_relative "../../result"
6
- require_relative "../generators/add_omnifocus_task"
6
+ require_relative "../generators/add_omni_focus_task"
7
7
  require_relative "../params"
8
8
 
9
9
  module OmnifocusMcp
@@ -4,7 +4,7 @@ require_relative "../../../parsers/apple_script_envelope"
4
4
  require_relative "../../../infrastructure/script_runner"
5
5
  require_relative "../../../utils/blank"
6
6
  require_relative "../../../result"
7
- require_relative "../../generators/add_omnifocus_task"
7
+ require_relative "../../generators/add_omni_focus_task"
8
8
  require_relative "param_builder"
9
9
 
10
10
  module OmnifocusMcp
@@ -20,6 +20,7 @@ module OmnifocusMcp
20
20
  def prepare!
21
21
  mark_cycle_failures(cycle_messages: cycle_detector.detect)
22
22
  mark_unknown_parent_temp_id
23
+ self
23
24
  end
24
25
 
25
26
  # Stable order: by hierarchy_level (nil -> 0), then original index.
@@ -3,7 +3,7 @@
3
3
  require_relative "../../infrastructure/script_runner"
4
4
  require_relative "../../result"
5
5
  require_relative "../params"
6
- require_relative "add_omnifocus_task"
6
+ require_relative "add_omni_focus_task"
7
7
  require_relative "add_project"
8
8
  require_relative "batch_add_items/planner"
9
9
  require_relative "batch_add_items/batch_item"
@@ -33,10 +33,10 @@ module OmnifocusMcp
33
33
  def call(items)
34
34
  batch_items = Array(items).map { |item| coerce_item(item) }
35
35
  .then { |coerced| build_batch_items(coerced) }
36
- planner = Planner.new(batch_items).tap(&:prepare!)
37
36
 
38
37
  return OmnifocusMcp::Result.ok(batch_items.map(&:result)) if try_bulk_add!(batch_items:)
39
38
 
39
+ planner = Planner.new(batch_items).prepare!
40
40
  process_items(ordered: planner.processing_order, planner:)
41
41
  planner.finalize_unresolved!
42
42
 
@@ -32,8 +32,10 @@ module OmnifocusMcp
32
32
 
33
33
  def coerce_item(item)
34
34
  case item
35
- when Params::BatchRemoveItemParams then item
36
- when Hash then Params::BatchRemoveItemParams.from_hash(item)
35
+ when Params::BatchRemoveItemParams
36
+ item
37
+ when Hash
38
+ Params::BatchRemoveItemParams.from_hash(item)
37
39
  else raise ArgumentError, "expected BatchRemoveItemParams or Hash, got #{item.class}"
38
40
  end
39
41
  end
@@ -11,6 +11,7 @@ module OmnifocusMcp
11
11
  # Exposes `format_tasks`, `format_projects`, `format_folders`,
12
12
  # `format_query_results`, `format_filters`. Used by
13
13
  # {Definitions::QueryOmnifocusTool} to build the user-facing text reply.
14
+ # rubocop:disable Metrics
14
15
  module QueryResults
15
16
  class << self
16
17
  def format_query_results(items:, entity:, filters: nil)
@@ -178,6 +179,7 @@ module OmnifocusMcp
178
179
  end
179
180
  end
180
181
  end
182
+ # rubocop:enable Metrics
181
183
  end
182
184
  end
183
185
  end
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module OmnifocusMcp
4
- VERSION = "1.0.1"
4
+ VERSION = "1.0.2"
5
5
  VERSION_ARGS = %w[--version -v version].freeze
6
6
  end
data/lib/omnifocus_mcp.rb CHANGED
@@ -22,7 +22,6 @@ require_relative "omnifocus_mcp/utils/apple_script_envelope"
22
22
  require_relative "omnifocus_mcp/utils/apple_script_helpers"
23
23
  require_relative "omnifocus_mcp/utils/blank"
24
24
  require_relative "omnifocus_mcp/utils/date_filter"
25
- require_relative "omnifocus_mcp/utils/date_formatting"
26
25
  require_relative "omnifocus_mcp/utils/iso_date"
27
26
  require_relative "omnifocus_mcp/utils/script_execution"
28
27
 
@@ -39,13 +38,13 @@ require_relative "omnifocus_mcp/tools/generators/query_omnifocus"
39
38
  require_relative "omnifocus_mcp/tools/generators/query_omnifocus_debug"
40
39
  require_relative "omnifocus_mcp/tools/generators/database_stats"
41
40
  require_relative "omnifocus_mcp/tools/generators/add_project"
42
- require_relative "omnifocus_mcp/tools/generators/add_omnifocus_task"
41
+ require_relative "omnifocus_mcp/tools/generators/add_omni_focus_task"
43
42
  require_relative "omnifocus_mcp/tools/generators/edit_item"
44
43
  require_relative "omnifocus_mcp/tools/generators/remove_item"
45
44
  require_relative "omnifocus_mcp/tools/operations/query_omnifocus"
46
45
  require_relative "omnifocus_mcp/tools/operations/query_omnifocus_debug"
47
46
  require_relative "omnifocus_mcp/tools/operations/database_stats"
48
- require_relative "omnifocus_mcp/tools/operations/add_omnifocus_task"
47
+ require_relative "omnifocus_mcp/tools/operations/add_omni_focus_task"
49
48
  require_relative "omnifocus_mcp/tools/operations/add_project"
50
49
  require_relative "omnifocus_mcp/tools/operations/batch_add_items"
51
50
  require_relative "omnifocus_mcp/tools/operations/batch_remove_items"
@@ -61,16 +60,16 @@ require_relative "omnifocus_mcp/tools/presenters/list_perspectives"
61
60
  require_relative "omnifocus_mcp/tools/presenters/list_tags"
62
61
  require_relative "omnifocus_mcp/tools/presenters/perspective_view"
63
62
  require_relative "omnifocus_mcp/tools/presenters/query_reply"
64
- require_relative "omnifocus_mcp/tools/batch_report"
63
+
65
64
  require_relative "omnifocus_mcp/tools/database_stats"
66
65
  require_relative "omnifocus_mcp/tools/presenters/query_results"
67
- require_relative "omnifocus_mcp/tools/query_omnifocus_formatter"
66
+
68
67
  require_relative "omnifocus_mcp/tools/query_statuses"
69
68
  require_relative "omnifocus_mcp/tools/definitions/date_formatter"
70
69
  require_relative "omnifocus_mcp/tools/definitions/key_normalizer"
71
70
  require_relative "omnifocus_mcp/tools/definitions/mcp_envelope"
72
71
  require_relative "omnifocus_mcp/tools/definitions/operation_factory"
73
- require_relative "omnifocus_mcp/tools/messages/add_omnifocus_task"
72
+ require_relative "omnifocus_mcp/tools/messages/add_omni_focus_task"
74
73
  require_relative "omnifocus_mcp/tools/messages/add_project"
75
74
  require_relative "omnifocus_mcp/tools/messages/batch_remove_items"
76
75
  require_relative "omnifocus_mcp/tools/messages/edit_item"
@@ -78,7 +77,7 @@ require_relative "omnifocus_mcp/tools/messages/list_tools"
78
77
  require_relative "omnifocus_mcp/tools/messages/remove_item"
79
78
 
80
79
  # Tool definitions wrap primitives for MCP exposure.
81
- require_relative "omnifocus_mcp/tools/definitions/add_omnifocus_task_tool"
80
+ require_relative "omnifocus_mcp/tools/definitions/add_omni_focus_task_tool"
82
81
  require_relative "omnifocus_mcp/tools/definitions/add_project_tool"
83
82
  require_relative "omnifocus_mcp/tools/definitions/batch_add_items_tool"
84
83
  require_relative "omnifocus_mcp/tools/definitions/batch_remove_items_tool"
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: omnifocus_mcp
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.1
4
+ version: 1.0.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Henry Maddocks
@@ -60,9 +60,8 @@ files:
60
60
  - lib/omnifocus_mcp/resources/stats_resource.rb
61
61
  - lib/omnifocus_mcp/resources/today_resource.rb
62
62
  - lib/omnifocus_mcp/result.rb
63
- - lib/omnifocus_mcp/tools/batch_report.rb
64
63
  - lib/omnifocus_mcp/tools/database_stats.rb
65
- - lib/omnifocus_mcp/tools/definitions/add_omnifocus_task_tool.rb
64
+ - lib/omnifocus_mcp/tools/definitions/add_omni_focus_task_tool.rb
66
65
  - lib/omnifocus_mcp/tools/definitions/add_project_tool.rb
67
66
  - lib/omnifocus_mcp/tools/definitions/batch_add_items_tool.rb
68
67
  - lib/omnifocus_mcp/tools/definitions/batch_remove_items_tool.rb
@@ -77,8 +76,7 @@ files:
77
76
  - lib/omnifocus_mcp/tools/definitions/query_omnifocus_tool.rb
78
77
  - lib/omnifocus_mcp/tools/definitions/remove_item_tool.rb
79
78
  - lib/omnifocus_mcp/tools/generators.rb
80
- - lib/omnifocus_mcp/tools/generators/.keep
81
- - lib/omnifocus_mcp/tools/generators/add_omnifocus_task.rb
79
+ - lib/omnifocus_mcp/tools/generators/add_omni_focus_task.rb
82
80
  - lib/omnifocus_mcp/tools/generators/add_project.rb
83
81
  - lib/omnifocus_mcp/tools/generators/database_stats.rb
84
82
  - lib/omnifocus_mcp/tools/generators/edit_item.rb
@@ -89,14 +87,14 @@ files:
89
87
  - lib/omnifocus_mcp/tools/generators/query_omnifocus_debug.rb
90
88
  - lib/omnifocus_mcp/tools/generators/remove_item.rb
91
89
  - lib/omnifocus_mcp/tools/messages.rb
92
- - lib/omnifocus_mcp/tools/messages/add_omnifocus_task.rb
90
+ - lib/omnifocus_mcp/tools/messages/add_omni_focus_task.rb
93
91
  - lib/omnifocus_mcp/tools/messages/add_project.rb
94
92
  - lib/omnifocus_mcp/tools/messages/batch_remove_items.rb
95
93
  - lib/omnifocus_mcp/tools/messages/edit_item.rb
96
94
  - lib/omnifocus_mcp/tools/messages/list_tools.rb
97
95
  - lib/omnifocus_mcp/tools/messages/remove_item.rb
98
96
  - lib/omnifocus_mcp/tools/operations.rb
99
- - lib/omnifocus_mcp/tools/operations/add_omnifocus_task.rb
97
+ - lib/omnifocus_mcp/tools/operations/add_omni_focus_task.rb
100
98
  - lib/omnifocus_mcp/tools/operations/add_project.rb
101
99
  - lib/omnifocus_mcp/tools/operations/batch_add_items.rb
102
100
  - lib/omnifocus_mcp/tools/operations/batch_add_items/batch_item.rb
@@ -122,14 +120,12 @@ files:
122
120
  - lib/omnifocus_mcp/tools/presenters/perspective_view.rb
123
121
  - lib/omnifocus_mcp/tools/presenters/query_reply.rb
124
122
  - lib/omnifocus_mcp/tools/presenters/query_results.rb
125
- - lib/omnifocus_mcp/tools/query_omnifocus_formatter.rb
126
123
  - lib/omnifocus_mcp/tools/query_statuses.rb
127
124
  - lib/omnifocus_mcp/utils/apple_script.rb
128
125
  - lib/omnifocus_mcp/utils/apple_script_envelope.rb
129
126
  - lib/omnifocus_mcp/utils/apple_script_helpers.rb
130
127
  - lib/omnifocus_mcp/utils/blank.rb
131
128
  - lib/omnifocus_mcp/utils/date_filter.rb
132
- - lib/omnifocus_mcp/utils/date_formatting.rb
133
129
  - lib/omnifocus_mcp/utils/iso_date.rb
134
130
  - lib/omnifocus_mcp/utils/omnifocus_scripts/getPerspectiveView.js
135
131
  - lib/omnifocus_mcp/utils/omnifocus_scripts/listPerspectives.js
@@ -1,9 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require_relative "presenters/batch_report"
4
-
5
- module OmnifocusMcp
6
- module Tools
7
- BatchReport = Presenters::BatchReport
8
- end
9
- end
@@ -1 +0,0 @@
1
-
@@ -1,9 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require_relative "presenters/query_results"
4
-
5
- module OmnifocusMcp
6
- module Tools
7
- QueryOmnifocusFormatter = Presenters::QueryResults
8
- end
9
- end
@@ -1,9 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require_relative "../infrastructure/apple_script_date_builder"
4
-
5
- module OmnifocusMcp
6
- module Utils
7
- DateFormatting = Infrastructure::AppleScriptDateBuilder
8
- end
9
- end