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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +13 -1
- data/README.md +7 -16
- data/bin/omnifocus-mcp +0 -1
- data/lib/omnifocus_mcp/infrastructure/script_runner.rb +1 -4
- data/lib/omnifocus_mcp/tools/database_stats.rb +0 -154
- data/lib/omnifocus_mcp/tools/definitions/{add_omnifocus_task_tool.rb → add_omni_focus_task_tool.rb} +3 -3
- data/lib/omnifocus_mcp/tools/definitions/date_formatter.rb +2 -4
- data/lib/omnifocus_mcp/tools/generators/database_stats.rb +134 -3
- data/lib/omnifocus_mcp/tools/generators/edit_item.rb +1 -3
- data/lib/omnifocus_mcp/tools/generators/query_omnifocus.rb +22 -20
- data/lib/omnifocus_mcp/tools/operations/{add_omnifocus_task.rb → add_omni_focus_task.rb} +1 -1
- data/lib/omnifocus_mcp/tools/operations/batch_add_items/bulk_executor.rb +1 -1
- data/lib/omnifocus_mcp/tools/operations/batch_add_items/planner.rb +1 -0
- data/lib/omnifocus_mcp/tools/operations/batch_add_items.rb +2 -2
- data/lib/omnifocus_mcp/tools/operations/batch_remove_items.rb +4 -2
- data/lib/omnifocus_mcp/tools/presenters/query_results.rb +2 -0
- data/lib/omnifocus_mcp/version.rb +1 -1
- data/lib/omnifocus_mcp.rb +6 -7
- metadata +5 -9
- data/lib/omnifocus_mcp/tools/batch_report.rb +0 -9
- data/lib/omnifocus_mcp/tools/generators/.keep +0 -1
- data/lib/omnifocus_mcp/tools/query_omnifocus_formatter.rb +0 -9
- data/lib/omnifocus_mcp/utils/date_formatting.rb +0 -9
- /data/lib/omnifocus_mcp/tools/generators/{add_omnifocus_task.rb → add_omni_focus_task.rb} +0 -0
- /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:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: b8c375f2413b7dd77c677c5abed0ccb49b4efebd2ed7e8fc36179f65cd763154
|
|
4
|
+
data.tar.gz: ca70f41c0e96bbb3a5048d1c673a950fe91f252c3b658eb09c99d0462452a35c
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: e2887652751a36fec3f1fbadfbc9feca9c64a9063317f25a1c13a8f0817236c4f2fa0fdf022b4930d8cce7493a8a0b217c72ed100755b55a44a9733888f122a8
|
|
7
|
+
data.tar.gz: fd235093c277e4bd78d5886512d66489f2524c39c3af875877502bdd7fba3064de3b19287660e7582009eab69a9e7646ca98e0ab3c7b3cab451124f3a4940556
|
data/CHANGELOG.md
CHANGED
|
@@ -1,6 +1,18 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
-
## [
|
|
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,
|
|
4
|
-
|
|
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
|
|
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
|
|
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
|
@@ -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
|
data/lib/omnifocus_mcp/tools/definitions/{add_omnifocus_task_tool.rb → add_omni_focus_task_tool.rb}
RENAMED
|
@@ -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/
|
|
8
|
-
require_relative "../operations/
|
|
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 `
|
|
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
|
-
|
|
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
|
-
|
|
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 "
|
|
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
|
-
|
|
11
|
-
|
|
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
|
-
|
|
98
|
-
|
|
99
|
-
|
|
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
|
|
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
|
-
|
|
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/
|
|
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/
|
|
7
|
+
require_relative "../../generators/add_omni_focus_task"
|
|
8
8
|
require_relative "param_builder"
|
|
9
9
|
|
|
10
10
|
module OmnifocusMcp
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
require_relative "../../infrastructure/script_runner"
|
|
4
4
|
require_relative "../../result"
|
|
5
5
|
require_relative "../params"
|
|
6
|
-
require_relative "
|
|
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
|
|
36
|
-
|
|
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
|
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/
|
|
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/
|
|
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
|
-
|
|
63
|
+
|
|
65
64
|
require_relative "omnifocus_mcp/tools/database_stats"
|
|
66
65
|
require_relative "omnifocus_mcp/tools/presenters/query_results"
|
|
67
|
-
|
|
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/
|
|
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/
|
|
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.
|
|
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/
|
|
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
|
|
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/
|
|
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/
|
|
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 +0,0 @@
|
|
|
1
|
-
|
|
File without changes
|
|
File without changes
|