@scardis/omnifocus-mcp 0.1.0 → 0.1.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.
- package/dist/schemas/shapes.d.ts +3 -33
- package/dist/schemas/shapes.d.ts.map +1 -1
- package/dist/schemas/shapes.js +3 -5
- package/dist/schemas/shapes.js.map +1 -1
- package/dist/server.js +3 -5
- package/dist/server.js.map +1 -1
- package/dist/tools/createTask.d.ts +1 -11
- package/dist/tools/createTask.d.ts.map +1 -1
- package/dist/tools/editTask.d.ts +1 -11
- package/dist/tools/editTask.d.ts.map +1 -1
- package/dist/tools/index.d.ts +2 -22
- package/dist/tools/index.d.ts.map +1 -1
- package/package.json +7 -1
- package/src/snippets/edit_task.js +4 -6
- package/.claude/commands/opsx/apply.md +0 -152
- package/.claude/commands/opsx/archive.md +0 -157
- package/.claude/commands/opsx/bulk-archive.md +0 -242
- package/.claude/commands/opsx/continue.md +0 -114
- package/.claude/commands/opsx/explore.md +0 -173
- package/.claude/commands/opsx/ff.md +0 -97
- package/.claude/commands/opsx/new.md +0 -69
- package/.claude/commands/opsx/onboard.md +0 -550
- package/.claude/commands/opsx/propose.md +0 -106
- package/.claude/commands/opsx/sync.md +0 -134
- package/.claude/commands/opsx/verify.md +0 -164
- package/.claude/skills/openspec-apply-change/SKILL.md +0 -156
- package/.claude/skills/openspec-archive-change/SKILL.md +0 -114
- package/.claude/skills/openspec-bulk-archive-change/SKILL.md +0 -246
- package/.claude/skills/openspec-continue-change/SKILL.md +0 -118
- package/.claude/skills/openspec-explore/SKILL.md +0 -288
- package/.claude/skills/openspec-ff-change/SKILL.md +0 -101
- package/.claude/skills/openspec-new-change/SKILL.md +0 -74
- package/.claude/skills/openspec-onboard/SKILL.md +0 -554
- package/.claude/skills/openspec-propose/SKILL.md +0 -110
- package/.claude/skills/openspec-sync-specs/SKILL.md +0 -138
- package/.claude/skills/openspec-verify-change/SKILL.md +0 -168
- package/CONTRIBUTING.md +0 -83
- package/openspec/changes/archive/2026-04-09-bootstrap-omnifocus-mcp/.openspec.yaml +0 -2
- package/openspec/changes/archive/2026-04-09-bootstrap-omnifocus-mcp/design.md +0 -162
- package/openspec/changes/archive/2026-04-09-bootstrap-omnifocus-mcp/proposal.md +0 -49
- package/openspec/changes/archive/2026-04-09-bootstrap-omnifocus-mcp/specs/attachments/spec.md +0 -9
- package/openspec/changes/archive/2026-04-09-bootstrap-omnifocus-mcp/specs/batch-operations/spec.md +0 -9
- package/openspec/changes/archive/2026-04-09-bootstrap-omnifocus-mcp/specs/database-inspection/spec.md +0 -9
- package/openspec/changes/archive/2026-04-09-bootstrap-omnifocus-mcp/specs/execution-runtime/spec.md +0 -69
- package/openspec/changes/archive/2026-04-09-bootstrap-omnifocus-mcp/specs/folder-management/spec.md +0 -25
- package/openspec/changes/archive/2026-04-09-bootstrap-omnifocus-mcp/specs/forecast/spec.md +0 -9
- package/openspec/changes/archive/2026-04-09-bootstrap-omnifocus-mcp/specs/identity-resolution/spec.md +0 -45
- package/openspec/changes/archive/2026-04-09-bootstrap-omnifocus-mcp/specs/perspective-management/spec.md +0 -9
- package/openspec/changes/archive/2026-04-09-bootstrap-omnifocus-mcp/specs/project-management/spec.md +0 -25
- package/openspec/changes/archive/2026-04-09-bootstrap-omnifocus-mcp/specs/recurrence/spec.md +0 -9
- package/openspec/changes/archive/2026-04-09-bootstrap-omnifocus-mcp/specs/settings/spec.md +0 -9
- package/openspec/changes/archive/2026-04-09-bootstrap-omnifocus-mcp/specs/tag-management/spec.md +0 -25
- package/openspec/changes/archive/2026-04-09-bootstrap-omnifocus-mcp/specs/task-management/spec.md +0 -29
- package/openspec/changes/archive/2026-04-09-bootstrap-omnifocus-mcp/specs/url-automation/spec.md +0 -9
- package/openspec/changes/archive/2026-04-09-bootstrap-omnifocus-mcp/specs/window-state/spec.md +0 -9
- package/openspec/changes/archive/2026-04-09-bootstrap-omnifocus-mcp/tasks.md +0 -84
- package/openspec/changes/archive/2026-04-09-folder-crud/.openspec.yaml +0 -2
- package/openspec/changes/archive/2026-04-09-folder-crud/design.md +0 -58
- package/openspec/changes/archive/2026-04-09-folder-crud/proposal.md +0 -28
- package/openspec/changes/archive/2026-04-09-folder-crud/specs/folder-write/spec.md +0 -45
- package/openspec/changes/archive/2026-04-09-folder-crud/tasks.md +0 -41
- package/openspec/changes/archive/2026-04-09-folder-tag-list-filtering/.openspec.yaml +0 -2
- package/openspec/changes/archive/2026-04-09-folder-tag-list-filtering/design.md +0 -38
- package/openspec/changes/archive/2026-04-09-folder-tag-list-filtering/proposal.md +0 -30
- package/openspec/changes/archive/2026-04-09-folder-tag-list-filtering/specs/folder-management/spec.md +0 -21
- package/openspec/changes/archive/2026-04-09-folder-tag-list-filtering/specs/tag-management/spec.md +0 -21
- package/openspec/changes/archive/2026-04-09-folder-tag-list-filtering/tasks.md +0 -35
- package/openspec/changes/archive/2026-04-09-move-operations/.openspec.yaml +0 -2
- package/openspec/changes/archive/2026-04-09-move-operations/design.md +0 -43
- package/openspec/changes/archive/2026-04-09-move-operations/proposal.md +0 -25
- package/openspec/changes/archive/2026-04-09-move-operations/specs/move-operations/spec.md +0 -41
- package/openspec/changes/archive/2026-04-09-move-operations/tasks.md +0 -40
- package/openspec/changes/archive/2026-04-09-project-crud/.openspec.yaml +0 -2
- package/openspec/changes/archive/2026-04-09-project-crud/design.md +0 -60
- package/openspec/changes/archive/2026-04-09-project-crud/proposal.md +0 -29
- package/openspec/changes/archive/2026-04-09-project-crud/specs/project-write/spec.md +0 -74
- package/openspec/changes/archive/2026-04-09-project-crud/tasks.md +0 -48
- package/openspec/changes/archive/2026-04-09-project-filtering/.openspec.yaml +0 -2
- package/openspec/changes/archive/2026-04-09-project-filtering/design.md +0 -52
- package/openspec/changes/archive/2026-04-09-project-filtering/proposal.md +0 -26
- package/openspec/changes/archive/2026-04-09-project-filtering/specs/project-filtering/spec.md +0 -66
- package/openspec/changes/archive/2026-04-09-project-filtering/specs/project-management/spec.md +0 -13
- package/openspec/changes/archive/2026-04-09-project-filtering/tasks.md +0 -41
- package/openspec/changes/archive/2026-04-09-tag-crud/.openspec.yaml +0 -2
- package/openspec/changes/archive/2026-04-09-tag-crud/design.md +0 -45
- package/openspec/changes/archive/2026-04-09-tag-crud/proposal.md +0 -28
- package/openspec/changes/archive/2026-04-09-tag-crud/specs/tag-write/spec.md +0 -49
- package/openspec/changes/archive/2026-04-09-tag-crud/tasks.md +0 -41
- package/openspec/changes/archive/2026-04-09-task-crud/.openspec.yaml +0 -2
- package/openspec/changes/archive/2026-04-09-task-crud/design.md +0 -62
- package/openspec/changes/archive/2026-04-09-task-crud/proposal.md +0 -29
- package/openspec/changes/archive/2026-04-09-task-crud/specs/task-management/spec.md +0 -17
- package/openspec/changes/archive/2026-04-09-task-crud/specs/task-write/spec.md +0 -89
- package/openspec/changes/archive/2026-04-09-task-crud/tasks.md +0 -55
- package/openspec/changes/archive/2026-04-09-task-filtering/.openspec.yaml +0 -2
- package/openspec/changes/archive/2026-04-09-task-filtering/design.md +0 -61
- package/openspec/changes/archive/2026-04-09-task-filtering/proposal.md +0 -26
- package/openspec/changes/archive/2026-04-09-task-filtering/specs/task-filtering/spec.md +0 -63
- package/openspec/changes/archive/2026-04-09-task-filtering/specs/task-management/spec.md +0 -17
- package/openspec/changes/archive/2026-04-09-task-filtering/tasks.md +0 -42
- package/openspec/changes/archive/2026-04-10-planned-date/.openspec.yaml +0 -2
- package/openspec/changes/archive/2026-04-10-planned-date/design.md +0 -27
- package/openspec/changes/archive/2026-04-10-planned-date/proposal.md +0 -29
- package/openspec/changes/archive/2026-04-10-planned-date/specs/task-management/spec.md +0 -29
- package/openspec/changes/archive/2026-04-10-planned-date/specs/task-write/spec.md +0 -69
- package/openspec/changes/archive/2026-04-10-planned-date/tasks.md +0 -26
- package/openspec/changes/archive/2026-04-10-task-recurrence/.openspec.yaml +0 -2
- package/openspec/changes/archive/2026-04-10-task-recurrence/design.md +0 -81
- package/openspec/changes/archive/2026-04-10-task-recurrence/proposal.md +0 -28
- package/openspec/changes/archive/2026-04-10-task-recurrence/specs/recurrence/spec.md +0 -47
- package/openspec/changes/archive/2026-04-10-task-recurrence/specs/task-management/spec.md +0 -25
- package/openspec/changes/archive/2026-04-10-task-recurrence/specs/task-write/spec.md +0 -61
- package/openspec/changes/archive/2026-04-10-task-recurrence/tasks.md +0 -39
- package/openspec/config.yaml +0 -20
- package/openspec/specs/attachments/spec.md +0 -15
- package/openspec/specs/batch-operations/spec.md +0 -15
- package/openspec/specs/database-inspection/spec.md +0 -15
- package/openspec/specs/execution-runtime/spec.md +0 -75
- package/openspec/specs/folder-management/spec.md +0 -39
- package/openspec/specs/folder-write/spec.md +0 -45
- package/openspec/specs/forecast/spec.md +0 -15
- package/openspec/specs/identity-resolution/spec.md +0 -51
- package/openspec/specs/move-operations/spec.md +0 -41
- package/openspec/specs/perspective-management/spec.md +0 -15
- package/openspec/specs/project-filtering/spec.md +0 -72
- package/openspec/specs/project-management/spec.md +0 -31
- package/openspec/specs/project-write/spec.md +0 -79
- package/openspec/specs/recurrence/spec.md +0 -51
- package/openspec/specs/settings/spec.md +0 -15
- package/openspec/specs/tag-management/spec.md +0 -39
- package/openspec/specs/tag-write/spec.md +0 -49
- package/openspec/specs/task-filtering/spec.md +0 -63
- package/openspec/specs/task-management/spec.md +0 -51
- package/openspec/specs/task-write/spec.md +0 -115
- package/openspec/specs/url-automation/spec.md +0 -15
- package/openspec/specs/window-state/spec.md +0 -15
- package/scripts/cleanup-fixtures.ts +0 -89
- package/server.json +0 -21
- package/src/runtime/bridge.ts +0 -97
- package/src/runtime/index.ts +0 -4
- package/src/runtime/jxaShim.ts +0 -55
- package/src/runtime/resultProtocol.ts +0 -62
- package/src/runtime/snippetLoader.ts +0 -79
- package/src/schemas/enums.ts +0 -32
- package/src/schemas/index.ts +0 -38
- package/src/schemas/shapes.ts +0 -267
- package/src/server.ts +0 -58
- package/src/tools/completeProject.ts +0 -21
- package/src/tools/completeTask.ts +0 -23
- package/src/tools/createFolder.ts +0 -20
- package/src/tools/createProject.ts +0 -20
- package/src/tools/createTag.ts +0 -20
- package/src/tools/createTask.ts +0 -20
- package/src/tools/deleteFolder.ts +0 -24
- package/src/tools/deleteProject.ts +0 -24
- package/src/tools/deleteTag.ts +0 -24
- package/src/tools/deleteTask.ts +0 -26
- package/src/tools/dropProject.ts +0 -21
- package/src/tools/dropTask.ts +0 -23
- package/src/tools/editFolder.ts +0 -19
- package/src/tools/editProject.ts +0 -20
- package/src/tools/editTag.ts +0 -20
- package/src/tools/editTask.ts +0 -20
- package/src/tools/getFolder.ts +0 -24
- package/src/tools/getProject.ts +0 -24
- package/src/tools/getTag.ts +0 -24
- package/src/tools/getTask.ts +0 -24
- package/src/tools/index.ts +0 -85
- package/src/tools/listFolders.ts +0 -32
- package/src/tools/listProjects.ts +0 -32
- package/src/tools/listTags.ts +0 -32
- package/src/tools/listTasks.ts +0 -56
- package/src/tools/moveProject.ts +0 -20
- package/src/tools/moveTask.ts +0 -20
- package/src/tools/resolveName.ts +0 -37
- package/test/integration/.gitkeep +0 -0
- package/test/integration/completeProject.int.test.ts +0 -25
- package/test/integration/completeTask.int.test.ts +0 -30
- package/test/integration/createFolder.int.test.ts +0 -50
- package/test/integration/createProject.int.test.ts +0 -49
- package/test/integration/createTag.int.test.ts +0 -52
- package/test/integration/createTask.int.test.ts +0 -55
- package/test/integration/deleteFolder.int.test.ts +0 -64
- package/test/integration/deleteProject.int.test.ts +0 -31
- package/test/integration/deleteTag.int.test.ts +0 -61
- package/test/integration/deleteTask.int.test.ts +0 -36
- package/test/integration/dropProject.int.test.ts +0 -24
- package/test/integration/dropTask.int.test.ts +0 -29
- package/test/integration/editFolder.int.test.ts +0 -43
- package/test/integration/editProject.int.test.ts +0 -39
- package/test/integration/editTag.int.test.ts +0 -43
- package/test/integration/editTask.int.test.ts +0 -56
- package/test/integration/fixtures.ts +0 -219
- package/test/integration/getTask.int.test.ts +0 -64
- package/test/integration/listFoldersFiltered.int.test.ts +0 -98
- package/test/integration/listProjects.int.test.ts +0 -73
- package/test/integration/listProjectsFiltered.int.test.ts +0 -96
- package/test/integration/listTagsFiltered.int.test.ts +0 -54
- package/test/integration/listTasksFiltered.int.test.ts +0 -141
- package/test/integration/moveProject.int.test.ts +0 -57
- package/test/integration/moveTask.int.test.ts +0 -61
- package/test/integration/plannedDate.int.test.ts +0 -72
- package/test/integration/preflight.ts +0 -60
- package/test/integration/resolveName.int.test.ts +0 -86
- package/test/integration/taskRecurrence.int.test.ts +0 -106
- package/test/unit/.gitkeep +0 -0
- package/test/unit/bridge.injection.test.ts +0 -66
- package/test/unit/resultProtocol.test.ts +0 -71
- package/test/unit/schemas.createFolder.test.ts +0 -38
- package/test/unit/schemas.createProject.test.ts +0 -115
- package/test/unit/schemas.createTask.test.ts +0 -87
- package/test/unit/schemas.editTag.test.ts +0 -64
- package/test/unit/schemas.folderTagFiltering.test.ts +0 -42
- package/test/unit/schemas.listProjects.test.ts +0 -44
- package/test/unit/schemas.moveOperations.test.ts +0 -60
- package/test/unit/schemas.recurrence.test.ts +0 -120
- package/test/unit/schemas.test.ts +0 -126
- package/test/unit/snippetLoader.test.ts +0 -56
- package/test/unit/tools.deleteTask.test.ts +0 -19
- package/test/unit/tools.listTasks.test.ts +0 -126
- package/tsconfig.json +0 -19
- package/vitest.config.ts +0 -8
- package/vitest.integration.config.ts +0 -18
package/openspec/changes/archive/2026-04-09-bootstrap-omnifocus-mcp/specs/execution-runtime/spec.md
DELETED
|
@@ -1,69 +0,0 @@
|
|
|
1
|
-
## ADDED Requirements
|
|
2
|
-
|
|
3
|
-
### Requirement: OmniJS execution via JXA bridge
|
|
4
|
-
|
|
5
|
-
The system SHALL execute every OmniFocus domain operation inside OmniFocus's OmniJS runtime by invoking `Application('OmniFocus').evaluateJavascript(snippet)` from a JXA host process spawned via `osascript -l JavaScript`. The JXA host SHALL NOT invoke any OmniFocus scripting-dictionary method other than `evaluateJavascript` for domain operations.
|
|
6
|
-
|
|
7
|
-
#### Scenario: Tool invocation routes through the bridge
|
|
8
|
-
- **WHEN** a tool handler invokes the runtime with a snippet and args
|
|
9
|
-
- **THEN** the runtime spawns `osascript -l JavaScript`, passes a JXA wrapper that calls `Application('OmniFocus').evaluateJavascript` with the prepared snippet, and awaits stdout
|
|
10
|
-
|
|
11
|
-
#### Scenario: Scripting-dictionary domain methods are forbidden
|
|
12
|
-
- **WHEN** a contributor adds a tool handler that calls e.g. `Application('OmniFocus').defaultDocument.projects` directly
|
|
13
|
-
- **THEN** a unit test (or review) flags the call as a runtime-contract violation and the change is rejected
|
|
14
|
-
|
|
15
|
-
### Requirement: Snippet argument injection via JSON literal
|
|
16
|
-
|
|
17
|
-
The system SHALL construct executable scripts by replacing exactly one `__ARGS__` placeholder in a static `.js` snippet template with the result of `JSON.stringify(args)`. No other interpolation of user-supplied data into script source is permitted anywhere in the codebase.
|
|
18
|
-
|
|
19
|
-
#### Scenario: Apostrophes and quotes survive injection
|
|
20
|
-
- **WHEN** a tool is invoked with `args = {name: "Finn's \"birthday\""}`
|
|
21
|
-
- **THEN** the executed snippet receives `args.name === "Finn's \"birthday\""` and no syntax error occurs
|
|
22
|
-
|
|
23
|
-
#### Scenario: Unicode and newlines survive injection
|
|
24
|
-
- **WHEN** a tool is invoked with `args = {note: "line1\nline2 — emoji 🎯"}`
|
|
25
|
-
- **THEN** the executed snippet receives the note verbatim including the newline and unicode code points
|
|
26
|
-
|
|
27
|
-
#### Scenario: Snippets are standalone and paste-ready
|
|
28
|
-
- **WHEN** a developer opens any file under `src/snippets/`
|
|
29
|
-
- **THEN** the file is a valid JavaScript program that can be pasted into OmniFocus's Automation Console after replacing `__ARGS__` with a hand-written object literal, with no additional preprocessing required
|
|
30
|
-
|
|
31
|
-
### Requirement: One-line JSON result protocol
|
|
32
|
-
|
|
33
|
-
The system SHALL represent every bridge invocation's result as exactly one line of JSON on the JXA host's stdout, matching the envelope `{ok: true, data: <value>} | {ok: false, error: {name: string, message: string, stack?: string}}`. The TypeScript runtime SHALL read stdout, extract the first JSON-parseable line, and return the parsed envelope to the caller. A result envelope with `ok: false` SHALL be thrown as a structured error with the error details preserved.
|
|
34
|
-
|
|
35
|
-
#### Scenario: Successful operation returns data envelope
|
|
36
|
-
- **WHEN** a snippet completes without throwing and its final expression is `JSON.stringify({ok: true, data: {id: "abc"}})`
|
|
37
|
-
- **THEN** the TS runtime receives `{ok: true, data: {id: "abc"}}` and returns `{id: "abc"}` to the tool handler
|
|
38
|
-
|
|
39
|
-
#### Scenario: Snippet exception produces error envelope
|
|
40
|
-
- **WHEN** a snippet throws an `Error` with message "not found"
|
|
41
|
-
- **THEN** the JXA shim catches the exception, prints `{ok: false, error: {name: "Error", message: "not found", ...}}` as one line on stdout, and the TS runtime throws a structured error carrying the name, message, and stack
|
|
42
|
-
|
|
43
|
-
#### Scenario: Extraneous stdout chatter does not corrupt results
|
|
44
|
-
- **WHEN** any process in the pipeline writes incidental non-JSON output to stdout before the result line
|
|
45
|
-
- **THEN** the TS runtime still parses the result envelope correctly by selecting the first complete JSON-parseable line
|
|
46
|
-
|
|
47
|
-
### Requirement: Snippet loader
|
|
48
|
-
|
|
49
|
-
The system SHALL load snippet templates from `src/snippets/<name>.js` relative to the compiled server's known snippet root, caching the file contents in memory after the first read. The loader SHALL reject snippets that do not contain exactly one `__ARGS__` token.
|
|
50
|
-
|
|
51
|
-
#### Scenario: Snippet is loaded and cached
|
|
52
|
-
- **WHEN** a tool handler invokes the runtime with snippet name `"list_projects"` twice in the same process
|
|
53
|
-
- **THEN** the file `list_projects.js` is read from disk exactly once and the cached content is used for the second invocation
|
|
54
|
-
|
|
55
|
-
#### Scenario: Snippet with zero placeholders is rejected
|
|
56
|
-
- **WHEN** a snippet file contains no `__ARGS__` token
|
|
57
|
-
- **THEN** the loader throws a contract violation at load time, not at runtime
|
|
58
|
-
|
|
59
|
-
#### Scenario: Snippet with multiple placeholders is rejected
|
|
60
|
-
- **WHEN** a snippet file contains two or more `__ARGS__` tokens
|
|
61
|
-
- **THEN** the loader throws a contract violation at load time
|
|
62
|
-
|
|
63
|
-
### Requirement: Script timeout
|
|
64
|
-
|
|
65
|
-
The system SHALL enforce a configurable per-invocation timeout on `osascript` execution, defaulting to 30 seconds, and SHALL terminate the child process and throw a timeout error when exceeded.
|
|
66
|
-
|
|
67
|
-
#### Scenario: Long-running snippet is terminated
|
|
68
|
-
- **WHEN** a snippet runs longer than the configured timeout
|
|
69
|
-
- **THEN** the runtime sends SIGTERM to the `osascript` process and throws a timeout error identifying the snippet name and elapsed time
|
package/openspec/changes/archive/2026-04-09-bootstrap-omnifocus-mcp/specs/folder-management/spec.md
DELETED
|
@@ -1,25 +0,0 @@
|
|
|
1
|
-
## ADDED Requirements
|
|
2
|
-
|
|
3
|
-
### Requirement: List folders
|
|
4
|
-
|
|
5
|
-
The system SHALL provide a `list_folders` tool that returns every folder in the OmniFocus database as an array of summaries, each containing at minimum `{id, name, path, parentId, status}`. The `path` field SHALL use the canonical ` ▸ ` separator and SHALL equal the folder's name for top-level folders. The `parentId` field SHALL be `null` for top-level folders. The `status` field SHALL be one of `"active" | "dropped"`.
|
|
6
|
-
|
|
7
|
-
#### Scenario: All folders are returned with full paths
|
|
8
|
-
- **WHEN** `list_folders` is called with no arguments
|
|
9
|
-
- **THEN** the tool returns every folder in the database including nested folders, each with its full ancestor path
|
|
10
|
-
|
|
11
|
-
#### Scenario: Top-level folders have null parent
|
|
12
|
-
- **WHEN** a folder exists at the root of the folder hierarchy
|
|
13
|
-
- **THEN** its summary carries `parentId: null` and `path` equal to its name
|
|
14
|
-
|
|
15
|
-
### Requirement: Get folder by ID
|
|
16
|
-
|
|
17
|
-
The system SHALL provide a `get_folder` tool that accepts `{id: string}` and returns the full detail record of the named folder, including `{id, name, path, parentId, status, childFolderIds, projectIds}`. If no folder exists with that ID, the tool SHALL return a structured not-found error.
|
|
18
|
-
|
|
19
|
-
#### Scenario: Existing folder returns full detail with children
|
|
20
|
-
- **WHEN** `get_folder` is called with the ID of an existing folder
|
|
21
|
-
- **THEN** the tool returns the folder's full detail including the IDs of its immediate child folders and immediate child projects
|
|
22
|
-
|
|
23
|
-
#### Scenario: Missing folder returns not-found error
|
|
24
|
-
- **WHEN** `get_folder` is called with an ID that does not correspond to any folder
|
|
25
|
-
- **THEN** the tool returns a structured error with a not-found code
|
|
@@ -1,9 +0,0 @@
|
|
|
1
|
-
## ADDED Requirements
|
|
2
|
-
|
|
3
|
-
### Requirement: Capability declared
|
|
4
|
-
|
|
5
|
-
The `forecast` capability SHALL cover OmniFocus Forecast data, including forecast days, tasks due or deferred on a given day, the configured forecast tag, and forecast badge counts. Requirements for individual tools SHALL be added by the `forecast-and-inspection` change.
|
|
6
|
-
|
|
7
|
-
#### Scenario: Capability is named and scoped
|
|
8
|
-
- **WHEN** a future change proposes adding forecast tools
|
|
9
|
-
- **THEN** it lands requirements under this capability rather than inventing a new capability name
|
|
@@ -1,45 +0,0 @@
|
|
|
1
|
-
## ADDED Requirements
|
|
2
|
-
|
|
3
|
-
### Requirement: Canonical ID addressing
|
|
4
|
-
|
|
5
|
-
The system SHALL use `id.primaryKey` as the canonical identifier for every addressable OmniFocus entity (Task, Project, Folder, Tag, Perspective). Every read tool SHALL return an `id` field carrying this value. Every write tool (in this and future changes) SHALL accept an `id` parameter as its primary addressing mode.
|
|
6
|
-
|
|
7
|
-
#### Scenario: Read tools return primary key
|
|
8
|
-
- **WHEN** a caller invokes `list_projects`
|
|
9
|
-
- **THEN** every returned element has an `id` field equal to the OmniJS expression `project.id.primaryKey` for that project
|
|
10
|
-
|
|
11
|
-
#### Scenario: IDs are stable within a session
|
|
12
|
-
- **WHEN** a caller reads a project's ID via `list_projects`, then reads the same project again via `get_project` using that ID
|
|
13
|
-
- **THEN** the second read succeeds and returns the same project, without re-resolving by name
|
|
14
|
-
|
|
15
|
-
### Requirement: Name and path resolution with ambiguity reporting
|
|
16
|
-
|
|
17
|
-
The system SHALL provide a `resolve_name` tool that accepts `{type, query, scope?}` and returns a list of candidate entities `[{id, name, path, type, ...}]`. When more than one entity matches the query, the tool SHALL return all matches and SHALL NOT silently pick a winner. The caller is responsible for disambiguating.
|
|
18
|
-
|
|
19
|
-
#### Scenario: Unique match returns single candidate
|
|
20
|
-
- **WHEN** `resolve_name` is called with `{type: "project", query: "Q4 Planning"}` and exactly one project has that name
|
|
21
|
-
- **THEN** the tool returns a list with one element containing the project's id, name, and full folder path
|
|
22
|
-
|
|
23
|
-
#### Scenario: Ambiguous match returns all candidates
|
|
24
|
-
- **WHEN** `resolve_name` is called with `{type: "project", query: "Inbox Cleanup"}` and two projects have that name under different folders
|
|
25
|
-
- **THEN** the tool returns a list with both candidates, each carrying a distinct id and distinct folder path
|
|
26
|
-
|
|
27
|
-
#### Scenario: No match returns empty list
|
|
28
|
-
- **WHEN** `resolve_name` is called with a query that matches no entity of the requested type
|
|
29
|
-
- **THEN** the tool returns an empty list and does not throw an error
|
|
30
|
-
|
|
31
|
-
#### Scenario: Path-qualified query narrows scope
|
|
32
|
-
- **WHEN** `resolve_name` is called with `{type: "folder", query: "Acme", scope: "Work ▸ Clients"}`
|
|
33
|
-
- **THEN** the tool returns only folders named "Acme" that are direct or transitive children of the folder at path "Work ▸ Clients"
|
|
34
|
-
|
|
35
|
-
### Requirement: Folder path addressing
|
|
36
|
-
|
|
37
|
-
The system SHALL represent folder paths as strings using the separator ` ▸ ` (U+25B8 with surrounding spaces) and SHALL support resolving a folder by its full path as an alternative to its ID. When a path is ambiguous (the same path exists under multiple roots, which cannot occur for folders but can for tags), the resolution SHALL report the ambiguity through `resolve_name`.
|
|
38
|
-
|
|
39
|
-
#### Scenario: Folder resolves by full path
|
|
40
|
-
- **WHEN** `resolve_name` is called with `{type: "folder", query: "Work ▸ Clients ▸ Acme"}`
|
|
41
|
-
- **THEN** the tool returns the folder whose full ancestor chain matches that path exactly
|
|
42
|
-
|
|
43
|
-
#### Scenario: Returned paths use the canonical separator
|
|
44
|
-
- **WHEN** any read tool returns a folder path or a project's containing folder path
|
|
45
|
-
- **THEN** the path uses ` ▸ ` as the separator between ancestor names
|
|
@@ -1,9 +0,0 @@
|
|
|
1
|
-
## ADDED Requirements
|
|
2
|
-
|
|
3
|
-
### Requirement: Capability declared
|
|
4
|
-
|
|
5
|
-
The `perspective-management` capability SHALL cover listing built-in and custom perspectives, reading perspective metadata, and activating a perspective in a document window. Custom perspective *creation and editing* are a non-goal pending verification of OmniJS scriptability of `Perspective.Custom` definitions; if a future investigation proves the API is available, the non-goal may be revisited by a later change. Requirements for individual tools SHALL be added by the `perspectives-and-windows` change.
|
|
6
|
-
|
|
7
|
-
#### Scenario: Capability is named and scoped
|
|
8
|
-
- **WHEN** a future change proposes adding perspective tools
|
|
9
|
-
- **THEN** it lands requirements under this capability rather than inventing a new capability name
|
package/openspec/changes/archive/2026-04-09-bootstrap-omnifocus-mcp/specs/project-management/spec.md
DELETED
|
@@ -1,25 +0,0 @@
|
|
|
1
|
-
## ADDED Requirements
|
|
2
|
-
|
|
3
|
-
### Requirement: List projects
|
|
4
|
-
|
|
5
|
-
The system SHALL provide a `list_projects` tool that returns every project in the OmniFocus database as an array of summaries, each containing at minimum `{id, name, folderPath, status, type}`. The `status` field SHALL be one of `"active" | "onHold" | "done" | "dropped"`. The `type` field SHALL be one of `"parallel" | "sequential" | "singleActions"`. The `folderPath` field SHALL use the canonical ` ▸ ` separator and SHALL be an empty string for projects at the top level with no containing folder.
|
|
6
|
-
|
|
7
|
-
#### Scenario: All projects are returned with canonical fields
|
|
8
|
-
- **WHEN** `list_projects` is called with no arguments
|
|
9
|
-
- **THEN** the tool returns every project in the database, each with id, name, folderPath, status, and type populated using the canonical enum values
|
|
10
|
-
|
|
11
|
-
#### Scenario: Single Actions list is reported with correct type
|
|
12
|
-
- **WHEN** the database contains a Single Actions list project
|
|
13
|
-
- **THEN** that project appears in the result with `type: "singleActions"`, never as `"parallel"` or `"sequential"`
|
|
14
|
-
|
|
15
|
-
### Requirement: Get project by ID
|
|
16
|
-
|
|
17
|
-
The system SHALL provide a `get_project` tool that accepts `{id: string}` and returns the full detail record of the named project, including `{id, name, note, folderPath, status, type, flagged, deferDate, dueDate, completionDate, reviewInterval, nextReviewDate, lastReviewDate, tagIds}`. If no project exists with that ID, the tool SHALL return a structured not-found error.
|
|
18
|
-
|
|
19
|
-
#### Scenario: Existing project returns full detail
|
|
20
|
-
- **WHEN** `get_project` is called with the ID of an existing project
|
|
21
|
-
- **THEN** the tool returns the project's full detail record
|
|
22
|
-
|
|
23
|
-
#### Scenario: Missing project returns not-found error
|
|
24
|
-
- **WHEN** `get_project` is called with an ID that does not correspond to any project
|
|
25
|
-
- **THEN** the tool returns a structured error with a not-found code
|
package/openspec/changes/archive/2026-04-09-bootstrap-omnifocus-mcp/specs/recurrence/spec.md
DELETED
|
@@ -1,9 +0,0 @@
|
|
|
1
|
-
## ADDED Requirements
|
|
2
|
-
|
|
3
|
-
### Requirement: Capability declared
|
|
4
|
-
|
|
5
|
-
The `recurrence` capability SHALL cover the construction, assignment, reading, and clearing of `Task.RepetitionRule` values on OmniFocus tasks, including the `RepetitionMethod` (`Fixed`, `DueDate`, `Start`, `None`) and the underlying ICS RRULE string. Tools under this capability SHALL expose both a structured schema for common recurrence patterns (frequency, interval, weekday set) and a raw `rrule` escape hatch for patterns the structured schema does not express. Requirements for individual tools SHALL be added by the `recurrence` change. This change only declares the capability and its scope.
|
|
6
|
-
|
|
7
|
-
#### Scenario: Capability is named and scoped
|
|
8
|
-
- **WHEN** a future change proposes adding recurrence-related tools
|
|
9
|
-
- **THEN** it lands requirements under this capability rather than inventing a new capability name
|
|
@@ -1,9 +0,0 @@
|
|
|
1
|
-
## ADDED Requirements
|
|
2
|
-
|
|
3
|
-
### Requirement: Capability declared
|
|
4
|
-
|
|
5
|
-
The `settings` capability SHALL cover reading OmniFocus application settings and preferences where the OmniJS API exposes them. Write access SHALL be limited to the subset of settings that OmniJS documents as writable; the specific writable keys SHALL be enumerated when the `settings` change is drafted. Requirements for individual tools SHALL be added by that change.
|
|
6
|
-
|
|
7
|
-
#### Scenario: Capability is named and scoped
|
|
8
|
-
- **WHEN** a future change proposes adding settings tools
|
|
9
|
-
- **THEN** it lands requirements under this capability, with an explicit enumeration of writable keys vs read-only keys
|
package/openspec/changes/archive/2026-04-09-bootstrap-omnifocus-mcp/specs/tag-management/spec.md
DELETED
|
@@ -1,25 +0,0 @@
|
|
|
1
|
-
## ADDED Requirements
|
|
2
|
-
|
|
3
|
-
### Requirement: List tags
|
|
4
|
-
|
|
5
|
-
The system SHALL provide a `list_tags` tool that returns every tag in the OmniFocus database as an array of summaries, each containing at minimum `{id, name, path, parentId, status}`. Tags form a tree in OmniFocus; the `path` field SHALL use the canonical ` ▸ ` separator and represent the full ancestor chain. The `parentId` field SHALL be `null` for top-level tags. The `status` field SHALL be one of `"active" | "onHold" | "dropped"`.
|
|
6
|
-
|
|
7
|
-
#### Scenario: All tags are returned including nested tags
|
|
8
|
-
- **WHEN** `list_tags` is called with no arguments
|
|
9
|
-
- **THEN** the tool returns every tag in the database, top-level and nested, each with its full path and correct parentId
|
|
10
|
-
|
|
11
|
-
#### Scenario: On-hold tag is reported with correct status
|
|
12
|
-
- **WHEN** the database contains a tag that has been placed on hold
|
|
13
|
-
- **THEN** that tag appears in the result with `status: "onHold"`
|
|
14
|
-
|
|
15
|
-
### Requirement: Get tag by ID
|
|
16
|
-
|
|
17
|
-
The system SHALL provide a `get_tag` tool that accepts `{id: string}` and returns the full detail record of the named tag, including `{id, name, path, parentId, status, childTagIds}`. If no tag exists with that ID, the tool SHALL return a structured not-found error.
|
|
18
|
-
|
|
19
|
-
#### Scenario: Existing tag returns full detail
|
|
20
|
-
- **WHEN** `get_tag` is called with the ID of an existing tag
|
|
21
|
-
- **THEN** the tool returns the tag's full detail including the IDs of its immediate child tags
|
|
22
|
-
|
|
23
|
-
#### Scenario: Missing tag returns not-found error
|
|
24
|
-
- **WHEN** `get_tag` is called with an ID that does not correspond to any tag
|
|
25
|
-
- **THEN** the tool returns a structured error with a not-found code
|
package/openspec/changes/archive/2026-04-09-bootstrap-omnifocus-mcp/specs/task-management/spec.md
DELETED
|
@@ -1,29 +0,0 @@
|
|
|
1
|
-
## ADDED Requirements
|
|
2
|
-
|
|
3
|
-
### Requirement: List tasks with scope filter
|
|
4
|
-
|
|
5
|
-
The system SHALL provide a `list_tasks` tool that returns tasks within a caller-specified scope. The scope SHALL be one of: `{projectId: string}`, `{folderId: string}` (all tasks in projects under that folder, recursively), `{inbox: true}`, or `{all: true}` (the full flattened task list). The tool SHALL return an array of task summaries, each containing at minimum `{id, name, status, flagged, containerId, containerType}`.
|
|
6
|
-
|
|
7
|
-
#### Scenario: List tasks in a specific project
|
|
8
|
-
- **WHEN** `list_tasks` is called with `{projectId: "abc123"}`
|
|
9
|
-
- **THEN** the tool returns every task directly or transitively contained in that project, with each element carrying the project id as `containerId` and `"project"` as `containerType`
|
|
10
|
-
|
|
11
|
-
#### Scenario: List inbox tasks
|
|
12
|
-
- **WHEN** `list_tasks` is called with `{inbox: true}`
|
|
13
|
-
- **THEN** the tool returns every task in the OmniFocus inbox with `containerType` set to `"inbox"`
|
|
14
|
-
|
|
15
|
-
#### Scenario: Invalid scope is rejected at the TS boundary
|
|
16
|
-
- **WHEN** `list_tasks` is called with `{projectId: "abc", inbox: true}` (mutually exclusive scopes)
|
|
17
|
-
- **THEN** the tool returns a validation error before any snippet executes
|
|
18
|
-
|
|
19
|
-
### Requirement: Get task by ID
|
|
20
|
-
|
|
21
|
-
The system SHALL provide a `get_task` tool that accepts `{id: string}` and returns the full detail record of the named task, including `{id, name, note, status, flagged, deferDate, dueDate, completionDate, estimatedMinutes, containerId, containerType, tagIds}`. If no task exists with that ID, the tool SHALL return a structured not-found error.
|
|
22
|
-
|
|
23
|
-
#### Scenario: Existing task returns full detail
|
|
24
|
-
- **WHEN** `get_task` is called with the ID of an existing task
|
|
25
|
-
- **THEN** the tool returns the task's full detail record including all scalar fields and the list of tag IDs assigned to it
|
|
26
|
-
|
|
27
|
-
#### Scenario: Missing task returns not-found error
|
|
28
|
-
- **WHEN** `get_task` is called with an ID that does not correspond to any task
|
|
29
|
-
- **THEN** the tool returns a structured error with a not-found code and does not throw an unhandled exception
|
package/openspec/changes/archive/2026-04-09-bootstrap-omnifocus-mcp/specs/url-automation/spec.md
DELETED
|
@@ -1,9 +0,0 @@
|
|
|
1
|
-
## ADDED Requirements
|
|
2
|
-
|
|
3
|
-
### Requirement: Capability declared
|
|
4
|
-
|
|
5
|
-
The `url-automation` capability SHALL cover `omnifocus://` URL construction (including task-paste format for bulk creation, add-to-inbox URLs, and deep links to entities by ID) and parsing of incoming `omnifocus://` URLs into structured form. Requirements for individual tools SHALL be added by the `url-automation` change.
|
|
6
|
-
|
|
7
|
-
#### Scenario: Capability is named and scoped
|
|
8
|
-
- **WHEN** a future change proposes adding URL-automation tools
|
|
9
|
-
- **THEN** it lands requirements under this capability rather than inventing a new capability name
|
package/openspec/changes/archive/2026-04-09-bootstrap-omnifocus-mcp/specs/window-state/spec.md
DELETED
|
@@ -1,9 +0,0 @@
|
|
|
1
|
-
## ADDED Requirements
|
|
2
|
-
|
|
3
|
-
### Requirement: Capability declared
|
|
4
|
-
|
|
5
|
-
The `window-state` capability SHALL cover read-only inspection of OmniFocus document window state, including the currently active window, active perspective, sidebar selection, and content selection. Window *mutation* (resize, close, focus, open) is an explicit non-goal. Requirements for individual tools SHALL be added by the `perspectives-and-windows` change.
|
|
6
|
-
|
|
7
|
-
#### Scenario: Capability is named and read-only scope is fixed
|
|
8
|
-
- **WHEN** a future change proposes adding window-state tools
|
|
9
|
-
- **THEN** it lands read-only requirements under this capability; any mutation proposal requires first revisiting the non-goal in design
|
|
@@ -1,84 +0,0 @@
|
|
|
1
|
-
## 1. Repository scaffold
|
|
2
|
-
|
|
3
|
-
- [x] 1.1 Create `package.json` with dependencies: `@modelcontextprotocol/sdk`, `zod`; devDependencies: `typescript`, `vitest`, `@types/node`, `tsx`
|
|
4
|
-
- [x] 1.2 Create `tsconfig.json` targeting Node 20, strict mode, `moduleResolution: "bundler"`, `outDir: "dist"`, sourcemaps on
|
|
5
|
-
- [x] 1.3 Create directory structure: `src/runtime/`, `src/snippets/`, `src/tools/`, `src/schemas/`, `test/unit/`, `test/integration/`
|
|
6
|
-
- [x] 1.4 Add `.gitignore` covering `node_modules`, `dist`, `*.log`, macOS cruft
|
|
7
|
-
- [x] 1.5 Add root `README.md` stub with macOS-only note and integration-test warning about sync
|
|
8
|
-
|
|
9
|
-
## 2. Execution runtime (src/runtime/)
|
|
10
|
-
|
|
11
|
-
- [x] 2.1 Implement `snippetLoader.ts`: reads `src/snippets/<name>.js` from the resolved snippet root, caches in memory, validates exactly one `__ARGS__` token, throws on zero or multiple
|
|
12
|
-
- [x] 2.2 Implement `resultProtocol.ts`: zod schema for the `{ok, data} | {ok: false, error}` envelope, `parseResultLine(stdout: string)` that selects the first JSON-parseable line and validates the envelope, `ExecutionError` class carrying name/message/stack from the error branch
|
|
13
|
-
- [x] 2.3 Implement `jxaShim.ts`: a template string containing the JXA wrapper that calls `Application('OmniFocus').evaluateJavascript(snippet)`, wraps in try/catch, prints the envelope as one JSON line
|
|
14
|
-
- [x] 2.4 Implement `bridge.ts`: `runSnippet(name: string, args: unknown, opts?: {timeoutMs?: number}): Promise<unknown>` — loads the snippet via the loader, injects args via `template.replace("__ARGS__", JSON.stringify(args))`, wraps the result in the JXA shim, spawns `osascript -l JavaScript`, enforces timeout via `AbortController` + SIGTERM, parses stdout via `parseResultLine`, throws `ExecutionError` on `ok: false`, returns `data` on success
|
|
15
|
-
- [x] 2.5 Export a single `runtime` barrel from `src/runtime/index.ts`
|
|
16
|
-
|
|
17
|
-
## 3. Shared schemas (src/schemas/)
|
|
18
|
-
|
|
19
|
-
- [x] 3.1 Define zod schemas: `IdSchema` (non-empty string), `EntityType` enum (`"task" | "project" | "folder" | "tag" | "perspective"`), `ProjectType` enum, `ProjectStatus` enum, `TaskStatus` enum, `TagStatus` enum, `FolderStatus` enum
|
|
20
|
-
- [x] 3.2 Define shared return-shape schemas: `TaskSummary`, `TaskDetail`, `ProjectSummary`, `ProjectDetail`, `FolderSummary`, `FolderDetail`, `TagSummary`, `TagDetail`, `ResolveCandidate`
|
|
21
|
-
- [x] 3.3 Export a `schemas` barrel from `src/schemas/index.ts`
|
|
22
|
-
|
|
23
|
-
## 4. Snippets (src/snippets/*.js) — read-only set for this change
|
|
24
|
-
|
|
25
|
-
- [x] 4.1 `list_projects.js`: iterate `flattenedProjects`, map to summary shape with `id.primaryKey`, canonical `status`/`type` enum values, folder path via ` ▸ ` separator; return `JSON.stringify({ok: true, data: [...]})`
|
|
26
|
-
- [x] 4.2 `get_project.js`: resolve project by `args.id` via `Project.byIdentifier` (or equivalent), build detail shape including review metadata and tag IDs, return envelope; throw on not-found with a `NotFoundError` constructor defined at the top of the snippet
|
|
27
|
-
- [x] 4.3 `list_folders.js`: iterate `flattenedFolders`, map to summary with full path; return envelope
|
|
28
|
-
- [x] 4.4 `get_folder.js`: resolve folder by ID, return detail including child folder IDs and immediate project IDs
|
|
29
|
-
- [x] 4.5 `list_tasks.js`: accept `args.scope` discriminated union (`projectId` / `folderId` / `inbox` / `all`), iterate the correct source, map to summary; return envelope
|
|
30
|
-
- [x] 4.6 `get_task.js`: resolve task by ID, return full detail including tag IDs
|
|
31
|
-
- [x] 4.7 `list_tags.js`: iterate `flattenedTags`, map to summary with full path, parentId, status
|
|
32
|
-
- [x] 4.8 `get_tag.js`: resolve tag by ID, return detail including child tag IDs
|
|
33
|
-
- [x] 4.9 `resolve_name.js`: accept `args.type`, `args.query`, optional `args.scope`; walk the relevant flat list, match by exact name (and path suffix if scope given), return array of candidates with `{id, name, path, type}`; never throw on zero matches
|
|
34
|
-
- [x] 4.10 Add a shared helper comment block at the top of each snippet documenting the paste-to-console procedure
|
|
35
|
-
|
|
36
|
-
## 5. MCP tools (src/tools/)
|
|
37
|
-
|
|
38
|
-
- [x] 5.1 `listProjects.ts`: zod input schema (empty object), handler invokes `runSnippet("list_projects", {})`, validates output against `z.array(ProjectSummary)`, returns
|
|
39
|
-
- [x] 5.2 `getProject.ts`: input `{id}`, handler invokes `runSnippet("get_project", {id})`, validates `ProjectDetail`
|
|
40
|
-
- [x] 5.3 `listFolders.ts`: empty input, returns `z.array(FolderSummary)`
|
|
41
|
-
- [x] 5.4 `getFolder.ts`: input `{id}`, returns `FolderDetail`
|
|
42
|
-
- [x] 5.5 `listTasks.ts`: input discriminated union (`{projectId} | {folderId} | {inbox: true} | {all: true}`) with zod `discriminatedUnion` or refinement rejecting mutual exclusivity, returns `z.array(TaskSummary)`
|
|
43
|
-
- [x] 5.6 `getTask.ts`: input `{id}`, returns `TaskDetail`
|
|
44
|
-
- [x] 5.7 `listTags.ts`: empty input, returns `z.array(TagSummary)`
|
|
45
|
-
- [x] 5.8 `getTag.ts`: input `{id}`, returns `TagDetail`
|
|
46
|
-
- [x] 5.9 `resolveName.ts`: input `{type, query, scope?}`, returns `z.array(ResolveCandidate)`
|
|
47
|
-
- [x] 5.10 Export a `tools` barrel from `src/tools/index.ts` listing all nine tool definitions
|
|
48
|
-
|
|
49
|
-
## 6. MCP server entrypoint
|
|
50
|
-
|
|
51
|
-
- [x] 6.1 `src/server.ts`: construct an `@modelcontextprotocol/sdk` server, register all nine tools from the barrel, wire stdio transport, start listening
|
|
52
|
-
- [x] 6.2 Add `bin` field to `package.json` pointing at the compiled entrypoint and a `scripts.start` for development via `tsx`
|
|
53
|
-
|
|
54
|
-
## 7. Unit tests (test/unit/)
|
|
55
|
-
|
|
56
|
-
- [x] 7.1 `snippetLoader.test.ts`: golden-file test that loading each snippet in `src/snippets/` succeeds and contains exactly one `__ARGS__`; contract-violation tests for zero/multiple placeholders (using fixture files)
|
|
57
|
-
- [x] 7.2 `resultProtocol.test.ts`: parses well-formed success envelope, parses well-formed error envelope, rejects malformed JSON, selects first parseable line when preceded by chatter
|
|
58
|
-
- [x] 7.3 `bridge.injection.test.ts`: given a snippet template and args containing apostrophes, quotes, backslashes, newlines, and emoji, assert the generated script source is valid JavaScript (parse via `new Function`) and that evaluating it recovers the original args object unchanged — proves Decision 2 structurally
|
|
59
|
-
- [x] 7.4 `schemas.test.ts`: round-trip validation for each shared schema; rejection tests for the `sequential`-as-string and `items`-as-JSON-string regression patterns at the boundary
|
|
60
|
-
- [x] 7.5 `tools.listTasks.test.ts`: rejects mutually exclusive scope combinations at the zod boundary before any bridge call
|
|
61
|
-
|
|
62
|
-
## 8. Integration test harness (test/integration/)
|
|
63
|
-
|
|
64
|
-
- [x] 8.1 `fixtures.ts`: `createTestFolder()` generates `__MCP_TEST_<uuid>__` top-level folder via the bridge; `cleanupTestFolder(id)` removes it; `withTestFolder(fn)` helper wraps a test body
|
|
65
|
-
- [x] 8.2 `preflight.ts`: detects OmniFocus sync status (via a read-only snippet that inspects the relevant setting); throws unless `MCP_TEST_ALLOW_SYNC=1` is set; invoked once in the vitest global setup
|
|
66
|
-
- [x] 8.3 `vitest.integration.config.ts`: separate vitest config for integration with `pool: "forks"`, `maxConcurrency: 1`, `globalSetup: ./preflight.ts`, `testMatch: test/integration/**`
|
|
67
|
-
- [x] 8.4 Integration test: `listProjects.int.test.ts` creates a fixture folder with a child project, calls the real bridge, asserts the fixture project appears in the result with the correct folder path
|
|
68
|
-
- [x] 8.5 Integration test: `getTask.int.test.ts` creates a fixture task with an apostrophe and unicode in its name, retrieves by ID, asserts name survives round-trip — proves Decision 2 end-to-end
|
|
69
|
-
- [x] 8.6 Integration test: `resolveName.int.test.ts` creates two fixture projects with identical names under different folders, asserts `resolve_name` returns both candidates with distinct paths
|
|
70
|
-
- [x] 8.7 Add a `scripts.test:integration` in `package.json` guarded by macOS check
|
|
71
|
-
- [x] 8.8 Add a `scripts.test:cleanup-fixtures` that scans for stale `__MCP_TEST_*` folders and removes them
|
|
72
|
-
|
|
73
|
-
## 9. Documentation
|
|
74
|
-
|
|
75
|
-
- [x] 9.1 Expand `README.md` with: prerequisites (macOS, OmniFocus running), install, MCP client configuration snippet, the list of tools delivered in this change, the loud warning about integration tests mutating real data and the sync preflight
|
|
76
|
-
- [x] 9.2 Add `CONTRIBUTING.md` documenting the snippet authoring rules (the `__ARGS__` placeholder, no other interpolation, paste-to-console workflow) and the forbidden-scripting-dictionary rule from execution-runtime spec
|
|
77
|
-
|
|
78
|
-
## 10. Verification
|
|
79
|
-
|
|
80
|
-
- [x] 10.1 Run `npm run typecheck` (or `tsc --noEmit`) clean
|
|
81
|
-
- [x] 10.2 Run `npm run test` (unit suite) clean
|
|
82
|
-
- [x] 10.3 Manually run integration suite against a real OmniFocus with sync disabled; verify fixture folder is created and torn down
|
|
83
|
-
- [x] 10.4 Manually connect the server to an MCP client (Claude Desktop or equivalent) and invoke each of the nine tools against a real database; verify results match the spec scenarios
|
|
84
|
-
- [x] 10.5 Confirm all spec scenarios in `specs/execution-runtime/spec.md` and `specs/identity-resolution/spec.md` have corresponding test coverage (unit + integration)
|
|
@@ -1,58 +0,0 @@
|
|
|
1
|
-
## Context
|
|
2
|
-
|
|
3
|
-
Folders in OmniJS are simpler than tasks or projects — they have only `name` and `status` as meaningful writable properties, and they nest via parent/child relationships. The main complexity is deletion: `deleteObject(folder)` leaves an empty shell (as discovered during bootstrap integration testing); the snippet must recursively delete contents first.
|
|
4
|
-
|
|
5
|
-
OmniJS folder write operations:
|
|
6
|
-
- `new Folder(name)` — creates a top-level folder (auto-inserted at root)
|
|
7
|
-
- `new Folder(name, folderObject)` — creates a nested folder inside `folderObject`
|
|
8
|
-
- `folder.name = "..."` — rename
|
|
9
|
-
- `deleteObject(folder)` — removes folder shell but NOT contents; must delete contents first
|
|
10
|
-
- Recursive deletion pattern (established in bootstrap fixtures): delete all projects in folder, recurse child folders, then delete the folder itself
|
|
11
|
-
|
|
12
|
-
## Goals / Non-Goals
|
|
13
|
-
|
|
14
|
-
**Goals:**
|
|
15
|
-
- Create top-level and nested folders
|
|
16
|
-
- Rename folders
|
|
17
|
-
- Delete folders with full recursive cascade (projects, tasks, child folders)
|
|
18
|
-
|
|
19
|
-
**Non-Goals:**
|
|
20
|
-
- Moving a folder to a different parent (separate `move-operations` change)
|
|
21
|
-
- Changing folder status directly (OmniFocus manages active/dropped based on contents)
|
|
22
|
-
|
|
23
|
-
## Decisions
|
|
24
|
-
|
|
25
|
-
### Decision 1: Recursive deletion in the snippet
|
|
26
|
-
|
|
27
|
-
The snippet for `delete_folder` must implement recursive deletion rather than relying on `deleteObject` to cascade. The pattern is:
|
|
28
|
-
|
|
29
|
-
```
|
|
30
|
-
function deleteFolder(f) {
|
|
31
|
-
f.flattenedProjects.forEach(p => deleteObject(p));
|
|
32
|
-
f.folders.forEach(child => deleteFolder(child));
|
|
33
|
-
deleteObject(f);
|
|
34
|
-
}
|
|
35
|
-
```
|
|
36
|
-
|
|
37
|
-
This was validated during bootstrap integration testing (`fixtures.ts` uses this exact pattern).
|
|
38
|
-
|
|
39
|
-
### Decision 2: `create_folder` placement via optional `parentFolderId`
|
|
40
|
-
|
|
41
|
-
- `parentFolderId` omitted → top-level folder
|
|
42
|
-
- `parentFolderId` provided → nested inside that folder
|
|
43
|
-
|
|
44
|
-
`new Folder(name)` for top-level; `new Folder(name, parentFolder)` for nested. Snippet resolves the parent by ID.
|
|
45
|
-
|
|
46
|
-
### Decision 3: No `edit_folder` status field
|
|
47
|
-
|
|
48
|
-
Folder status (`active` / `dropped`) in OmniFocus reflects whether the folder has active contents — it is not directly user-settable in the same way as project status. `edit_folder` exposes only `name`. If this proves insufficient in practice it can be extended.
|
|
49
|
-
|
|
50
|
-
### Decision 4: `delete_folder` tool description is the strongest warning in the server
|
|
51
|
-
|
|
52
|
-
This is the most destructive single operation: one call can delete an entire project hierarchy. The tool description must explicitly state that all child folders, projects, and tasks are permanently deleted, and that the AI must confirm with the user before calling it.
|
|
53
|
-
|
|
54
|
-
## Risks / Trade-offs
|
|
55
|
-
|
|
56
|
-
- **`deleteObject` on folder leaves shell** — confirmed behavior; recursive snippet pattern mitigates this fully.
|
|
57
|
-
- **Deleting a folder containing hundreds of projects** — could be slow inside `evaluateJavascript`; the 30s timeout applies. Not worth special-casing in v1.
|
|
58
|
-
- **Race between read and delete** — if the user reads a folder ID then calls delete, OmniFocus has no transaction isolation. Acceptable for a single-user desktop app.
|
|
@@ -1,28 +0,0 @@
|
|
|
1
|
-
## Why
|
|
2
|
-
|
|
3
|
-
The bootstrap change delivered read-only folder access. Callers can traverse the folder hierarchy but cannot create, rename, or delete folders — blocking any workflow that involves organizing projects into folders.
|
|
4
|
-
|
|
5
|
-
## What Changes
|
|
6
|
-
|
|
7
|
-
- Add `create_folder` tool: create a folder at the top level or nested inside an existing folder
|
|
8
|
-
- Add `edit_folder` tool: rename a folder
|
|
9
|
-
- Add `delete_folder` tool: permanently delete a folder and all its contents — projects, tasks, and child folders (tool description instructs the AI to confirm with the user before invoking and calls out the destructive cascade)
|
|
10
|
-
|
|
11
|
-
## Capabilities
|
|
12
|
-
|
|
13
|
-
### New Capabilities
|
|
14
|
-
|
|
15
|
-
- `folder-write`: Create, rename, and permanently delete OmniFocus folders
|
|
16
|
-
|
|
17
|
-
### Modified Capabilities
|
|
18
|
-
|
|
19
|
-
_(none — existing `folder-management` read requirements are unchanged)_
|
|
20
|
-
|
|
21
|
-
## Impact
|
|
22
|
-
|
|
23
|
-
- 3 new MCP tools registered in `src/server.ts`
|
|
24
|
-
- 3 new tool handler files in `src/tools/`
|
|
25
|
-
- 3 new OmniJS snippets in `src/snippets/`
|
|
26
|
-
- `ALLOWED_SNIPPETS` allowlist in `src/runtime/snippetLoader.ts` must be extended
|
|
27
|
-
- Integration tests will mutate real OmniFocus data (scoped to fixture folder per existing pattern)
|
|
28
|
-
- `delete_folder` is the most destructive single operation in the server — the tool description warrants an explicit warning that the entire subtree (child folders, projects, and all tasks) is permanently removed
|
|
@@ -1,45 +0,0 @@
|
|
|
1
|
-
## ADDED Requirements
|
|
2
|
-
|
|
3
|
-
### Requirement: Create folder
|
|
4
|
-
|
|
5
|
-
The system SHALL provide a `create_folder` tool that creates a new OmniFocus folder and returns its full detail record. The tool SHALL accept `{name: string, parentFolderId?: string}`. If `parentFolderId` is provided the folder SHALL be created nested inside that folder; otherwise it SHALL be created at the top level.
|
|
6
|
-
|
|
7
|
-
#### Scenario: Create top-level folder
|
|
8
|
-
- **WHEN** `create_folder` is called with `{name: "Work"}` and no `parentFolderId`
|
|
9
|
-
- **THEN** the tool creates the folder at the top level and returns its full detail record including a stable `id`
|
|
10
|
-
|
|
11
|
-
#### Scenario: Create nested folder
|
|
12
|
-
- **WHEN** `create_folder` is called with `{name: "Active", parentFolderId: "abc123"}`
|
|
13
|
-
- **THEN** the tool creates the folder inside the specified parent folder and returns its full detail record with `path` reflecting the full ancestor chain
|
|
14
|
-
|
|
15
|
-
#### Scenario: Non-existent parent folder returns not-found error
|
|
16
|
-
- **WHEN** `create_folder` is called with a `parentFolderId` that does not correspond to any folder
|
|
17
|
-
- **THEN** the tool returns a structured not-found error
|
|
18
|
-
|
|
19
|
-
### Requirement: Edit folder
|
|
20
|
-
|
|
21
|
-
The system SHALL provide an `edit_folder` tool that renames an existing folder and returns its updated full detail record. The tool SHALL accept `{id: string, name: string}`.
|
|
22
|
-
|
|
23
|
-
#### Scenario: Rename a folder
|
|
24
|
-
- **WHEN** `edit_folder` is called with `{id: "abc123", name: "Personal"}`
|
|
25
|
-
- **THEN** the folder's name is updated and the tool returns the updated detail record with the new name reflected in `path`
|
|
26
|
-
|
|
27
|
-
#### Scenario: Non-existent folder returns not-found error
|
|
28
|
-
- **WHEN** `edit_folder` is called with an ID that does not correspond to any folder
|
|
29
|
-
- **THEN** the tool returns a structured not-found error
|
|
30
|
-
|
|
31
|
-
### Requirement: Delete folder
|
|
32
|
-
|
|
33
|
-
The system SHALL provide a `delete_folder` tool that permanently and recursively deletes a folder, all child folders, all projects within those folders, and all tasks within those projects. The tool description SHALL instruct the AI to confirm with the user before invoking, explicitly stating that the entire subtree — child folders, projects, and all tasks — is permanently deleted and cannot be undone.
|
|
34
|
-
|
|
35
|
-
#### Scenario: Delete a folder and all its contents
|
|
36
|
-
- **WHEN** `delete_folder` is called with the ID of an existing folder
|
|
37
|
-
- **THEN** the folder, all descendant folders, all projects within those folders, and all tasks within those projects are permanently removed from OmniFocus and the tool returns a confirmation envelope
|
|
38
|
-
|
|
39
|
-
#### Scenario: Delete empty folder
|
|
40
|
-
- **WHEN** `delete_folder` is called with the ID of a folder that contains no projects or child folders
|
|
41
|
-
- **THEN** the folder is removed and the tool returns a confirmation envelope
|
|
42
|
-
|
|
43
|
-
#### Scenario: Non-existent folder returns not-found error
|
|
44
|
-
- **WHEN** `delete_folder` is called with an ID that does not correspond to any folder
|
|
45
|
-
- **THEN** the tool returns a structured not-found error
|
|
@@ -1,41 +0,0 @@
|
|
|
1
|
-
## 1. Snippets (src/snippets/)
|
|
2
|
-
|
|
3
|
-
- [x] 1.1 `create_folder.js`: accept `{name, parentFolderId?}`; if `parentFolderId` provided resolve parent by ID and throw `NotFoundError` if missing; create `new Folder(name)` or `new Folder(name, parentFolder)`; return full folder detail envelope
|
|
4
|
-
- [x] 1.2 `edit_folder.js`: accept `{id, name}`; resolve folder by ID; assign `folder.name = name`; return updated full folder detail envelope
|
|
5
|
-
- [x] 1.3 `delete_folder.js`: accept `{id}`; resolve folder by ID; recursively delete contents (projects then child folders) then `deleteObject(folder)`; return `{ok: true, data: {id}}`
|
|
6
|
-
|
|
7
|
-
## 2. Snippet allowlist
|
|
8
|
-
|
|
9
|
-
- [x] 2.1 Add `create_folder`, `edit_folder`, `delete_folder` to `ALLOWED_SNIPPETS` in `src/runtime/snippetLoader.ts`
|
|
10
|
-
|
|
11
|
-
## 3. Schemas (src/schemas/shapes.ts)
|
|
12
|
-
|
|
13
|
-
- [x] 3.1 Define `CreateFolderInput` zod schema: `{name: z.string().min(1), parentFolderId: IdSchema.optional()}`
|
|
14
|
-
- [x] 3.2 Define `EditFolderInput` zod schema: `{id: IdSchema, name: z.string().min(1)}`
|
|
15
|
-
|
|
16
|
-
## 4. Tool handlers (src/tools/)
|
|
17
|
-
|
|
18
|
-
- [x] 4.1 `createFolder.ts`: validate input with `CreateFolderInput`; invoke `runSnippet("create_folder", args)`; validate result against `FolderDetail`; return
|
|
19
|
-
- [x] 4.2 `editFolder.ts`: validate input with `EditFolderInput`; invoke `runSnippet("edit_folder", args)`; validate result against `FolderDetail`; return
|
|
20
|
-
- [x] 4.3 `deleteFolder.ts`: input `{id: IdSchema}`; tool description MUST state the AI should confirm with the user before invoking and explicitly note that the entire subtree — child folders, projects, and all tasks — is permanently deleted; invoke `runSnippet("delete_folder", {id})`; return confirmation
|
|
21
|
-
|
|
22
|
-
## 5. Server registration
|
|
23
|
-
|
|
24
|
-
- [x] 5.1 Export all three new tool definitions from `src/tools/index.ts`
|
|
25
|
-
|
|
26
|
-
## 6. Unit tests (test/unit/)
|
|
27
|
-
|
|
28
|
-
- [x] 6.1 `schemas.createFolder.test.ts`: valid inputs pass; empty name rejected
|
|
29
|
-
|
|
30
|
-
## 7. Integration tests (test/integration/)
|
|
31
|
-
|
|
32
|
-
- [x] 7.1 `createFolder.int.test.ts`: create top-level folder; create nested folder; verify `path` and `parentId`
|
|
33
|
-
- [x] 7.2 `editFolder.int.test.ts`: rename a folder; verify `name` and `path` updated
|
|
34
|
-
- [x] 7.3 `deleteFolder.int.test.ts`: create folder with child project and tasks; delete folder; verify `get_folder` returns not-found and projects are gone
|
|
35
|
-
|
|
36
|
-
## 8. Verification
|
|
37
|
-
|
|
38
|
-
- [x] 8.1 `npm run typecheck` clean
|
|
39
|
-
- [x] 8.2 `npm test` (unit suite) clean
|
|
40
|
-
- [x] 8.3 Manually run integration suite; verify fixture cleanup
|
|
41
|
-
- [ ] 8.4 Connect to Claude Desktop; exercise all three tools against a real database
|
|
@@ -1,38 +0,0 @@
|
|
|
1
|
-
## Context
|
|
2
|
-
|
|
3
|
-
`list_folders` and `list_tags` are the last two list tools without filtering or limit support. The pattern established by `list_tasks` and `list_projects` — filter inside the OmniJS snippet, expose filter object and limit from the TypeScript handler — applies directly.
|
|
4
|
-
|
|
5
|
-
Unlike tasks and projects, there is no universally agreed "terminal" status for folders (dropped folders are unusual but valid to show) or tags (onHold tags are actively used). Therefore the default behavior includes all statuses; filtering is opt-in.
|
|
6
|
-
|
|
7
|
-
## Goals / Non-Goals
|
|
8
|
-
|
|
9
|
-
**Goals:**
|
|
10
|
-
- `list_folders`: add `limit` (default 200) and optional `status` filter (`active` | `dropped`)
|
|
11
|
-
- `list_tags`: add `limit` (default 200) and optional `status` filter (`active` | `onHold` | `dropped`)
|
|
12
|
-
|
|
13
|
-
**Non-Goals:**
|
|
14
|
-
- `parentId` / subtree scoping for folders or tags (flat list is sufficient in practice)
|
|
15
|
-
- Filtering by name or path pattern
|
|
16
|
-
- Pagination
|
|
17
|
-
|
|
18
|
-
## Decisions
|
|
19
|
-
|
|
20
|
-
### Decision 1: No default status exclusion
|
|
21
|
-
|
|
22
|
-
Unlike `list_tasks` and `list_projects`, the default returns all statuses. Dropped folders/tags are uncommon enough that including them doesn't overwhelm results, and an LLM may need to see them. Callers can pass `status: ["active"]` to filter.
|
|
23
|
-
|
|
24
|
-
### Decision 2: Single status value, not array
|
|
25
|
-
|
|
26
|
-
For `list_folders` and `list_tags`, a single `status` string (not an array) is sufficient — users rarely need "active OR onHold" for tags. Simpler API. If multi-status becomes needed, it's a non-breaking addition later.
|
|
27
|
-
|
|
28
|
-
### Decision 3: Limit default 200
|
|
29
|
-
|
|
30
|
-
Folders and tags are fewer than tasks; 200 is generous. Consistent with the spirit of existing defaults (tasks: 200, projects: 100).
|
|
31
|
-
|
|
32
|
-
### Decision 4: Same filter-in-snippet pattern
|
|
33
|
-
|
|
34
|
-
All filtering in the OmniJS snippet before data crosses the JXA bridge. TypeScript layer validates and passes through. Consistent with list_tasks and list_projects.
|
|
35
|
-
|
|
36
|
-
## Risks / Trade-offs
|
|
37
|
-
|
|
38
|
-
- **Minimal risk** — purely additive. Existing callers passing no args get all items (same as before but now capped at 200, which is a minor behavior change). Acceptable.
|