mcp-omnifocus 0.1.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.
- package/LICENSE +21 -0
- package/README.md +61 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +276 -0
- package/dist/providers/applescript.d.ts +19 -0
- package/dist/providers/applescript.js +204 -0
- package/dist/providers/url-scheme.d.ts +20 -0
- package/dist/providers/url-scheme.js +178 -0
- package/dist/types.d.ts +55 -0
- package/dist/types.js +1 -0
- package/dist/validation.d.ts +68 -0
- package/dist/validation.js +49 -0
- package/dist/version-detector.d.ts +2 -0
- package/dist/version-detector.js +47 -0
- package/package.json +53 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 mcp-omnifocus contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
# MCP OmniFocus
|
|
2
|
+
|
|
3
|
+
MCP server for OmniFocus with auto-detection of Pro/Standard version.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- **Auto-detection**: Automatically detects OmniFocus Pro or Standard
|
|
8
|
+
- **Full Pro support**: AppleScript for read/write with sync
|
|
9
|
+
- **Standard fallback**: SQLite read + URL scheme for create
|
|
10
|
+
|
|
11
|
+
### Capabilities by Version
|
|
12
|
+
|
|
13
|
+
| Feature | Pro (AppleScript) | Standard |
|
|
14
|
+
|---------|-------------------|----------|
|
|
15
|
+
| Read tasks | ✓ | ✓ (SQLite) |
|
|
16
|
+
| Create task | ✓ | ✓ (URL scheme, syncs) |
|
|
17
|
+
| Update task | ✓ | ⚠️ (SQLite, no sync) |
|
|
18
|
+
| Complete task | ✓ | ⚠️ (SQLite, no sync) |
|
|
19
|
+
| Get projects | ✓ | ✓ (SQLite) |
|
|
20
|
+
|
|
21
|
+
**⚠️ Standard SQLite write**: Changes don't sync until OmniFocus restart.
|
|
22
|
+
|
|
23
|
+
## Installation
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
cd ~/Projects/mcp-omnifocus
|
|
27
|
+
npm install
|
|
28
|
+
npm run build
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
## Claude Code Configuration
|
|
32
|
+
|
|
33
|
+
Add to `~/.claude/claude_desktop_config.json`:
|
|
34
|
+
|
|
35
|
+
```json
|
|
36
|
+
{
|
|
37
|
+
"mcpServers": {
|
|
38
|
+
"omnifocus": {
|
|
39
|
+
"command": "node",
|
|
40
|
+
"args": ["/Users/YOUR_USERNAME/Projects/mcp-omnifocus/dist/index.js"]
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
## Tools
|
|
47
|
+
|
|
48
|
+
### omnifocus_get_tasks
|
|
49
|
+
Get tasks filtered by flagged, due today, or all.
|
|
50
|
+
|
|
51
|
+
### omnifocus_create_task
|
|
52
|
+
Create a new task with name, note, project, flagged, dueDate.
|
|
53
|
+
|
|
54
|
+
### omnifocus_update_task
|
|
55
|
+
Update existing task (Pro: syncs, Standard: SQLite only).
|
|
56
|
+
|
|
57
|
+
### omnifocus_complete_task
|
|
58
|
+
Mark task as complete (Pro: syncs, Standard: SQLite only).
|
|
59
|
+
|
|
60
|
+
### omnifocus_get_projects
|
|
61
|
+
Get list of active projects.
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1,276 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
3
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
4
|
+
import { CallToolRequestSchema, ListToolsRequestSchema, } from "@modelcontextprotocol/sdk/types.js";
|
|
5
|
+
import { createRequire } from "module";
|
|
6
|
+
import { ZodError } from "zod";
|
|
7
|
+
const require = createRequire(import.meta.url);
|
|
8
|
+
const { version: PACKAGE_VERSION } = require("../package.json");
|
|
9
|
+
import { detectVersion } from "./version-detector.js";
|
|
10
|
+
import { AppleScriptProvider } from "./providers/applescript.js";
|
|
11
|
+
import { UrlSchemeProvider } from "./providers/url-scheme.js";
|
|
12
|
+
import { CreateTaskInputSchema, UpdateTaskInputSchema, CompleteTaskInputSchema, GetTasksInputSchema, SetConfigInputSchema, } from "./validation.js";
|
|
13
|
+
function sanitizeErrorMessage(error) {
|
|
14
|
+
if (error instanceof ZodError) {
|
|
15
|
+
return error.errors.map(e => `${e.path.join(".")}: ${e.message}`).join("; ");
|
|
16
|
+
}
|
|
17
|
+
if (error instanceof Error) {
|
|
18
|
+
const msg = error.message;
|
|
19
|
+
// remove file paths and sensitive info
|
|
20
|
+
return msg
|
|
21
|
+
.replace(/\/Users\/[^/\s]+/g, "/Users/***")
|
|
22
|
+
.replace(/\/home\/[^/\s]+/g, "/home/***")
|
|
23
|
+
.replace(/at\s+.+:\d+:\d+/g, "")
|
|
24
|
+
.trim();
|
|
25
|
+
}
|
|
26
|
+
return "An unexpected error occurred";
|
|
27
|
+
}
|
|
28
|
+
let provider;
|
|
29
|
+
const server = new Server({
|
|
30
|
+
name: "mcp-omnifocus",
|
|
31
|
+
version: PACKAGE_VERSION,
|
|
32
|
+
}, {
|
|
33
|
+
capabilities: {
|
|
34
|
+
tools: {},
|
|
35
|
+
},
|
|
36
|
+
});
|
|
37
|
+
server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
38
|
+
const version = provider.version;
|
|
39
|
+
const writeWarning = version === "standard"
|
|
40
|
+
? " (Standard version: changes via SQLite won't sync until OmniFocus restart)"
|
|
41
|
+
: "";
|
|
42
|
+
return {
|
|
43
|
+
tools: [
|
|
44
|
+
{
|
|
45
|
+
name: "omnifocus_get_tasks",
|
|
46
|
+
description: `Get tasks from OmniFocus. Filter by flagged, due today, or all. Detected version: ${version}`,
|
|
47
|
+
inputSchema: {
|
|
48
|
+
type: "object",
|
|
49
|
+
properties: {
|
|
50
|
+
filter: {
|
|
51
|
+
type: "string",
|
|
52
|
+
enum: ["flagged", "due_today", "all"],
|
|
53
|
+
description: "Filter tasks: flagged, due_today, or all (default: flagged + due today)"
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
name: "omnifocus_create_task",
|
|
60
|
+
description: `Create a new task in OmniFocus. Detected version: ${version}`,
|
|
61
|
+
inputSchema: {
|
|
62
|
+
type: "object",
|
|
63
|
+
properties: {
|
|
64
|
+
name: {
|
|
65
|
+
type: "string",
|
|
66
|
+
description: "Task name (required)"
|
|
67
|
+
},
|
|
68
|
+
note: {
|
|
69
|
+
type: "string",
|
|
70
|
+
description: "Task note/description"
|
|
71
|
+
},
|
|
72
|
+
project: {
|
|
73
|
+
type: "string",
|
|
74
|
+
description: "Project name to add task to (optional, defaults to inbox)"
|
|
75
|
+
},
|
|
76
|
+
flagged: {
|
|
77
|
+
type: "boolean",
|
|
78
|
+
description: "Mark task as flagged"
|
|
79
|
+
},
|
|
80
|
+
dueDate: {
|
|
81
|
+
type: "string",
|
|
82
|
+
description: "Due date in ISO format (YYYY-MM-DD)"
|
|
83
|
+
}
|
|
84
|
+
},
|
|
85
|
+
required: ["name"]
|
|
86
|
+
}
|
|
87
|
+
},
|
|
88
|
+
{
|
|
89
|
+
name: "omnifocus_update_task",
|
|
90
|
+
description: `Update an existing task in OmniFocus.${writeWarning}`,
|
|
91
|
+
inputSchema: {
|
|
92
|
+
type: "object",
|
|
93
|
+
properties: {
|
|
94
|
+
taskId: {
|
|
95
|
+
type: "string",
|
|
96
|
+
description: "Task ID (required)"
|
|
97
|
+
},
|
|
98
|
+
name: {
|
|
99
|
+
type: "string",
|
|
100
|
+
description: "New task name"
|
|
101
|
+
},
|
|
102
|
+
note: {
|
|
103
|
+
type: "string",
|
|
104
|
+
description: "New task note"
|
|
105
|
+
},
|
|
106
|
+
flagged: {
|
|
107
|
+
type: "boolean",
|
|
108
|
+
description: "Set flagged status"
|
|
109
|
+
},
|
|
110
|
+
dueDate: {
|
|
111
|
+
type: "string",
|
|
112
|
+
description: "New due date in ISO format"
|
|
113
|
+
}
|
|
114
|
+
},
|
|
115
|
+
required: ["taskId"]
|
|
116
|
+
}
|
|
117
|
+
},
|
|
118
|
+
{
|
|
119
|
+
name: "omnifocus_complete_task",
|
|
120
|
+
description: `Mark a task as complete.${writeWarning}`,
|
|
121
|
+
inputSchema: {
|
|
122
|
+
type: "object",
|
|
123
|
+
properties: {
|
|
124
|
+
taskId: {
|
|
125
|
+
type: "string",
|
|
126
|
+
description: "Task ID to complete (required)"
|
|
127
|
+
}
|
|
128
|
+
},
|
|
129
|
+
required: ["taskId"]
|
|
130
|
+
}
|
|
131
|
+
},
|
|
132
|
+
{
|
|
133
|
+
name: "omnifocus_get_projects",
|
|
134
|
+
description: `Get list of active projects from OmniFocus. Detected version: ${version}`,
|
|
135
|
+
inputSchema: {
|
|
136
|
+
type: "object",
|
|
137
|
+
properties: {}
|
|
138
|
+
}
|
|
139
|
+
},
|
|
140
|
+
{
|
|
141
|
+
name: "omnifocus_get_config",
|
|
142
|
+
description: "Get current configuration settings",
|
|
143
|
+
inputSchema: {
|
|
144
|
+
type: "object",
|
|
145
|
+
properties: {}
|
|
146
|
+
}
|
|
147
|
+
},
|
|
148
|
+
{
|
|
149
|
+
name: "omnifocus_set_config",
|
|
150
|
+
description: "Update configuration settings. For Standard version: directSqlAccess controls whether to use direct SQLite access for update/complete operations (faster but requires OmniFocus restart to sync)",
|
|
151
|
+
inputSchema: {
|
|
152
|
+
type: "object",
|
|
153
|
+
properties: {
|
|
154
|
+
directSqlAccess: {
|
|
155
|
+
type: "boolean",
|
|
156
|
+
description: "Enable direct SQLite access for write operations (Standard version only)"
|
|
157
|
+
},
|
|
158
|
+
taskLimit: {
|
|
159
|
+
type: "number",
|
|
160
|
+
description: "Maximum number of tasks to return from getTasks (default: 500, max: 10000)"
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
]
|
|
166
|
+
};
|
|
167
|
+
});
|
|
168
|
+
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
169
|
+
const { name, arguments: args } = request.params;
|
|
170
|
+
try {
|
|
171
|
+
switch (name) {
|
|
172
|
+
case "omnifocus_get_tasks": {
|
|
173
|
+
const input = GetTasksInputSchema.parse(args ?? {});
|
|
174
|
+
const tasks = await provider.getTasks(input.filter);
|
|
175
|
+
return {
|
|
176
|
+
content: [{
|
|
177
|
+
type: "text",
|
|
178
|
+
text: JSON.stringify({ version: provider.version, tasks }, null, 2)
|
|
179
|
+
}]
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
case "omnifocus_create_task": {
|
|
183
|
+
const input = CreateTaskInputSchema.parse(args ?? {});
|
|
184
|
+
const result = await provider.createTask(input);
|
|
185
|
+
return {
|
|
186
|
+
content: [{
|
|
187
|
+
type: "text",
|
|
188
|
+
text: JSON.stringify({ version: provider.version, ...result }, null, 2)
|
|
189
|
+
}]
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
case "omnifocus_update_task": {
|
|
193
|
+
const input = UpdateTaskInputSchema.parse(args ?? {});
|
|
194
|
+
const result = await provider.updateTask(input);
|
|
195
|
+
return {
|
|
196
|
+
content: [{
|
|
197
|
+
type: "text",
|
|
198
|
+
text: JSON.stringify({ version: provider.version, ...result }, null, 2)
|
|
199
|
+
}]
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
case "omnifocus_complete_task": {
|
|
203
|
+
const input = CompleteTaskInputSchema.parse(args ?? {});
|
|
204
|
+
const result = await provider.completeTask(input.taskId);
|
|
205
|
+
return {
|
|
206
|
+
content: [{
|
|
207
|
+
type: "text",
|
|
208
|
+
text: JSON.stringify({ version: provider.version, ...result }, null, 2)
|
|
209
|
+
}]
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
case "omnifocus_get_projects": {
|
|
213
|
+
const projects = await provider.getProjects();
|
|
214
|
+
return {
|
|
215
|
+
content: [{
|
|
216
|
+
type: "text",
|
|
217
|
+
text: JSON.stringify({ version: provider.version, projects }, null, 2)
|
|
218
|
+
}]
|
|
219
|
+
};
|
|
220
|
+
}
|
|
221
|
+
case "omnifocus_get_config": {
|
|
222
|
+
return {
|
|
223
|
+
content: [{
|
|
224
|
+
type: "text",
|
|
225
|
+
text: JSON.stringify({
|
|
226
|
+
version: provider.version,
|
|
227
|
+
config: provider.config
|
|
228
|
+
}, null, 2)
|
|
229
|
+
}]
|
|
230
|
+
};
|
|
231
|
+
}
|
|
232
|
+
case "omnifocus_set_config": {
|
|
233
|
+
const input = SetConfigInputSchema.parse(args ?? {});
|
|
234
|
+
provider.setConfig(input);
|
|
235
|
+
return {
|
|
236
|
+
content: [{
|
|
237
|
+
type: "text",
|
|
238
|
+
text: JSON.stringify({
|
|
239
|
+
success: true,
|
|
240
|
+
version: provider.version,
|
|
241
|
+
config: provider.config
|
|
242
|
+
}, null, 2)
|
|
243
|
+
}]
|
|
244
|
+
};
|
|
245
|
+
}
|
|
246
|
+
default:
|
|
247
|
+
throw new Error(`Unknown tool: ${name}`);
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
catch (error) {
|
|
251
|
+
console.error("[mcp-omnifocus] Error:", error);
|
|
252
|
+
const message = sanitizeErrorMessage(error);
|
|
253
|
+
return {
|
|
254
|
+
content: [{
|
|
255
|
+
type: "text",
|
|
256
|
+
text: JSON.stringify({ error: message }, null, 2)
|
|
257
|
+
}],
|
|
258
|
+
isError: true
|
|
259
|
+
};
|
|
260
|
+
}
|
|
261
|
+
});
|
|
262
|
+
async function main() {
|
|
263
|
+
console.error("[mcp-omnifocus] Starting...");
|
|
264
|
+
const version = await detectVersion();
|
|
265
|
+
console.error(`[mcp-omnifocus] Detected OmniFocus version: ${version}`);
|
|
266
|
+
provider = version === "pro"
|
|
267
|
+
? new AppleScriptProvider()
|
|
268
|
+
: new UrlSchemeProvider();
|
|
269
|
+
const transport = new StdioServerTransport();
|
|
270
|
+
await server.connect(transport);
|
|
271
|
+
console.error("[mcp-omnifocus] Server connected");
|
|
272
|
+
}
|
|
273
|
+
main().catch((error) => {
|
|
274
|
+
console.error("[mcp-omnifocus] Fatal error:", error);
|
|
275
|
+
process.exit(1);
|
|
276
|
+
});
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { OmniFocusProvider, OmniFocusTask, CreateTaskInput, UpdateTaskInput, OmniFocusConfig } from "../types.js";
|
|
2
|
+
export declare function escapeAppleScriptString(str: string): string;
|
|
3
|
+
export declare class AppleScriptProvider implements OmniFocusProvider {
|
|
4
|
+
version: "pro";
|
|
5
|
+
config: OmniFocusConfig;
|
|
6
|
+
setConfig(newConfig: Partial<OmniFocusConfig>): void;
|
|
7
|
+
getTasks(filter?: "flagged" | "due_today" | "all"): Promise<OmniFocusTask[]>;
|
|
8
|
+
createTask(input: CreateTaskInput): Promise<{
|
|
9
|
+
success: boolean;
|
|
10
|
+
taskId?: string;
|
|
11
|
+
}>;
|
|
12
|
+
updateTask(input: UpdateTaskInput): Promise<{
|
|
13
|
+
success: boolean;
|
|
14
|
+
}>;
|
|
15
|
+
completeTask(taskId: string): Promise<{
|
|
16
|
+
success: boolean;
|
|
17
|
+
}>;
|
|
18
|
+
getProjects(): Promise<string[]>;
|
|
19
|
+
}
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
import { spawn } from "child_process";
|
|
2
|
+
import { DEFAULT_TASK_LIMIT } from "../types.js";
|
|
3
|
+
async function runAppleScript(script) {
|
|
4
|
+
return new Promise((resolve, reject) => {
|
|
5
|
+
const child = spawn("osascript", ["-"], {
|
|
6
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
7
|
+
});
|
|
8
|
+
let stdout = "";
|
|
9
|
+
let stderr = "";
|
|
10
|
+
child.stdout.on("data", (data) => {
|
|
11
|
+
stdout += data.toString();
|
|
12
|
+
});
|
|
13
|
+
child.stderr.on("data", (data) => {
|
|
14
|
+
stderr += data.toString();
|
|
15
|
+
});
|
|
16
|
+
child.on("close", (code) => {
|
|
17
|
+
if (code === 0) {
|
|
18
|
+
resolve(stdout.trim());
|
|
19
|
+
}
|
|
20
|
+
else {
|
|
21
|
+
reject(new Error(`AppleScript failed: ${stderr.trim() || "Unknown error"}`));
|
|
22
|
+
}
|
|
23
|
+
});
|
|
24
|
+
child.on("error", (err) => {
|
|
25
|
+
reject(err);
|
|
26
|
+
});
|
|
27
|
+
child.stdin.write(script);
|
|
28
|
+
child.stdin.end();
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
export function escapeAppleScriptString(str) {
|
|
32
|
+
return str
|
|
33
|
+
.replace(/\\/g, "\\\\")
|
|
34
|
+
.replace(/"/g, '\\"')
|
|
35
|
+
.replace(/\r/g, "\\r")
|
|
36
|
+
.replace(/\n/g, "\\n")
|
|
37
|
+
.replace(/\t/g, "\\t");
|
|
38
|
+
}
|
|
39
|
+
export class AppleScriptProvider {
|
|
40
|
+
version = "pro";
|
|
41
|
+
config = { directSqlAccess: false, taskLimit: DEFAULT_TASK_LIMIT };
|
|
42
|
+
setConfig(newConfig) {
|
|
43
|
+
// AppleScript provider doesn't use SQL, config is ignored
|
|
44
|
+
Object.assign(this.config, newConfig);
|
|
45
|
+
}
|
|
46
|
+
async getTasks(filter) {
|
|
47
|
+
let condition = "";
|
|
48
|
+
if (filter === "flagged") {
|
|
49
|
+
condition = "whose flagged is true";
|
|
50
|
+
}
|
|
51
|
+
else if (filter === "due_today") {
|
|
52
|
+
condition = "whose due date is not missing value and due date < (current date) + 1 * days";
|
|
53
|
+
}
|
|
54
|
+
const script = `
|
|
55
|
+
tell application "OmniFocus"
|
|
56
|
+
tell default document
|
|
57
|
+
set taskList to {}
|
|
58
|
+
set theTasks to flattened tasks ${condition}
|
|
59
|
+
repeat with t in theTasks
|
|
60
|
+
if completed of t is false then
|
|
61
|
+
set taskId to id of t
|
|
62
|
+
set taskName to name of t
|
|
63
|
+
set taskNote to note of t
|
|
64
|
+
set taskFlagged to flagged of t
|
|
65
|
+
set taskDue to ""
|
|
66
|
+
if due date of t is not missing value then
|
|
67
|
+
set taskDue to (due date of t) as «class isot» as string
|
|
68
|
+
end if
|
|
69
|
+
set projectName to ""
|
|
70
|
+
try
|
|
71
|
+
set projectName to name of containing project of t
|
|
72
|
+
end try
|
|
73
|
+
set end of taskList to taskId & "|||" & taskName & "|||" & taskNote & "|||" & taskFlagged & "|||" & taskDue & "|||" & projectName
|
|
74
|
+
end if
|
|
75
|
+
end repeat
|
|
76
|
+
set AppleScript's text item delimiters to "
|
|
77
|
+
---TASK---
|
|
78
|
+
"
|
|
79
|
+
return taskList as text
|
|
80
|
+
end tell
|
|
81
|
+
end tell
|
|
82
|
+
`;
|
|
83
|
+
const result = await runAppleScript(script);
|
|
84
|
+
if (!result)
|
|
85
|
+
return [];
|
|
86
|
+
// use newline-based delimiter to handle commas in task names
|
|
87
|
+
return result.split("\n---TASK---\n").map(line => {
|
|
88
|
+
const [id, name, note, flagged, dueDate, project] = line.split("|||");
|
|
89
|
+
return {
|
|
90
|
+
id,
|
|
91
|
+
name,
|
|
92
|
+
note: note || undefined,
|
|
93
|
+
flagged: flagged === "true",
|
|
94
|
+
dueDate: dueDate || undefined,
|
|
95
|
+
project: project || undefined,
|
|
96
|
+
completed: false
|
|
97
|
+
};
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
async createTask(input) {
|
|
101
|
+
const escapedName = escapeAppleScriptString(input.name);
|
|
102
|
+
const props = [`name:"${escapedName}"`];
|
|
103
|
+
if (input.note) {
|
|
104
|
+
const escapedNote = escapeAppleScriptString(input.note);
|
|
105
|
+
props.push(`note:"${escapedNote}"`);
|
|
106
|
+
}
|
|
107
|
+
if (input.flagged) {
|
|
108
|
+
props.push("flagged:true");
|
|
109
|
+
}
|
|
110
|
+
if (input.dueDate) {
|
|
111
|
+
const escapedDate = escapeAppleScriptString(input.dueDate);
|
|
112
|
+
props.push(`due date:date "${escapedDate}"`);
|
|
113
|
+
}
|
|
114
|
+
let script;
|
|
115
|
+
if (input.project) {
|
|
116
|
+
const escapedProject = escapeAppleScriptString(input.project);
|
|
117
|
+
script = `
|
|
118
|
+
tell application "OmniFocus"
|
|
119
|
+
tell default document
|
|
120
|
+
set theProject to first flattened project whose name is "${escapedProject}"
|
|
121
|
+
set newTask to make new task with properties {${props.join(", ")}} at end of tasks of theProject
|
|
122
|
+
return id of newTask
|
|
123
|
+
end tell
|
|
124
|
+
end tell
|
|
125
|
+
`;
|
|
126
|
+
}
|
|
127
|
+
else {
|
|
128
|
+
script = `
|
|
129
|
+
tell application "OmniFocus"
|
|
130
|
+
tell default document
|
|
131
|
+
set newTask to make new inbox task with properties {${props.join(", ")}}
|
|
132
|
+
return id of newTask
|
|
133
|
+
end tell
|
|
134
|
+
end tell
|
|
135
|
+
`;
|
|
136
|
+
}
|
|
137
|
+
const taskId = await runAppleScript(script);
|
|
138
|
+
return { success: true, taskId };
|
|
139
|
+
}
|
|
140
|
+
async updateTask(input) {
|
|
141
|
+
const updates = [];
|
|
142
|
+
const escapedTaskId = escapeAppleScriptString(input.taskId);
|
|
143
|
+
if (input.name) {
|
|
144
|
+
const escapedName = escapeAppleScriptString(input.name);
|
|
145
|
+
updates.push(`set name of theTask to "${escapedName}"`);
|
|
146
|
+
}
|
|
147
|
+
if (input.note !== undefined) {
|
|
148
|
+
const escapedNote = escapeAppleScriptString(input.note);
|
|
149
|
+
updates.push(`set note of theTask to "${escapedNote}"`);
|
|
150
|
+
}
|
|
151
|
+
if (input.flagged !== undefined) {
|
|
152
|
+
updates.push(`set flagged of theTask to ${input.flagged}`);
|
|
153
|
+
}
|
|
154
|
+
if (input.dueDate) {
|
|
155
|
+
const escapedDate = escapeAppleScriptString(input.dueDate);
|
|
156
|
+
updates.push(`set due date of theTask to date "${escapedDate}"`);
|
|
157
|
+
}
|
|
158
|
+
const script = `
|
|
159
|
+
tell application "OmniFocus"
|
|
160
|
+
tell default document
|
|
161
|
+
set theTask to first flattened task whose id is "${escapedTaskId}"
|
|
162
|
+
${updates.join("\n ")}
|
|
163
|
+
end tell
|
|
164
|
+
end tell
|
|
165
|
+
`;
|
|
166
|
+
await runAppleScript(script);
|
|
167
|
+
return { success: true };
|
|
168
|
+
}
|
|
169
|
+
async completeTask(taskId) {
|
|
170
|
+
const escapedTaskId = escapeAppleScriptString(taskId);
|
|
171
|
+
const script = `
|
|
172
|
+
tell application "OmniFocus"
|
|
173
|
+
tell default document
|
|
174
|
+
set theTask to first flattened task whose id is "${escapedTaskId}"
|
|
175
|
+
set completed of theTask to true
|
|
176
|
+
end tell
|
|
177
|
+
end tell
|
|
178
|
+
`;
|
|
179
|
+
await runAppleScript(script);
|
|
180
|
+
return { success: true };
|
|
181
|
+
}
|
|
182
|
+
async getProjects() {
|
|
183
|
+
const script = `
|
|
184
|
+
tell application "OmniFocus"
|
|
185
|
+
tell default document
|
|
186
|
+
set projectNames to {}
|
|
187
|
+
repeat with p in flattened projects
|
|
188
|
+
if status of p is active then
|
|
189
|
+
set end of projectNames to name of p
|
|
190
|
+
end if
|
|
191
|
+
end repeat
|
|
192
|
+
set AppleScript's text item delimiters to "
|
|
193
|
+
---PROJECT---
|
|
194
|
+
"
|
|
195
|
+
return projectNames as text
|
|
196
|
+
end tell
|
|
197
|
+
end tell
|
|
198
|
+
`;
|
|
199
|
+
const result = await runAppleScript(script);
|
|
200
|
+
if (!result)
|
|
201
|
+
return [];
|
|
202
|
+
return result.split("\n---PROJECT---\n");
|
|
203
|
+
}
|
|
204
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { OmniFocusProvider, OmniFocusTask, CreateTaskInput, UpdateTaskInput, OmniFocusConfig } from "../types.js";
|
|
2
|
+
export declare class UrlSchemeProvider implements OmniFocusProvider {
|
|
3
|
+
version: "standard";
|
|
4
|
+
config: OmniFocusConfig;
|
|
5
|
+
setConfig(newConfig: Partial<OmniFocusConfig>): void;
|
|
6
|
+
getTasks(filter?: "flagged" | "due_today" | "all"): Promise<OmniFocusTask[]>;
|
|
7
|
+
createTask(input: CreateTaskInput): Promise<{
|
|
8
|
+
success: boolean;
|
|
9
|
+
warning?: string;
|
|
10
|
+
}>;
|
|
11
|
+
updateTask(input: UpdateTaskInput): Promise<{
|
|
12
|
+
success: boolean;
|
|
13
|
+
warning?: string;
|
|
14
|
+
}>;
|
|
15
|
+
completeTask(taskId: string): Promise<{
|
|
16
|
+
success: boolean;
|
|
17
|
+
warning?: string;
|
|
18
|
+
}>;
|
|
19
|
+
getProjects(): Promise<string[]>;
|
|
20
|
+
}
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
import { execFile } from "child_process";
|
|
2
|
+
import { promisify } from "util";
|
|
3
|
+
import { homedir } from "os";
|
|
4
|
+
import Database from "better-sqlite3";
|
|
5
|
+
import { DEFAULT_TASK_LIMIT } from "../types.js";
|
|
6
|
+
const execFileAsync = promisify(execFile);
|
|
7
|
+
const DB_PATH = `${homedir()}/Library/Group Containers/34YW5XSRB7.com.omnigroup.OmniFocus/com.omnigroup.OmniFocus4/com.omnigroup.OmniFocusModel/OmniFocusDatabase.db`;
|
|
8
|
+
// Core Data (Apple) uses 2001-01-01 as epoch, Unix uses 1970-01-01
|
|
9
|
+
// difference is ~31 years (978307200 seconds)
|
|
10
|
+
const CORE_DATA_EPOCH_MS = new Date("2001-01-01T00:00:00Z").getTime();
|
|
11
|
+
function getDatabase(readonly = true) {
|
|
12
|
+
return new Database(DB_PATH, { readonly, fileMustExist: true });
|
|
13
|
+
}
|
|
14
|
+
export class UrlSchemeProvider {
|
|
15
|
+
version = "standard";
|
|
16
|
+
config = { directSqlAccess: true, taskLimit: DEFAULT_TASK_LIMIT };
|
|
17
|
+
setConfig(newConfig) {
|
|
18
|
+
Object.assign(this.config, newConfig);
|
|
19
|
+
}
|
|
20
|
+
async getTasks(filter) {
|
|
21
|
+
const db = getDatabase(true);
|
|
22
|
+
try {
|
|
23
|
+
let whereClause = "t.dateCompleted IS NULL";
|
|
24
|
+
if (filter === "flagged") {
|
|
25
|
+
whereClause += " AND t.flagged = 1";
|
|
26
|
+
}
|
|
27
|
+
else if (filter === "due_today") {
|
|
28
|
+
// '+31 years' converts Core Data epoch (2001) to Unix epoch (1970)
|
|
29
|
+
whereClause += " AND date(t.dateDue, 'unixepoch', '+31 years') = date('now')";
|
|
30
|
+
}
|
|
31
|
+
else if (!filter || filter === "all") {
|
|
32
|
+
whereClause += " AND (t.flagged = 1 OR date(t.dateDue, 'unixepoch', '+31 years') <= date('now'))";
|
|
33
|
+
}
|
|
34
|
+
const query = `
|
|
35
|
+
SELECT
|
|
36
|
+
t.persistentIdentifier as id,
|
|
37
|
+
t.name,
|
|
38
|
+
t.plainTextNote as note,
|
|
39
|
+
t.flagged,
|
|
40
|
+
datetime(t.dateDue, 'unixepoch', '+31 years') as dueDate,
|
|
41
|
+
p.name as project
|
|
42
|
+
FROM Task t
|
|
43
|
+
LEFT JOIN ProjectInfo pi ON t.containingProjectInfo = pi.pk
|
|
44
|
+
LEFT JOIN Task p ON pi.task = p.persistentIdentifier
|
|
45
|
+
WHERE ${whereClause}
|
|
46
|
+
ORDER BY t.dateDue ASC, t.flagged DESC
|
|
47
|
+
LIMIT ${this.config.taskLimit}
|
|
48
|
+
`;
|
|
49
|
+
const rows = db.prepare(query).all();
|
|
50
|
+
return rows.map(row => ({
|
|
51
|
+
id: row.id,
|
|
52
|
+
name: row.name,
|
|
53
|
+
note: row.note || undefined,
|
|
54
|
+
flagged: row.flagged === 1,
|
|
55
|
+
dueDate: row.dueDate || undefined,
|
|
56
|
+
project: row.project || undefined,
|
|
57
|
+
completed: false
|
|
58
|
+
}));
|
|
59
|
+
}
|
|
60
|
+
finally {
|
|
61
|
+
db.close();
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
async createTask(input) {
|
|
65
|
+
const params = new URLSearchParams();
|
|
66
|
+
params.set("name", input.name);
|
|
67
|
+
params.set("autosave", "true");
|
|
68
|
+
if (input.note) {
|
|
69
|
+
params.set("note", input.note);
|
|
70
|
+
}
|
|
71
|
+
if (input.flagged) {
|
|
72
|
+
params.set("flag", "true");
|
|
73
|
+
}
|
|
74
|
+
if (input.dueDate) {
|
|
75
|
+
params.set("due", input.dueDate);
|
|
76
|
+
}
|
|
77
|
+
if (input.project) {
|
|
78
|
+
params.set("project", input.project);
|
|
79
|
+
}
|
|
80
|
+
const url = `omnifocus:///add?${params.toString()}`;
|
|
81
|
+
await execFileAsync("open", [url]);
|
|
82
|
+
return {
|
|
83
|
+
success: true,
|
|
84
|
+
warning: "Task created via URL scheme. It will sync automatically."
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
async updateTask(input) {
|
|
88
|
+
if (!this.config.directSqlAccess) {
|
|
89
|
+
return {
|
|
90
|
+
success: false,
|
|
91
|
+
warning: "Direct SQL access is disabled. Update operations require directSqlAccess=true or OmniFocus Pro. Use omnifocus_set_config to enable it."
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
const db = getDatabase(false);
|
|
95
|
+
try {
|
|
96
|
+
const updates = [];
|
|
97
|
+
const params = {
|
|
98
|
+
taskId: input.taskId
|
|
99
|
+
};
|
|
100
|
+
if (input.name !== undefined) {
|
|
101
|
+
updates.push("name = @name");
|
|
102
|
+
params.name = input.name;
|
|
103
|
+
}
|
|
104
|
+
if (input.note !== undefined) {
|
|
105
|
+
updates.push("plainTextNote = @note");
|
|
106
|
+
params.note = input.note;
|
|
107
|
+
}
|
|
108
|
+
if (input.flagged !== undefined) {
|
|
109
|
+
updates.push("flagged = @flagged");
|
|
110
|
+
params.flagged = input.flagged ? 1 : 0;
|
|
111
|
+
}
|
|
112
|
+
if (input.dueDate !== undefined) {
|
|
113
|
+
const date = new Date(input.dueDate);
|
|
114
|
+
const timestamp = (date.getTime() - CORE_DATA_EPOCH_MS) / 1000;
|
|
115
|
+
updates.push("dateDue = @dateDue");
|
|
116
|
+
params.dateDue = timestamp;
|
|
117
|
+
}
|
|
118
|
+
if (updates.length === 0) {
|
|
119
|
+
return { success: true };
|
|
120
|
+
}
|
|
121
|
+
const query = `UPDATE Task SET ${updates.join(", ")} WHERE persistentIdentifier = @taskId`;
|
|
122
|
+
const stmt = db.prepare(query);
|
|
123
|
+
const result = stmt.run(params);
|
|
124
|
+
if (result.changes === 0) {
|
|
125
|
+
throw new Error("Task not found");
|
|
126
|
+
}
|
|
127
|
+
return {
|
|
128
|
+
success: true,
|
|
129
|
+
warning: "Task updated via SQLite. Changes won't sync until OmniFocus is restarted. Consider using OmniFocus Pro for full sync support."
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
finally {
|
|
133
|
+
db.close();
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
async completeTask(taskId) {
|
|
137
|
+
if (!this.config.directSqlAccess) {
|
|
138
|
+
return {
|
|
139
|
+
success: false,
|
|
140
|
+
warning: "Direct SQL access is disabled. Complete operations require directSqlAccess=true or OmniFocus Pro. Use omnifocus_set_config to enable it."
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
const db = getDatabase(false);
|
|
144
|
+
try {
|
|
145
|
+
const now = new Date();
|
|
146
|
+
const timestamp = (now.getTime() - CORE_DATA_EPOCH_MS) / 1000;
|
|
147
|
+
const stmt = db.prepare("UPDATE Task SET dateCompleted = @timestamp WHERE persistentIdentifier = @taskId");
|
|
148
|
+
const result = stmt.run({ timestamp, taskId });
|
|
149
|
+
if (result.changes === 0) {
|
|
150
|
+
throw new Error("Task not found");
|
|
151
|
+
}
|
|
152
|
+
return {
|
|
153
|
+
success: true,
|
|
154
|
+
warning: "Task marked complete via SQLite. Changes won't sync until OmniFocus is restarted. Consider using OmniFocus Pro for full sync support."
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
finally {
|
|
158
|
+
db.close();
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
async getProjects() {
|
|
162
|
+
const db = getDatabase(true);
|
|
163
|
+
try {
|
|
164
|
+
const query = `
|
|
165
|
+
SELECT t.name
|
|
166
|
+
FROM Task t
|
|
167
|
+
JOIN ProjectInfo pi ON t.persistentIdentifier = pi.task
|
|
168
|
+
WHERE t.dateCompleted IS NULL
|
|
169
|
+
ORDER BY t.name
|
|
170
|
+
`;
|
|
171
|
+
const rows = db.prepare(query).all();
|
|
172
|
+
return rows.map(row => row.name);
|
|
173
|
+
}
|
|
174
|
+
finally {
|
|
175
|
+
db.close();
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
}
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
export interface OmniFocusTask {
|
|
2
|
+
id: string;
|
|
3
|
+
name: string;
|
|
4
|
+
note?: string;
|
|
5
|
+
project?: string;
|
|
6
|
+
flagged?: boolean;
|
|
7
|
+
dueDate?: string;
|
|
8
|
+
completed?: boolean;
|
|
9
|
+
}
|
|
10
|
+
export interface CreateTaskInput {
|
|
11
|
+
name: string;
|
|
12
|
+
note?: string;
|
|
13
|
+
project?: string;
|
|
14
|
+
flagged?: boolean;
|
|
15
|
+
dueDate?: string;
|
|
16
|
+
}
|
|
17
|
+
export interface UpdateTaskInput {
|
|
18
|
+
taskId: string;
|
|
19
|
+
name?: string;
|
|
20
|
+
note?: string;
|
|
21
|
+
flagged?: boolean;
|
|
22
|
+
dueDate?: string;
|
|
23
|
+
}
|
|
24
|
+
export interface GetTasksInput {
|
|
25
|
+
filter?: "flagged" | "due_today" | "all";
|
|
26
|
+
}
|
|
27
|
+
export interface CompleteTaskInput {
|
|
28
|
+
taskId: string;
|
|
29
|
+
}
|
|
30
|
+
export type OmniFocusVersion = "pro" | "standard";
|
|
31
|
+
export interface OmniFocusConfig {
|
|
32
|
+
directSqlAccess: boolean;
|
|
33
|
+
taskLimit: number;
|
|
34
|
+
}
|
|
35
|
+
export declare const DEFAULT_TASK_LIMIT = 500;
|
|
36
|
+
export interface OmniFocusProvider {
|
|
37
|
+
config: OmniFocusConfig;
|
|
38
|
+
setConfig(config: Partial<OmniFocusConfig>): void;
|
|
39
|
+
version: OmniFocusVersion;
|
|
40
|
+
getTasks(filter?: "flagged" | "due_today" | "all"): Promise<OmniFocusTask[]>;
|
|
41
|
+
createTask(input: CreateTaskInput): Promise<{
|
|
42
|
+
success: boolean;
|
|
43
|
+
taskId?: string;
|
|
44
|
+
warning?: string;
|
|
45
|
+
}>;
|
|
46
|
+
updateTask(input: UpdateTaskInput): Promise<{
|
|
47
|
+
success: boolean;
|
|
48
|
+
warning?: string;
|
|
49
|
+
}>;
|
|
50
|
+
completeTask(taskId: string): Promise<{
|
|
51
|
+
success: boolean;
|
|
52
|
+
warning?: string;
|
|
53
|
+
}>;
|
|
54
|
+
getProjects(): Promise<string[]>;
|
|
55
|
+
}
|
package/dist/types.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export const DEFAULT_TASK_LIMIT = 500;
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
export declare const CreateTaskInputSchema: z.ZodObject<{
|
|
3
|
+
name: z.ZodString;
|
|
4
|
+
note: z.ZodOptional<z.ZodString>;
|
|
5
|
+
project: z.ZodOptional<z.ZodString>;
|
|
6
|
+
flagged: z.ZodOptional<z.ZodBoolean>;
|
|
7
|
+
dueDate: z.ZodOptional<z.ZodEffects<z.ZodString, string, string>>;
|
|
8
|
+
}, "strip", z.ZodTypeAny, {
|
|
9
|
+
name: string;
|
|
10
|
+
flagged?: boolean | undefined;
|
|
11
|
+
note?: string | undefined;
|
|
12
|
+
dueDate?: string | undefined;
|
|
13
|
+
project?: string | undefined;
|
|
14
|
+
}, {
|
|
15
|
+
name: string;
|
|
16
|
+
flagged?: boolean | undefined;
|
|
17
|
+
note?: string | undefined;
|
|
18
|
+
dueDate?: string | undefined;
|
|
19
|
+
project?: string | undefined;
|
|
20
|
+
}>;
|
|
21
|
+
export declare const UpdateTaskInputSchema: z.ZodObject<{
|
|
22
|
+
taskId: z.ZodString;
|
|
23
|
+
name: z.ZodOptional<z.ZodString>;
|
|
24
|
+
note: z.ZodOptional<z.ZodString>;
|
|
25
|
+
flagged: z.ZodOptional<z.ZodBoolean>;
|
|
26
|
+
dueDate: z.ZodOptional<z.ZodEffects<z.ZodString, string, string>>;
|
|
27
|
+
}, "strip", z.ZodTypeAny, {
|
|
28
|
+
taskId: string;
|
|
29
|
+
flagged?: boolean | undefined;
|
|
30
|
+
name?: string | undefined;
|
|
31
|
+
note?: string | undefined;
|
|
32
|
+
dueDate?: string | undefined;
|
|
33
|
+
}, {
|
|
34
|
+
taskId: string;
|
|
35
|
+
flagged?: boolean | undefined;
|
|
36
|
+
name?: string | undefined;
|
|
37
|
+
note?: string | undefined;
|
|
38
|
+
dueDate?: string | undefined;
|
|
39
|
+
}>;
|
|
40
|
+
export declare const CompleteTaskInputSchema: z.ZodObject<{
|
|
41
|
+
taskId: z.ZodString;
|
|
42
|
+
}, "strip", z.ZodTypeAny, {
|
|
43
|
+
taskId: string;
|
|
44
|
+
}, {
|
|
45
|
+
taskId: string;
|
|
46
|
+
}>;
|
|
47
|
+
export declare const GetTasksInputSchema: z.ZodObject<{
|
|
48
|
+
filter: z.ZodOptional<z.ZodEnum<["flagged", "due_today", "all"]>>;
|
|
49
|
+
}, "strip", z.ZodTypeAny, {
|
|
50
|
+
filter?: "flagged" | "due_today" | "all" | undefined;
|
|
51
|
+
}, {
|
|
52
|
+
filter?: "flagged" | "due_today" | "all" | undefined;
|
|
53
|
+
}>;
|
|
54
|
+
export declare const SetConfigInputSchema: z.ZodObject<{
|
|
55
|
+
directSqlAccess: z.ZodOptional<z.ZodBoolean>;
|
|
56
|
+
taskLimit: z.ZodOptional<z.ZodNumber>;
|
|
57
|
+
}, "strip", z.ZodTypeAny, {
|
|
58
|
+
directSqlAccess?: boolean | undefined;
|
|
59
|
+
taskLimit?: number | undefined;
|
|
60
|
+
}, {
|
|
61
|
+
directSqlAccess?: boolean | undefined;
|
|
62
|
+
taskLimit?: number | undefined;
|
|
63
|
+
}>;
|
|
64
|
+
export type ValidatedCreateTaskInput = z.infer<typeof CreateTaskInputSchema>;
|
|
65
|
+
export type ValidatedUpdateTaskInput = z.infer<typeof UpdateTaskInputSchema>;
|
|
66
|
+
export type ValidatedCompleteTaskInput = z.infer<typeof CompleteTaskInputSchema>;
|
|
67
|
+
export type ValidatedGetTasksInput = z.infer<typeof GetTasksInputSchema>;
|
|
68
|
+
export type ValidatedSetConfigInput = z.infer<typeof SetConfigInputSchema>;
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
const MAX_NAME_LENGTH = 1000;
|
|
3
|
+
const MAX_NOTE_LENGTH = 10000;
|
|
4
|
+
const MAX_PROJECT_LENGTH = 500;
|
|
5
|
+
const MAX_TASK_ID_LENGTH = 100;
|
|
6
|
+
const ISO_DATE_REGEX = /^\d{4}-\d{2}-\d{2}$/;
|
|
7
|
+
const dateSchema = z.string().regex(ISO_DATE_REGEX, "Invalid date format. Use YYYY-MM-DD").refine((val) => {
|
|
8
|
+
const date = new Date(val);
|
|
9
|
+
return !isNaN(date.getTime());
|
|
10
|
+
}, { message: "Invalid date value" });
|
|
11
|
+
const taskIdSchema = z.string()
|
|
12
|
+
.min(1, "Task ID is required")
|
|
13
|
+
.max(MAX_TASK_ID_LENGTH, `Task ID must be at most ${MAX_TASK_ID_LENGTH} characters`)
|
|
14
|
+
.regex(/^[a-zA-Z0-9_-]+$/, "Task ID contains invalid characters");
|
|
15
|
+
export const CreateTaskInputSchema = z.object({
|
|
16
|
+
name: z.string()
|
|
17
|
+
.min(1, "Task name is required")
|
|
18
|
+
.max(MAX_NAME_LENGTH, `Task name must be at most ${MAX_NAME_LENGTH} characters`),
|
|
19
|
+
note: z.string()
|
|
20
|
+
.max(MAX_NOTE_LENGTH, `Note must be at most ${MAX_NOTE_LENGTH} characters`)
|
|
21
|
+
.optional(),
|
|
22
|
+
project: z.string()
|
|
23
|
+
.max(MAX_PROJECT_LENGTH, `Project name must be at most ${MAX_PROJECT_LENGTH} characters`)
|
|
24
|
+
.optional(),
|
|
25
|
+
flagged: z.boolean().optional(),
|
|
26
|
+
dueDate: dateSchema.optional(),
|
|
27
|
+
});
|
|
28
|
+
export const UpdateTaskInputSchema = z.object({
|
|
29
|
+
taskId: taskIdSchema,
|
|
30
|
+
name: z.string()
|
|
31
|
+
.min(1, "Task name cannot be empty")
|
|
32
|
+
.max(MAX_NAME_LENGTH, `Task name must be at most ${MAX_NAME_LENGTH} characters`)
|
|
33
|
+
.optional(),
|
|
34
|
+
note: z.string()
|
|
35
|
+
.max(MAX_NOTE_LENGTH, `Note must be at most ${MAX_NOTE_LENGTH} characters`)
|
|
36
|
+
.optional(),
|
|
37
|
+
flagged: z.boolean().optional(),
|
|
38
|
+
dueDate: dateSchema.optional(),
|
|
39
|
+
});
|
|
40
|
+
export const CompleteTaskInputSchema = z.object({
|
|
41
|
+
taskId: taskIdSchema,
|
|
42
|
+
});
|
|
43
|
+
export const GetTasksInputSchema = z.object({
|
|
44
|
+
filter: z.enum(["flagged", "due_today", "all"]).optional(),
|
|
45
|
+
});
|
|
46
|
+
export const SetConfigInputSchema = z.object({
|
|
47
|
+
directSqlAccess: z.boolean().optional(),
|
|
48
|
+
taskLimit: z.number().int().min(1).max(10000).optional(),
|
|
49
|
+
});
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { spawn } from "child_process";
|
|
2
|
+
function runAppleScriptCheck(script) {
|
|
3
|
+
return new Promise((resolve) => {
|
|
4
|
+
const child = spawn("osascript", ["-"], {
|
|
5
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
6
|
+
});
|
|
7
|
+
let stderr = "";
|
|
8
|
+
child.stderr.on("data", (data) => {
|
|
9
|
+
stderr += data.toString();
|
|
10
|
+
});
|
|
11
|
+
child.on("close", (code) => {
|
|
12
|
+
resolve({ success: code === 0, stderr });
|
|
13
|
+
});
|
|
14
|
+
child.on("error", () => {
|
|
15
|
+
resolve({ success: false, stderr: "spawn error" });
|
|
16
|
+
});
|
|
17
|
+
child.stdin.write(script);
|
|
18
|
+
child.stdin.end();
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
const VERSION_DETECTION_TIMEOUT_MS = 5000;
|
|
22
|
+
async function detectVersionInternal() {
|
|
23
|
+
const script = 'tell application "OmniFocus" to get name of first flattened task';
|
|
24
|
+
const result = await runAppleScriptCheck(script);
|
|
25
|
+
if (result.success) {
|
|
26
|
+
return "pro";
|
|
27
|
+
}
|
|
28
|
+
// error -1743 = scripting not authorized (Standard version)
|
|
29
|
+
if (result.stderr.includes("-1743")) {
|
|
30
|
+
return "standard";
|
|
31
|
+
}
|
|
32
|
+
// if no tasks exist, AppleScript still works - it's Pro
|
|
33
|
+
if (result.stderr.includes("Can't get")) {
|
|
34
|
+
return "pro";
|
|
35
|
+
}
|
|
36
|
+
// default to standard if we can't determine
|
|
37
|
+
return "standard";
|
|
38
|
+
}
|
|
39
|
+
export async function detectVersion() {
|
|
40
|
+
const timeout = new Promise((resolve) => {
|
|
41
|
+
setTimeout(() => {
|
|
42
|
+
console.error("[mcp-omnifocus] Version detection timed out, falling back to standard");
|
|
43
|
+
resolve("standard");
|
|
44
|
+
}, VERSION_DETECTION_TIMEOUT_MS);
|
|
45
|
+
});
|
|
46
|
+
return Promise.race([detectVersionInternal(), timeout]);
|
|
47
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "mcp-omnifocus",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "MCP server for OmniFocus with auto-detection of Pro/Standard version",
|
|
5
|
+
"author": "Mikhail Lihachev",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "https://github.com/avlihachev/mcp-omnifocus"
|
|
10
|
+
},
|
|
11
|
+
"keywords": [
|
|
12
|
+
"mcp",
|
|
13
|
+
"omnifocus",
|
|
14
|
+
"claude",
|
|
15
|
+
"anthropic",
|
|
16
|
+
"productivity",
|
|
17
|
+
"task-management"
|
|
18
|
+
],
|
|
19
|
+
"type": "module",
|
|
20
|
+
"main": "dist/index.js",
|
|
21
|
+
"bin": {
|
|
22
|
+
"mcp-omnifocus": "dist/index.js"
|
|
23
|
+
},
|
|
24
|
+
"files": [
|
|
25
|
+
"dist",
|
|
26
|
+
"README.md",
|
|
27
|
+
"LICENSE"
|
|
28
|
+
],
|
|
29
|
+
"scripts": {
|
|
30
|
+
"build": "tsc",
|
|
31
|
+
"dev": "tsc --watch",
|
|
32
|
+
"start": "node dist/index.js",
|
|
33
|
+
"test": "vitest run",
|
|
34
|
+
"test:watch": "vitest",
|
|
35
|
+
"test:coverage": "vitest run --coverage",
|
|
36
|
+
"prepublishOnly": "npm run build && npm test"
|
|
37
|
+
},
|
|
38
|
+
"dependencies": {
|
|
39
|
+
"@modelcontextprotocol/sdk": "^1.0.0",
|
|
40
|
+
"better-sqlite3": "^11.0.0",
|
|
41
|
+
"zod": "^3.23.0"
|
|
42
|
+
},
|
|
43
|
+
"devDependencies": {
|
|
44
|
+
"@types/better-sqlite3": "^7.6.0",
|
|
45
|
+
"@types/node": "^20.0.0",
|
|
46
|
+
"@vitest/coverage-v8": "^4.0.17",
|
|
47
|
+
"typescript": "^5.0.0",
|
|
48
|
+
"vitest": "^4.0.17"
|
|
49
|
+
},
|
|
50
|
+
"engines": {
|
|
51
|
+
"node": ">=18"
|
|
52
|
+
}
|
|
53
|
+
}
|