cursor-opencode-provider 0.8.0 → 0.8.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (67) hide show
  1. package/CHANGELOG.md +79 -0
  2. package/README.md +28 -9
  3. package/SECURITY.md +43 -7
  4. package/dist/context/build.d.ts +22 -4
  5. package/dist/context/build.js +33 -5
  6. package/dist/context/dynamic-catalog.js +6 -1
  7. package/dist/context/frozen.js +6 -2
  8. package/dist/context/index.d.ts +2 -0
  9. package/dist/context/index.js +2 -0
  10. package/dist/context/paths.d.ts +44 -17
  11. package/dist/context/paths.js +65 -18
  12. package/dist/context/skills-bridge.d.ts +22 -0
  13. package/dist/context/skills-bridge.js +20 -0
  14. package/dist/context/skills.d.ts +34 -0
  15. package/dist/context/skills.js +211 -0
  16. package/dist/host-agent-mode.d.ts +53 -4
  17. package/dist/host-agent-mode.js +117 -13
  18. package/dist/host-plan-file.d.ts +28 -0
  19. package/dist/host-plan-file.js +76 -0
  20. package/dist/image-input.d.ts +18 -0
  21. package/dist/image-input.js +119 -28
  22. package/dist/image-save.d.ts +10 -2
  23. package/dist/image-save.js +33 -21
  24. package/dist/language-model.d.ts +45 -17
  25. package/dist/language-model.js +925 -292
  26. package/dist/model-config.d.ts +9 -0
  27. package/dist/model-config.js +28 -1
  28. package/dist/opencode2/catalog.d.ts +1 -0
  29. package/dist/opencode2/catalog.js +2 -0
  30. package/dist/opencode2/image-save-tool.d.ts +8 -0
  31. package/dist/opencode2/image-save-tool.js +84 -0
  32. package/dist/opencode2/types.d.ts +15 -0
  33. package/dist/parallel-step.d.ts +65 -0
  34. package/dist/parallel-step.js +102 -0
  35. package/dist/plugin-opencode2.js +48 -40
  36. package/dist/plugin.js +69 -18
  37. package/dist/pricing-data.d.ts +14 -1
  38. package/dist/pricing-data.js +14 -1
  39. package/dist/protocol/ask-question.d.ts +12 -1
  40. package/dist/protocol/ask-question.js +77 -3
  41. package/dist/protocol/create-plan.d.ts +47 -25
  42. package/dist/protocol/create-plan.js +33 -23
  43. package/dist/protocol/interactions.d.ts +26 -17
  44. package/dist/protocol/interactions.js +80 -31
  45. package/dist/protocol/messages.js +74 -14
  46. package/dist/protocol/request.d.ts +5 -0
  47. package/dist/protocol/request.js +2 -0
  48. package/dist/protocol/switch-mode.d.ts +66 -27
  49. package/dist/protocol/switch-mode.js +147 -73
  50. package/dist/protocol/token-details.d.ts +2 -0
  51. package/dist/protocol/token-details.js +11 -1
  52. package/dist/protocol/tool-call-bridge.d.ts +4 -0
  53. package/dist/protocol/tool-call-bridge.js +14 -0
  54. package/dist/protocol/tools.d.ts +16 -1
  55. package/dist/protocol/tools.js +113 -11
  56. package/dist/replay-safety.d.ts +6 -0
  57. package/dist/replay-safety.js +56 -6
  58. package/dist/session.d.ts +35 -1
  59. package/dist/session.js +45 -1
  60. package/dist/todo-store.d.ts +2 -0
  61. package/dist/todo-store.js +19 -0
  62. package/dist/transport/connect.d.ts +2 -0
  63. package/dist/transport/connect.js +23 -6
  64. package/dist/usage.js +23 -6
  65. package/package.json +3 -2
  66. package/dist/plan-execution-kickoff.d.ts +0 -57
  67. package/dist/plan-execution-kickoff.js +0 -186
package/CHANGELOG.md ADDED
@@ -0,0 +1,79 @@
1
+ # Changelog
2
+
3
+ ## [Unreleased]
4
+
5
+ ## [0.8.1] - 2026-10-08
6
+
7
+ ### Added
8
+
9
+ - Cursor models see host skills the way Cursor presents them: RequestContext `agent_skills` with each skill's file path and description (no content), so matching skills can be loaded by reading the file when the host advertises `skill` ([#62](https://github.com/oakimov/cursor-opencode-provider/pull/62) by [@nkoynov](https://github.com/nkoynov))
10
+ - Claude Haiku 5.5 pricing, context, and capability metadata from Cursor's model docs
11
+
12
+ ### Changed
13
+
14
+ - External contributions are welcome again; PR descriptions must stay concise and human-readable
15
+ - CreatePlan writes plans where OpenCode keeps them: the session's own plan file on OpenCode 1.x (`.opencode/plans/` in a git project, otherwise the data `plans/` folder) and the Plan directory (`~/.opencode/plan`) on OpenCode 2.0, instead of a provider-chosen folder
16
+ - Cursor follows the OpenCode agent on every turn, as Cursor CLI does: choosing `plan` in OpenCode's agent picker puts Cursor in plan mode, and leaving it returns Cursor to agent mode
17
+ - Cursor SwitchMode into plan mode moves the session to OpenCode's `plan` agent once the turn ends (OpenCode 2.0, and 1.x without `plan_enter`)
18
+ - In plan mode without a host `plan_exit`, CreatePlan shows the plan and asks through `question` whether to start implementing; Yes switches to the build agent and continues. Under the `plan` agent with `plan_exit`, CreatePlan writes the session plan file and runs that review instead; a host plan-stage tool receives the plan directly
19
+ - `cursor-opencode-provider/image-save` accepts `ask: null` for hosts without a permission prompt (the image is written after containment only); a missing `ask` is still refused
20
+
21
+ ### Fixed
22
+
23
+ - Recovery Runs remember detached tool images without resending them on later turns; progress-only reopens use the current Run ID for host instruction updates
24
+ - Tool images stay with the correct result; unreadable or oversized images are reported without blocking other results, and recovery retains supported image formats
25
+ - Host instruction updates survive image reads, and declined tool calls return errors instead of successes
26
+ - Images returned by MCP tools reach the model in the same turn instead of after the next user message ([#46](https://github.com/oakimov/cursor-opencode-provider/pull/46) by [@nkoynov](https://github.com/nkoynov), [#45](https://github.com/oakimov/cursor-opencode-provider/issues/45))
27
+ - Images read with the `read` tool reach the model in the same turn instead of a "Media attached" placeholder ([#42](https://github.com/oakimov/cursor-opencode-provider/pull/42) by [@nkoynov](https://github.com/nkoynov))
28
+ - Cursor generations that emit several tool calls in one step now surface them together to OpenCode (instead of finishing after the first), so parallel host tools and subagents can run concurrently ([#44](https://github.com/oakimov/cursor-opencode-provider/issues/44))
29
+ - Tool-step and fresh-turn drain timeouts preserve late Cursor frames; interrupted parallel steps return pending tools without replaying already-started host calls
30
+ - A slow or failed health-check ping on the shared Cursor connection no longer aborts other chats streaming on it with "automatic retry unsafe" ([#47](https://github.com/oakimov/cursor-opencode-provider/pull/47) by [@moritzWa](https://github.com/moritzWa))
31
+ - Canceling a response with no pending tools closes the Cursor Run after its active pump stops, instead of leaving the stream and heartbeat open; tool-result continuations still retain their Run
32
+ - Tool-less title and summary Runs advertise an empty tool catalog (matching OpenCode's host-empty title/compaction Sends) instead of re-advertising the sticky set, so Cursor is not tempted to call tools on those turns
33
+ - Cache diagnosis labels zero cache-read counters on CreatePlan/SwitchMode turns without inferring cache hits from context size
34
+ - Skill locations containing HTML special characters load correctly on OpenCode 1; OpenCode 2 skill catalogs stay scoped to their own project, and structural skill lookups receive the owning session ID
35
+ - Cold-start tool-less requests finish without waiting for a sibling tool catalog; titles and summaries containing words such as "unavailable" retain their answer
36
+ - Malformed todo lists and question options remain subject to validation instead of silently clearing tasks or inventing choices
37
+ - `todowrite` calls that omit `priority` (Cursor TodoWrite shape) get OpenCode's required defaults before host validation, so todo updates no longer fail with `Missing key … ["priority"]`
38
+ - `question` calls that omit `header` get a short label from the question text before host validation, so prompts no longer fail with `Missing key … ["header"]`
39
+ - Dynamic-catalog guidance names the namespace for host tools (`opencode`) and states that `cursor` holds only Cursor's built-in tools, so a first `skill` lookup no longer fails in the wrong namespace.
40
+ - A SwitchMode handoff that starts a new host plan turn explicitly terminates the old Cursor Run before the host switch, preserving its checkpoint and preventing premature CreatePlan calls. Native planning guidance distinguishes direct functions from dynamic discovery; debug logs retain native discovery errors.
41
+ - Dynamic tool definitions carry their exact invocation identity and complete outer envelope beside the inner argument schema, so discovery and shortened search results retain required outer call fields.
42
+ - Shell calls preserve an advertised description and supply one for native commands when the canonical schema requires it, before required-argument validation.
43
+ - Dynamic-call guidance requires complete tool identities on every invocation; file tools reject misplaced shell commands, and debug logs retain server-side MCP validation errors that never reached host execution.
44
+ - Interaction guidance no longer advertises native AskQuestion as bridged without `question`, and explicitly skips optional delegation without a canonical `task`/`subagent` executor.
45
+ - Tool-less title and summary requests explicitly process supplied context without executing its task, and missing required tool arguments are rejected before host execution with a correlated correction request.
46
+ - Stale Cursor token categories no longer appear as current context metadata or cache changes; usage validation marks a retained older breakdown `stale` while checking current occupancy independently.
47
+ - Refused exec debug lines include the rejection reason, so partial-read protection can be distinguished from an invalid tool mapping.
48
+ - OpenCode 2.0 saves Cursor-generated images through `cursor_image_save` instead of refusing the binary write
49
+ - On OpenCode 2.0, an approved plan starts implementing and plans land in the Plan directory even after the plugin is set up for several locations or reloaded; before, disposing an older setup removed the newer one's agent switch and the session stopped after approval
50
+ - Assistant text before and after a tool call or plan review is separated into paragraphs instead of running together, so a shown plan's heading renders
51
+ - Session titles and summaries contain only the model's answer, not its narration before refused tools or refusal-shaped text after a lifecycle refuse
52
+ - A new message that arrives with a plan approval or plan-mode switch no longer discards the Cursor turn that raised it, and one that arrives while Cursor is still answering no longer waits for that unseen answer before starting over
53
+ - A new message sent while a plan review, question, or mode switch is still open declines it instead of leaving the earlier Cursor run open
54
+ - A pending tool result in a new message reaches its Cursor run before helper detection, so the conversation is kept even when the tool catalog shrank
55
+ - Approving a plan is not undone by plan-mode reminders the host appends after the answer, and a failed approval delivery no longer changes the mode or starts implementing
56
+ - Native agent switches run once per session at a time and keep a newer request that arrives while one is applying
57
+ - On OpenCode 1.x, a SwitchMode in a session's first turn moves it to the `plan` agent too
58
+ - Where OpenCode offers no `plan_enter`, the model is told to enter plan mode with Cursor's SwitchMode directly instead of concluding plan mode is unavailable
59
+ - With a host plan-stage tool, plan mode is kept while the host still advertises `plan_enter`
60
+ - After MCP tools appear mid-turn, the next user message reuses RequestContext instead of rebuilding it
61
+ - A Cursor run that fails before showing any output can be retried automatically again: timing, tracing, and progress fields Cursor now sends on ordinary updates no longer mark every run unsafe to retry
62
+ - The debug log's turn usage validation no longer reports `status=mismatch` when Cursor's context shrinks between steps
63
+ - Cursor models carry a model `family`, so OpenCode can write session titles with its small model (for example GPT-5.6 Luna) instead of the model you are coding with; context/speed suffixes and Kimi minor versions stay in their model's family ([#43](https://github.com/oakimov/cursor-opencode-provider/pull/43) by [@nkoynov](https://github.com/nkoynov))
64
+ - A parameterless default variant is labeled distinctly from its model in the picker.
65
+
66
+ ## [0.8.0] - 2026-10-04
67
+
68
+ ### Changed
69
+
70
+ - Host system context is delivered as a single always-apply rule; the provider no longer rediscovers local `AGENTS.md`, skills, or agent files for Cursor `RequestContext` ([#37](https://github.com/oakimov/cursor-opencode-provider/pull/37))
71
+ - RequestContext tool overlay is names-only; full tool definitions are answered from the live host catalog on exec #36 ([#37](https://github.com/oakimov/cursor-opencode-provider/pull/37))
72
+
73
+ ### Fixed
74
+
75
+ - Cursor browser-login and API-key credentials renew the same way Cursor's own clients do, so long sessions stop failing after token expiry ([#35](https://github.com/oakimov/cursor-opencode-provider/issues/35))
76
+ - Cursor conversations stay consistent across OpenCode 2.x subagents, model switches, and host mid-turn notes instead of superseding the held Run or losing tool continuations ([#34](https://github.com/oakimov/cursor-opencode-provider/pull/34))
77
+ - Recovered RequestContext and standalone tool catalogs survive restart and lifecycle turns without dropping MCP/tool state
78
+
79
+ Prior releases before the changelog was introduced are available via git tags (`v0.7.6`, `v0.7.5`, …).
package/README.md CHANGED
@@ -4,7 +4,7 @@ Use [Cursor](https://cursor.com) subscription models from [OpenCode](https://ope
4
4
 
5
5
  This project is a custom **AI SDK provider** (`LanguageModelV3`) plus an **OpenCode plugin** that handles authentication and model discovery. Instead of calling a generic chat-completions API, it encodes and decodes Cursor's protobuf agent protocol over HTTP/2 to Cursor's agent backend.
6
6
 
7
- > **Runs unchanged across OpenCode, OpenCode 2.0, and four additional coding agents.** OpenCode loads this plugin natively, including through the dedicated OpenCode 2.0 entrypoint. [OCP — OpenCode Plugin Compatibility](https://github.com/oakimov/opencode-plugin-compat) runs the same published plugin, without a provider fork, in **Kilo Code, MiMo Code, pi, and oh-my-pi**. See [Coding-agent compatibility](#coding-agent-compatibility).
7
+ > **Runs unchanged across OpenCode, OpenCode 2.0, and five additional coding agents.** OpenCode loads this plugin natively, including through the dedicated OpenCode 2.0 entrypoint. [OCP — OpenCode Plugin Compatibility](https://github.com/oakimov/opencode-plugin-compat) runs the same published plugin, without a provider fork, in **Kilo Code, MiMo Code, pi, oh-my-pi, and DeepSeek Harness**. See [Coding-agent compatibility](#coding-agent-compatibility).
8
8
  >
9
9
  > **Status:** Usable end-to-end for authentication, model discovery, streaming, and tools. See [Known limitations](#known-limitations).
10
10
 
@@ -24,11 +24,13 @@ instead of requiring a host-specific Cursor provider.
24
24
  | **MiMo Code** | Unchanged through OCP's OpenCode-clone compatibility layer |
25
25
  | **pi** | Unchanged through `@opencode-compat/pi-bridge` and pi's provider extension API |
26
26
  | **oh-my-pi (omp)** | Unchanged through the same Pi-family bridge |
27
+ | **DeepSeek Harness (dsh)** | Unchanged through `@opencode-compat/dsh-bridge` and DSH's Cordis `LlmAdapter` API |
27
28
 
28
29
  Start with OCP's host guides:
29
30
 
30
31
  - [OpenCode clones: Kilo Code and MiMo Code](https://github.com/oakimov/opencode-plugin-compat/blob/main/docs/hosts/opencode-clones.md)
31
32
  - [Pi family: pi and oh-my-pi](https://github.com/oakimov/opencode-plugin-compat/blob/main/docs/hosts/pi-family.md)
33
+ - [DSH family: DeepSeek Harness](https://github.com/oakimov/opencode-plugin-compat/blob/main/docs/hosts/dsh-family.md)
32
34
 
33
35
  OCP is designed to grow by adding host profiles and narrow adapters or bridges,
34
36
  so support for other coding agents can be added there while this provider and
@@ -42,7 +44,7 @@ OpenCode driving a Cursor-routed Grok model through this provider:
42
44
 
43
45
  ## Features
44
46
 
45
- - **Multi-host compatibility** — the same plugin package runs natively in OpenCode and OpenCode 2.0, and unchanged through OCP in Kilo Code, MiMo Code, pi, and oh-my-pi; new host support belongs in the compatibility layer rather than a provider fork
47
+ - **Multi-host compatibility** — the same plugin package runs natively in OpenCode and OpenCode 2.0, and unchanged through OCP in Kilo Code, MiMo Code, pi, oh-my-pi, and DeepSeek Harness; new host support belongs in the compatibility layer rather than a provider fork
46
48
  - **OpenCode integration** — registers a `cursor` provider with auth hooks and cached model list
47
49
  - **Authentication** — browser OAuth (PKCE), or API key from [cursor.com/settings](https://cursor.com/settings)
48
50
  - **Model discovery** — fetches available models from Cursor's API and caches them locally
@@ -179,6 +181,12 @@ The provider resolves each catalog context limit in this order:
179
181
 
180
182
  The docs fallback only fills static catalog metadata. It does not invent Cursor variants or enable a long-context tier that `AvailableModels` did not advertise.
181
183
 
184
+ #### Model families
185
+
186
+ Catalog entries derive a models.dev-style `family` from the Cursor model id (`claude-haiku-4-5` → `claude-haiku`, `gpt-5.6-luna` → `gpt-luna`). Version segments and trailing `-1m` / `-fast` suffixes do not split families; Kimi keeps its major generation (`kimi-k2.7-code` → `kimi-k2`). Auto (`default`) has no derived family. A non-empty family supplied in cached model metadata takes precedence; the live `AvailableModels` schema has no family field.
187
+
188
+ OpenCode uses family metadata to select a small model for session titles (2.0: `gpt-luna`, `gemini-flash-lite`, `gemini-flash`, `claude-haiku`; 1.x: `gemini-flash`, `gpt-nano`, `claude-haiku`). If no eligible family is available, titles fall back to the session model. To override this selection, set the `title` agent's `model` on either major, or `small_model` on 1.x. Compaction summaries use the host's compaction model selection; this metadata does not move them to the small model. OpenCode 2's model-listing tool shows one entry per family unless asked for all.
189
+
182
190
  #### Image input
183
191
 
184
192
  The provider advertises `text` + `image` input only when the selected Cursor model supports images; model output remains text. This works in the classic plugin, the 1.18 v2 plugin, and OpenCode 2.0.
@@ -189,7 +197,11 @@ Image support is resolved in this order:
189
197
  2. The `Capabilities` column in Cursor's [`docs.md`](https://cursor.com/docs.md), generated alongside context and pricing metadata
190
198
  3. Text-only when neither source describes the model
191
199
 
192
- OpenCode image file parts are decoded from bytes, base64/data URLs, local file URLs, or HTTP(S) URLs and sent through Cursor's `UserMessage.selected_context.selected_images` field. On fresh or rebased Runs, the provider also harvests image `file-data` from tool results and `file` parts from assistant and historical user messages (the trailing user message stays owned by last-user extraction); previously sent history images are deduplicated by content hash per OpenCode session. Held-open continuation results remain text-only because Cursor's exec-result channel has no image field. Attachments are limited to 20 MiB total, matching OpenCode's desktop attachment budget. PDF, audio, and video inputs are not advertised or silently discarded.
200
+ OpenCode image file parts are decoded from bytes, base64/data URLs, local file URLs, or HTTP(S) URLs and sent through Cursor's `UserMessage.selected_context.selected_images` field. Fresh and rebased Runs normalize the same image formats as held-Run continuations, including AI SDK `image-data` / `image-url` and OpenCode attachment URLs. Previously sent history images are deduplicated by content hash per OpenCode session.
201
+
202
+ On a held-open Run, an image read returns bytes in `ReadSuccess.data` (including GenerateImage reference reads), and MCP tool images return as `McpImageContent` items after the success text (`McpToolResultContentItem` field 2). Detached media is attributed only where OpenCode's result order and markers establish its owner; ambiguous images stay available through history instead of being attached to an unrelated tool. A host instruction update beside a native read uses Cursor's context-injection channel because read text and bytes are mutually exclusive.
203
+
204
+ One 20 MiB decoded-byte budget covers all image-bearing pending results in a continuation, without Cursor CLI's resize/re-encode step. Unreadable or oversized tool images are reported without blocking other results; cancellation still stops decoding. Other result shapes have no image field, so their images reach Cursor as history on the next fresh Run. Direct PDF, audio, and video attachments are unsupported; tool-result delivery only forwards image media.
193
205
 
194
206
  #### Max mode
195
207
 
@@ -253,7 +265,7 @@ bun run generate:pricing # refresh src/pricing-data.ts from Cursor docs
253
265
  bun run check:pricing # fixture coverage for known model ids
254
266
  ```
255
267
 
256
- **HARD STOP before any version bump or `v*` tag:** run `generate:pricing` and `check:pricing` in the same session, fix any unmapped Cursor display names, and commit mapping/`pricing-data.ts` updates **before** touching `"version"` or creating the tag. Publish CI regenerates rates from the live docs and will fail the release on an unmapped name — that is a last line of defense, not the primary check. See `AGENTS.md`.
268
+ **HARD STOP before any version bump or `v*` tag:** run `generate:pricing` and `check:pricing` in the same session, fix any unmapped Cursor display names, commit mapping/`pricing-data.ts` updates, and finalize [`CHANGELOG.md`](CHANGELOG.md) (`[Unreleased]` → dated version) **before** touching `"version"` or creating the tag. Publish CI regenerates rates from the live docs and will fail the release on an unmapped name — that is a last line of defense, not the primary check. See `AGENTS.md` and [Changelog](#changelog).
257
269
 
258
270
  ## Architecture
259
271
 
@@ -270,7 +282,7 @@ OpenCode
270
282
  |--------|------|
271
283
  | `src/plugin.ts` | Classic OpenCode hooks: provider registration, OAuth, API key exchange, login renewal and persistence |
272
284
  | `src/plugin-v2.ts` | OpenCode 1.18 Effect/Promise v2 plugin (`ctx.aisdk.*`); load via `./plugin/v2` only |
273
- | `src/plugin-opencode2.ts` | OpenCode 2.0 plugin (`provider.transform`, integration, tools, aisdk, `shell.create.before`, `websearch`, plan kickoff); load via `./plugin/opencode2` or `./server` |
285
+ | `src/plugin-opencode2.ts` | OpenCode 2.0 plugin (`provider.transform`, integration, tools, aisdk, `shell.create.before`, `websearch`); load via `./plugin/opencode2` or `./server` |
274
286
  | `src/opencode2/` | 2.0-only provider inventory, integration/auth, and local API types |
275
287
  | `src/plugin-core.ts` | Host-neutral SDK factory, package matching, API base/telemetry resolution |
276
288
  | `src/model-config.ts` | Cursor model → OpenCode model mapping shared by every plugin surface |
@@ -294,7 +306,9 @@ OpenCode
294
306
 
295
307
  The provider adds OpenCode-specific system guidance to normal tool-capable conversations, including tool availability, canonical workspace-path grounding, preferring OpenCode `todowrite` / `todoread` over Cursor TodoWrite when those tools are advertised without inviting catalog-comparison narration, and preferring `edit` / `write` (or `apply_patch`, when the host advertises that instead) over shell-based file mutation when those tools are available. Compaction keeps its dedicated prompt unchanged.
296
308
 
297
- Cursor keeps OpenCode `skill` and configured MCP tools off its native top-level function list on **both** OpenCode 1.x and OpenCode 2.0 — they appear as names-only MCP descriptors and are reached through GetDynamicTools / CallDynamicTool (exec #36 supplies full definitions). When the host advertises `skill` and/or tools of MCP servers configured in `opencode.json`, the provider names those routes in system guidance and mirrors OpenCode's load-when-matches / do-not-reinvoke rules so models prefer `skill` / MCP over Grep/Shell and do not narrate that those tools are unavailable. Skill catalog changes after admission are host `<system-update>` notes, not a second RequestContext skill list. OpenCode 2 still moves MCP servers that did not set `"codemode": true` onto the direct AI SDK catalog first; without that placement they never reach Cursor's MCP ads at all (see [OpenCode 2 MCP tools](docs/opencode-2.md#mcp-tools)).
309
+ Cursor keeps OpenCode `skill` and configured MCP tools off its native top-level function list on **both** OpenCode 1.x and OpenCode 2.0 — they appear as names-only MCP descriptors and are reached through GetDynamicTools / CallDynamicTool (exec #36 supplies full definitions). When the host advertises `skill` and/or tools of MCP servers configured in `opencode.json`, the provider names those routes in system guidance and mirrors OpenCode's load-when-matches / do-not-reinvoke rules so models prefer `skill` / MCP over Grep/Shell and do not narrate that those tools are unavailable. When the host advertises `skill`, skills that have a usable file path are also listed in RequestContext `agent_skills` (path and description only), as Cursor's own clients list them. Skill catalog changes after admission are host `<system-update>` notes; `agent_skills` follows the baseline catalog until the next epoch. OpenCode 2 still moves MCP servers that did not set `"codemode": true` onto the direct AI SDK catalog first; without that placement they never reach Cursor's MCP ads at all (see [OpenCode 2 MCP tools](docs/opencode-2.md#mcp-tools)).
310
+
311
+ Each discovered dynamic definition carries its exact `namespace` and `toolName` in its description. Keep those fields at the outer level of every `CallDynamicTool` call; the discovered input schema describes only its `arguments` object.
298
312
 
299
313
  Native Cursor todo display updates (`update_todos_tool_call` / `read_todos_tool_call`) are mirrored into `todowrite` / `todoread` when those host tools are advertised. OpenCode 2 dropped the host builtins; set `CURSOR_OPENCODE2_TODOS=1` (or `true`) to register the same canonical names when the catalog omits them (in-memory per session, cleared on `session.deleted` and process restart). With the flag unset, the plugin registers no todo tools. Enabled plugin tools are **direct** catalog tools (`options.codemode: false` plus an `output` schema), not Code Mode helpers. The provider retains the last known full list per OpenCode session — refreshed by bridged writes, direct host `todowrite` calls, and `todoread` results, surviving Run close, checkpoint resume, and later turns — so `merge: true` updates that omit Cursor's completed list still become a host replace-all snapshot (matched by id, then by content across id spaces). This snapshot is in-memory only and never enters the prompt, RequestContext, or persisted restart state, so Cursor's prompt-cache prefix is unaffected; a provider process restart clears it until the next write/read (intentional — see `tasks/lessons.md`).
300
314
 
@@ -366,6 +380,7 @@ Project `instructions` may reference absolute or `~/` paths (OpenCode parity). S
366
380
 
367
381
  ## Known limitations
368
382
 
383
+ - **Native planning calls and handoffs** — CreatePlan and SwitchMode use their already supplied native function definitions, never dynamic discovery or CallDynamicTool. Where an approved switch resumes in the host's plan agent after the current turn, that turn ends before any further tool call; the following plan turn records the plan. Debug logs include server-side native discovery errors as well as MCP invocation errors, without replaying either as host execution.
369
384
  - **Personal use / ToS** — this provider speaks Cursor’s private agent protocol (CLI-shaped client identity). Use only with an account you own; Cursor may change or restrict the API without notice.
370
385
  - **Cursor's checkpoint controls displayed totals** — live continuation captures show `TurnEnded` counters can decrease between turns and can represent aggregate model work rather than final context occupancy. When a checkpoint contains `ConversationStateStructure.token_details`, the provider makes AI SDK input + output equal Cursor's `used_tokens`; at `TurnEnded`, output/reasoning remain based on `TurnEnded`, while uncached/cache-read/cache-write input are proportionally normalized to the remaining context input. OpenCode TUI/GUI replace each assistant message's tokens (TUI requires `tokens.output > 0`) and add cost per step, so mid-turn tool boundaries emit the last-known occupancy snapshot rather than a delta or a billed zero. Those snapshots set `output=1` so the footer updates and `providerMetadata.copilot.totalNanoAiu: 0` so OpenCode's getUsage reports $0 instead of charging occupancy as a new prompt. A Run without fresh token details retains the previous checkpoint snapshot and marks `providerMetadata.cursor.context` as `source: "checkpoint-previous-turn", stale: true`. If no checkpoint has ever supplied token details, standard usage is zero and context metadata is absent, so `TurnEnded` is never misrepresented as occupancy; its exact values remain under `providerMetadata.cursor.*Raw`. The full known snapshot includes `usedTokens`, `maxTokens`, percentage, category breakdown, source, and staleness. Cursor consistently sends `cache_write=0` in captured Runs, so no alternate write count is available.
371
386
  - **`request_context` from OpenCode** — each Run sends Cursor `RequestContext` with OpenCode's system prompt (its `AGENTS.md` / `instructions`, skill list, MCP instructions) plus the provider's interaction guidance as one always-apply rule, workspace env, git, layout, host-advertised `task`/`subagent` names, names-only tool overlay, and plugin lines. Cursor does not follow a client-seeded `system` message or an untyped rule, so the always-apply (`global`) rule is the channel that makes `AGENTS.md` effective; the provider does not read instruction or skill files itself. Merged `opencode.json` is still read for MCP server ids, plugin lists, and interaction guidance. Expensive workspace state (environment, repository, git, and layout) is frozen per Cursor conversation and persisted for restart recovery. Tools/MCP names, host subagents, and plugin metadata are rediscovered each Run then epoch-held (equal ids keep frozen bytes; new ids append); unchanged overlays reuse byte-identical encoded context. Concurrent first Runs share one base build, and compaction/reset clears it. Its canonical root is also used by the [injected system guidance](#injected-system-guidance). Cursor-only cloud/sandbox marketplace surfaces are omitted. `env.workspace_paths` / `process_working_directory` stay on the real git workspace; Cursor's metadata root (`project_folder`, MCP `workspace_project_dir`, terminals/transcripts) is advertised under `<host-cache>/projects/<slug>/` (default `~/.cache/opencode/projects/…`) so dumps like `agent-tools/` do not land in the repo. OpenCode remains the permission authority: its coarse allow/ask/deny configuration is not fabricated into Cursor's unrelated allow/block instruction-list messages.
@@ -377,10 +392,10 @@ Project `instructions` may reference absolute or `~/` paths (OpenCode parity). S
377
392
  - **MCP resource tools (`list_mcp_resources` / `read_mcp_resource`) also use collision-safe aliases, and Cursor's native resource exec is always answered** — OpenCode's resource tools (advertised only when a connected MCP server declares `capabilities.resources`) share bare names with two of Cursor's own native tool-call variants. Left unaliased, Cursor's client can route a call onto its native exec channel instead of the ordinary MCP tool path, which this provider does not translate — confirmed live: reading a resource was dispatched natively and killed the Run before this fix. The provider now aliases both names to `custom_list_mcp_resources` / `custom_read_mcp_resource` (same mechanism as web search/fetch) so normal calls execute through OpenCode's tools, and separately answers Cursor's native resource-exec fields directly on the protocol floor — an empty list, or a "server not found" error for read — so an unsolicited native request from Cursor can never fail the Run. Downloads (`download_path`) have no OpenCode equivalent and are always refused; binary resource content is subject to the same [50 KB / attachment-type limits](#known-limitations) as other tool results.
378
393
  - **Background shells are non-interactive** — Cursor's native background-shell spawn and soft-background shell-stream timeouts are bridged through OpenCode's foreground-only `bash` tool. With bash/zsh, the classic plugin keeps the original permission/UI command and executes the wrapper through `shell.env` (`BASH_ENV` / `ZDOTDIR`); OpenCode's non-interactive sh/dash argv ignores those startup variables, so the plugin uses a short `exec /bin/sh '<wrapper-file>'` command backed by the same private wrapper. Native background-spawn requests also carry a self-contained marker-producing fallback when the classic hooks are absent. Spawn/soft-bg wrappers detach with `nohup`, redirect output under `${TMPDIR:-/tmp}/cursor-opencode-{bg,shell}.*`, and return the real PID (or typed timeout/exit) to Cursor. Private markers and OpenCode's `<shell_metadata>` envelope are stripped before storage/render, with a short still-running / started / timed-out status line left for the bash bubble. Requests that require `write_shell_stdin` are rejected explicitly because OpenCode does not expose an interactive background-process lifecycle through its AI SDK tool interface. The POSIX wrap path is not implemented for native Windows PowerShell/`cmd`.
379
394
  - **Cursor-native AskQuestion reaches you through OpenCode's question tool** — when Cursor asks a clarifying question, the provider translates the interaction into the host `question` tool, so the prompt appears in OpenCode with its real options and your answer is mapped back to Cursor's option ids (an answer you type yourself becomes Cursor's freeform text). Questions Cursor marks as non-blocking are acknowledged immediately and answered later, matching Cursor CLI. If the current agent does not offer `question` — most subagents deny it, and compaction turns have no tools — the request is declined with that reason and the model asks in its reply instead.
380
- - **Cursor-native SwitchMode follows each OpenCode generation's planning model** — on OpenCode 1.x, advertised `plan_enter` / `plan_exit` tools remain authoritative and Cursor waits for their approve/reject result. OpenCode 2.0 replaced those tools with vendor-maintained primary `plan` / `build` agents, so its plugin applies an approved Cursor mode change through `session.switchAgent` after the owning Run is terminal; leaving plan still uses the advertised `question` gate when no `plan_exit` tool exists. Direct host UI agent switches are also observed. Any real host-agent or stable system-prompt change rotates and reseeds the Cursor conversation instead of resuming an incompatible checkpoint, while title/generation lifecycle prompts remain ephemeral and do not evict the primary cache. Provider-owned Cursor reminders remain the fallback when a host cannot switch agents.
381
- - **Cursor image generation saves through OpenCode's permission where the host API exposes it** — Cursor generates the image on its servers and then writes it like any other file. On the classic plugin/OCP surfaces, the provider holds raw bytes and asks for the same permissions as a normal write (`external_directory`, then `edit`) before saving byte-for-byte. OpenCode 2.0's public plugin `ToolContext` does not expose a permission-request method, so its entrypoint deliberately does not advertise `cursor_image_save`; generation and binary writes are refused before staging instead of bypassing permissions or exposing an always-failing tool. By default supported-host images land in the Cursor project folder under `assets/`, kept out of the git tree as agent artifacts. Editing an existing image with `reference_image_paths` is not supported yet. The classic implementation is adapted from [`jkalasas/opencode-antigravity-image`](https://github.com/jkalasas/opencode-antigravity-image) (MIT) by jkalasas.
395
+ - **Cursor-native SwitchMode follows each OpenCode generation's planning model** — on OpenCode 1.x, advertised `plan_enter` / `plan_exit` tools remain authoritative and Cursor waits for their approve/reject result. OpenCode 1.x without `plan_enter` (current releases advertise only `plan_exit`, behind `OPENCODE_EXPERIMENTAL_PLAN_MODE`) is switched into its own `plan` primary agent once the owning Run ends, through a synthetic `session.promptAsync` turn for that agent (only for sessions running one of the primary agents read from `app.agents()`); a CreatePlan raised before that switch is deferred to the plan-agent turn. Inside the `plan` agent, CreatePlan writes the plan to the session's plan file (OpenCode's `Session.plan` location, or the structural path bridge's `planFile`) and runs the `plan_exit` review; OpenCode moving the session to its build agent is the approval. Cursor's own mode follows the OpenCode agent on every turn (`UserMessage.mode`, as Cursor CLI sends it): choosing `plan` in the agent picker puts Cursor in plan mode, and leaving it ends Cursor plan mode. OpenCode 2.0 replaced those tools with vendor-maintained primary `plan` / `build` agents, so its plugin applies an approved Cursor mode change through `session.switchAgent` after the owning Run is terminal; without a `plan_exit` tool, the plan is approved through CreatePlan's `question` prompt (below), and a Cursor SwitchMode out of the host `plan` agent is pointed there. Direct host UI agent switches are also observed. Any real host-agent or stable system-prompt change rotates and reseeds the Cursor conversation instead of resuming an incompatible checkpoint, while title/generation lifecycle prompts remain ephemeral and do not evict the primary cache. Provider-owned Cursor reminders remain the fallback when a host cannot switch agents.
396
+ - **Cursor image generation saves through OpenCode's permission where the host API exposes it** — Cursor generates the image on its servers and then writes it like any other file. The provider holds raw bytes and commits them through handle-only `cursor_image_save`. On the classic plugin/OCP surfaces it asks for the same permissions as a normal write (`external_directory`, then `edit`) before saving byte-for-byte. OpenCode 2.0's public plugin `ToolContext` has no permission-request method, so that entrypoint still advertises the tool with catalog `permission: "edit"` and writes after containment. By default images land in the Cursor project folder under `assets/`, kept out of the git tree as agent artifacts. GenerateImage `reference_image_paths` use the same held-Run `ReadSuccess.data` path as an ordinary image `read` (no CLI resize; large refs may still hit the 20 MiB decode budget). The classic implementation is adapted from [`jkalasas/opencode-antigravity-image`](https://github.com/jkalasas/opencode-antigravity-image) (MIT) by jkalasas.
382
397
  - **Switching models mid-session reseeds Cursor instead of resuming a stale checkpoint** — a Cursor conversation only contains what Cursor itself produced. When the host's latest assistant turn in a session came from another model (another provider or a local model), the provider starts a fresh Cursor conversation seeded with the full host history, tool results included, rather than resuming the old checkpoint that would hide the other model's work (and has been observed to end Runs with `Cursor API error (code=internal)`). If that history would fill more than 80% of the target model's context, the provider reports a context overflow before opening a Run so the host compacts first.
383
- - **Cursor-native CreatePlan writes a host plan file, then asks before executing it** — when Cursor raises `create_plan_request_query`, the provider writes plain markdown to your host's own plans directory (`<hostGlobalDataDir>/plans/`, never your repository and never Cursor's `~/.cursor/plans`), with no Cursor YAML frontmatter. Writing the plan does not ask; running it does. If you are in plan mode, the provider prints the plan into the conversation — Cursor sends it over a side channel that would otherwise never display it — and then asks *"Plan at &lt;path&gt; is complete. Would you like to switch to the build agent and start implementing?"* — the same prompt OpenCode's own `plan_exit` uses — and keeps Cursor waiting for your answer. Answer **Yes** and the plan is reported as approved and implementation begins; answer **No** (or dismiss it) and Cursor is told the plan was not accepted, so the model refines it and proposes again instead of starting work. Hosts that advertise their own plan-staging tool run their native review UI instead, and it reports the same approve/refine outcome. Empty args still get the CLI empty-`plan_uri` success ack. Display `create_plan_tool_call` continues to mirror into `todowrite` separately.
398
+ - **Cursor-native CreatePlan writes a host plan file, and executing it always needs the user's approval** — when Cursor raises `create_plan_request_query`, the provider writes plain markdown where OpenCode itself keeps plans (never Cursor's `~/.cursor/plans`), with no Cursor YAML frontmatter: on OpenCode 1.x the session's `Session.plan` file (`<worktree>/.opencode/plans/<created>-<slug>.md` in a git project, else `<data>/plans/`); on OpenCode 2.0 its Plan directory (`~/.opencode/plan/`). A structural path bridge, when installed, supplies the host's own location instead. Writing never asks; executing always does, through the first channel the host offers: a host plan-stage tool runs its native review; under the host `plan` agent with `plan_exit`, CreatePlan runs that review and the session moving to the build agent is the approval; otherwise, in plan mode, the plan is shown in the transcript and the advertised `question` tool asks whether to switch to the build agent and start implementing (Yes switches and continues; anything else keeps planning). With none of these the plan is recorded and the user switches to the build agent when they want it implemented. Empty args still get the CLI empty-`plan_uri` success ack. Display `create_plan_tool_call` continues to mirror into `todowrite` separately.
384
399
  - **Other Cursor-native interaction queries remain headless** — remaining UI/approval *queries* (as distinct from display tool calls and the AskQuestion / SwitchMode / CreatePlan / GenerateImage bridges) still cannot be surfaced through the AI SDK provider interface. The normal system prompt redirects available web capabilities to executable host tools (`custom_websearch`, `custom_webfetch`); native web/PR/MCP/SCM requests are declined so they remain behind host tool permissions. Compaction prompts are unchanged. Unknown future interaction variants fail the turn explicitly instead of hanging the Run stream.
385
400
  - **Compaction resets Cursor conversation state without resetting stable workspace context** — the classic plugin marks OpenCode's `compaction` agent explicitly. On those turns the provider mints an isolated Cursor `conversation_id`, drops the prior checkpoint + KV blobs, preserves real tool outputs as OpenCode-host observations in the seed history, and re-advertises the session's last tool catalog while refusing execution during the summary itself. The first normal turn then rebases once more onto a fresh conversation seeded with OpenCode's compacted prompt and normal system instructions, so the summary-agent checkpoint cannot suppress later tool calls. Across both id rotations, the frozen workspace/rules/environment base and its prior encoded-context comparison seed are transferred instead of rebuilt; tools, skills, agents, plugins, and MCP state are still rediscovered, and the complete context is reused only when its bytes remain identical. Ordinary no-tool / `toolChoice:none` calls do not reset conversation state. Debug logs include system-prompt and encoded RequestContext hashes for cache-prefix verification.
386
401
  - **Conversation bindings survive provider restarts** — after each successful `TurnEnded`, the OpenCode-session binding, latest checkpoint, checkpoint-reachable KV blob graph, frozen request-context base, last real tool catalog, host agent, stable system-prompt hash, and pending post-compaction rebase marker are atomically replaced in that session's private `<host-cache>/cursor-conversations/*.pb.gz` snapshot. The protobuf stores checkpoints, blob ids/data, and request context as bytes without JSON/base64 expansion, then gzip compresses the complete record. A checkpoint alone is insufficient: Cursor requests its referenced hashes through `get_blob` when the next Run starts. Reachability follows Cursor CLI's export traversal (turns, messages, steps, images, summaries, todos, prompts, and nested subagents), pruning superseded blobs; decode failure or a missing referenced hash conservatively retains the complete blob set. The stable OpenCode session id selects the file; Cursor conversation ids may rotate during compaction, agent switches, or prompt changes and remain inside the snapshot. Independent files prevent concurrent OpenCode processes from losing unrelated sessions by replacing a shared aggregate cache. Startup restores records updated within 24 hours and safely flushes older ones without deleting a concurrently published fresh replacement. Experimental legacy JSON snapshots are discarded rather than migrated. Debug logs report hydration as `restored`, `missing`, `invalid`, or `expired`, plus the compressed and protobuf snapshot sizes. The restored catalog is a fallback only for lifecycle turns such as compaction; an ordinary restricted/no-tool turn never resurrects it. Process-global bindings and prompt/catalog state still use a 256-session LRU bound; memory eviction drops the live checkpoint, blobs, and frozen context, which can be rehydrated from the restart cache on later use.
@@ -389,6 +404,10 @@ Project `instructions` may reference absolute or `~/` paths (OpenCode parity). S
389
404
  - **Large reads are capped by OpenCode, and the cap is reported** — OpenCode 1.x `read` stops at `MAX_BYTES = 50 * 1024` and appends `(Output capped at 50 KB. Showing lines X-Y. Use offset=N to continue.)`. OpenCode 2 prints `Read file <path>, lines <start>-<end>` with `N: ` prefixes and, when the page ends before EOF, `[Output truncated. Continue reading with offset: N]` (50 KB or 2,000 lines); it also shortens each individual line after 2,000 characters. The provider strips either envelope before returning content to Cursor so the model cannot echo wrappers into a later write, but it re-states every partial result: a native read appends a `[Partial read: …]` marker after the content, an MCP read gets a separate notice content item, and a Pi read carries structured `PiReadExecSuccess.truncation`. Relative grep, glob, and directory entries are resolved against the workspace root. Shell stdout absolutizes only relative path tokens, not prose. Checkpointed turns, which do not resend the system prompt, get that same root on the user message. The structured `truncated` flag alone is not enough — verified against live `gpt-5.4-mini` and `grok-4.5` sessions, both of which asserted "highly confident" that partial content was the whole file until the textual marker was added. Deliberately paged reads (explicit `offset`/`limit`) are not marked unless the host also hits the byte cap or character-truncates a line. Whole-file writes and overwriting `Add File` patches that echo the marker are rejected before OpenCode executes them; targeted edits remain possible, including edits to source that quotes the warning text. Cursor's private legacy-edit read is handled separately as described above, so an edit can operate on a workspace file larger than 50 KB without turning the capped preview into a replacement. The cap is hardcoded in OpenCode's read tool — it is not the configurable `tool_output.max_bytes`, which governs `Truncate.Service` and not read's line accumulation — so ordinary model reads still require paging with `offset`.
390
405
  - **No fallback models** — if Cursor’s `AvailableModels` API is unreachable and there is no local cache, the provider exposes no models.
391
406
 
407
+ ## Changelog
408
+
409
+ See [CHANGELOG.md](CHANGELOG.md) for user-facing release notes. New work lands under `[Unreleased]` until the next version tag.
410
+
392
411
  ## License
393
412
 
394
413
  MIT
package/SECURITY.md CHANGED
@@ -1,10 +1,22 @@
1
- # Security notes
1
+ # Security
2
2
 
3
- This provider mirrors OpenCode’s trust model for project configuration. That has implications for what ends up in Cursor context.
3
+ ## Threat Model
4
4
 
5
- ## Project `instructions` can include arbitrary local paths
5
+ ### Overview
6
6
 
7
- OpenCode’s `opencode.json` / `opencode.jsonc` `instructions` array may list:
7
+ cursor-opencode-provider is an OpenCode plugin and AI SDK provider that speaks Cursor's private agent protocol. Through the host it can trigger powerful tools (shell, file operations, network access via host MCP/tools) and it sends workspace context, instructions, and tool results to Cursor.
8
+
9
+ ### No Sandbox
10
+
11
+ Neither this provider nor OpenCode's permission system is a security sandbox. Permissions help users review and control actions; they are not an isolation boundary.
12
+
13
+ If you need isolation, run the host in a disposable container, virtual machine, or similarly restricted environment. Review project instructions and third-party tool or MCP server configuration before allowing them to execute.
14
+
15
+ ### Project `instructions` can include arbitrary local paths
16
+
17
+ This provider mirrors OpenCode's trust model for project configuration. That has implications for what ends up in Cursor context.
18
+
19
+ OpenCode's `opencode.json` / `opencode.jsonc` `instructions` array may list:
8
20
 
9
21
  - relative paths / globs (e.g. `.cursor/rules/*.md`)
10
22
  - absolute paths
@@ -21,11 +33,11 @@ So a project config like:
21
33
  }
22
34
  ```
23
35
 
24
- can cause those files to be read and sent to the model provider (OpenCode prompt path and/or this provider’s Cursor `RequestContext`). That is intentional OpenCode parity, not a Cursor-only hole. There is no path allowlist that rejects absolute/`~/` instruction paths (matching OpenCode; OpenCode has not treated this as a defect).
36
+ can cause those files to be read and sent to the model provider (OpenCode prompt path and/or this provider's Cursor `RequestContext`). That is intentional OpenCode parity, not a Cursor-only hole. There is no path allowlist that rejects absolute/`~/` instruction paths (matching OpenCode; OpenCode has not treated this as a defect).
25
37
 
26
38
  Treat project `opencode.json` as **trusted**. Do not open untrusted repositories with project config enabled if that is unacceptable.
27
39
 
28
- ## Mitigation: disable project config
40
+ #### Mitigation: disable project config
29
41
 
30
42
  To ignore project-level OpenCode config (including project `instructions` and project `AGENTS.md` / `CLAUDE.md` / `CONTEXT.md` discovery), set:
31
43
 
@@ -37,6 +49,30 @@ OpenCode honors this flag when loading config and assembling prompts. This provi
37
49
 
38
50
  Truthy values: `1` or `true` (case-insensitive), same as OpenCode.
39
51
 
40
- ## Related hardening in this provider
52
+ #### Related hardening in this provider
41
53
 
42
54
  - Remote `instructions` URLs are **HTTPS-only** (`http://` is skipped). Redirects (including to a local proxy) are intentional and not blocked.
55
+
56
+ ### Out of Scope
57
+
58
+ | Category | Rationale |
59
+ | --- | --- |
60
+ | **Sandbox escapes** | The host permission system is not a sandbox (see above) |
61
+ | **Cursor / LLM data handling** | Data sent to Cursor is governed by Cursor's policies and your account terms |
62
+ | **MCP server behavior** | External MCP servers you configure are outside our trust boundary |
63
+ | **Malicious project config** | Users control their own configuration; trusted project `instructions` reading absolute/`~/` paths is expected OpenCode parity (see above) |
64
+ | **Host tool execution after user approval** | Actions the host runs under its normal permission prompts are expected behavior |
65
+
66
+ ---
67
+
68
+ ## Reporting Security Issues
69
+
70
+ Do not report a suspected vulnerability in a public GitHub issue, discussion, or pull request.
71
+
72
+ Use GitHub's private [vulnerability reporting form](https://github.com/oakimov/cursor-opencode-provider/security/advisories/new). This keeps the report and subsequent discussion private between the reporter and the repository maintainers.
73
+
74
+ Include the affected version, environment (OpenCode 1.x vs 2.0, plugin entrypoint), impact, reproduction steps, and any suggested mitigation. The initial message should not include credentials, personal data, or exploit code beyond what is needed to reproduce the issue. If the report requires sensitive supporting material, first send a minimal description and ask the maintainers to coordinate a suitable transfer method.
75
+
76
+ Reports must describe a concrete, reproducible security impact. Automated scanner output or model-generated speculation without validation may not receive a response.
77
+
78
+ We will make a reasonable effort to acknowledge valid reports, but we cannot promise a specific response or remediation timeline.
@@ -1,5 +1,6 @@
1
1
  import { type OpencodeToolDef } from "../protocol/tools.js";
2
2
  import { type OpencodeJson } from "./rules.js";
3
+ import { hostSkillFiles, loadBridgeSkills } from "./skills.js";
3
4
  export type BuildRequestContextInput = {
4
5
  workspaceRoot: string;
5
6
  tools?: OpencodeToolDef[];
@@ -13,6 +14,8 @@ export type BuildRequestContextInput = {
13
14
  mergedConfig?: OpencodeJson;
14
15
  /** Host system context to deliver as the frozen system-instructions rule. */
15
16
  systemInstructions?: SystemInstructions;
17
+ /** Optional host session id for `opencode.host.skills` list(). */
18
+ sessionID?: string;
16
19
  };
17
20
  /**
18
21
  * Host system context (OpenCode's system prompt plus this provider's
@@ -48,8 +51,8 @@ export declare function withSystemInstructions(context: Record<string, unknown>,
48
51
  export declare const DYNAMIC_REQUEST_CONTEXT_KEYS: readonly ["tools", "custom_subagents", "mcp_file_system_options", "mcp_meta_tool_options", "web_search_enabled", "web_fetch_enabled", "custom_subagents_info_complete", "mcp_file_system_info_complete", "mcp_info_complete", "hooks_additional_context"];
49
52
  export type DynamicRequestContextKey = typeof DYNAMIC_REQUEST_CONTEXT_KEYS[number];
50
53
  /**
51
- * The host system context (delivered as the system-instructions rule) already
52
- * carries these; never keep them on a frozen base.
54
+ * Derived from the system-instructions rule on every materialization (see
55
+ * `skills.ts`); never kept on a frozen base.
53
56
  */
54
57
  export declare const HOST_DUPLICATED_REQUEST_CONTEXT_KEYS: readonly ["agent_skills", "agent_skills_info_complete"];
55
58
  /**
@@ -59,9 +62,24 @@ export declare const HOST_DUPLICATED_REQUEST_CONTEXT_KEYS: readonly ["agent_skil
59
62
  * own directories.
60
63
  */
61
64
  export declare function buildRequestContext(input: BuildRequestContextInput): Promise<Record<string, unknown>>;
65
+ /** Locations for path-desc materialization (bridge, then remembered OC2 files). */
66
+ export declare function resolveSkillLocations(input: Pick<BuildRequestContextInput, "tools" | "sessionID">, workspaceRoot: string): Promise<{
67
+ bridgeSkills?: Awaited<ReturnType<typeof loadBridgeSkills>>;
68
+ skillFiles?: ReturnType<typeof hostSkillFiles>;
69
+ }>;
62
70
  /** Rediscover only capability/plugin sections that may change during a chat. */
63
71
  export declare function buildDynamicRequestContext(input: BuildRequestContextInput): Promise<Record<string, unknown>>;
64
- /** Keep expensive workspace state frozen while replacing every live capability field. */
65
- export declare function materializeRequestContext(base: Record<string, unknown>, dynamic: Record<string, unknown>): Record<string, unknown>;
72
+ export type MaterializeRequestContextOptions = {
73
+ tools?: OpencodeToolDef[];
74
+ skillFiles?: ReadonlyMap<string, string>;
75
+ bridgeSkills?: Awaited<ReturnType<typeof loadBridgeSkills>>;
76
+ };
77
+ /**
78
+ * Keep expensive workspace state frozen while replacing every live capability
79
+ * field. Skill locations (bridge / OpenCode 2 files / OpenCode 1 catalog
80
+ * paths) turn the skill catalog in the system-instructions rule into Cursor's
81
+ * path-desc `agent_skills`.
82
+ */
83
+ export declare function materializeRequestContext(base: Record<string, unknown>, dynamic: Record<string, unknown>, options?: MaterializeRequestContextOptions): Record<string, unknown>;
66
84
  /** Strip live capability fields before retaining/persisting a conversation base. */
67
85
  export declare function requestContextBase(context: Record<string, unknown>): Record<string, unknown>;
@@ -7,6 +7,7 @@ import { collectProjectLayout } from "./layout.js";
7
7
  import { buildEnv } from "./env.js";
8
8
  import { ensureOpencodeProjectDir } from "./paths.js";
9
9
  import { holdCapabilityOverlay } from "./overlay.js";
10
+ import { agentSkillsForCursor, applyAgentSkillsToContext, hostSkillFiles, loadBridgeSkills, skillToolAdvertised, } from "./skills.js";
10
11
  import { traceRequestContextPaths } from "../debug.js";
11
12
  /** Rule name Cursor shows for the host system context (not a file). */
12
13
  export const SYSTEM_INSTRUCTIONS_RULE_PATH = "OpenCode system instructions";
@@ -74,8 +75,8 @@ export const DYNAMIC_REQUEST_CONTEXT_KEYS = [
74
75
  "hooks_additional_context",
75
76
  ];
76
77
  /**
77
- * The host system context (delivered as the system-instructions rule) already
78
- * carries these; never keep them on a frozen base.
78
+ * Derived from the system-instructions rule on every materialization (see
79
+ * `skills.ts`); never kept on a frozen base.
79
80
  */
80
81
  export const HOST_DUPLICATED_REQUEST_CONTEXT_KEYS = [
81
82
  "agent_skills",
@@ -120,10 +121,26 @@ export async function buildRequestContext(input) {
120
121
  git_status_info_complete: true,
121
122
  };
122
123
  const base = withSystemInstructions(workspace, input.systemInstructions);
123
- const ctx = materializeRequestContext(base, dynamic);
124
+ const skillLocations = await resolveSkillLocations(input, workspaceRoot);
125
+ const ctx = materializeRequestContext(base, dynamic, {
126
+ tools: input.tools,
127
+ ...skillLocations,
128
+ });
124
129
  traceRequestContextPaths("buildRequestContext", ctx);
125
130
  return ctx;
126
131
  }
132
+ /** Locations for path-desc materialization (bridge, then remembered OC2 files). */
133
+ export async function resolveSkillLocations(input, workspaceRoot) {
134
+ if (!skillToolAdvertised(input.tools))
135
+ return {};
136
+ return {
137
+ bridgeSkills: await loadBridgeSkills({
138
+ workspaceRoot,
139
+ sessionID: input.sessionID,
140
+ }),
141
+ skillFiles: hostSkillFiles(workspaceRoot),
142
+ };
143
+ }
127
144
  async function buildDynamicRequestContextFromDiscovery(input, workspaceRoot, config) {
128
145
  const providerIdentifier = input.providerIdentifier ?? "opencode";
129
146
  const tools = input.tools ?? [];
@@ -184,8 +201,13 @@ export async function buildDynamicRequestContext(input) {
184
201
  : await loadMergedConfig(workspaceRoot);
185
202
  return buildDynamicRequestContextFromDiscovery(input, workspaceRoot, config);
186
203
  }
187
- /** Keep expensive workspace state frozen while replacing every live capability field. */
188
- export function materializeRequestContext(base, dynamic) {
204
+ /**
205
+ * Keep expensive workspace state frozen while replacing every live capability
206
+ * field. Skill locations (bridge / OpenCode 2 files / OpenCode 1 catalog
207
+ * paths) turn the skill catalog in the system-instructions rule into Cursor's
208
+ * path-desc `agent_skills`.
209
+ */
210
+ export function materializeRequestContext(base, dynamic, options) {
189
211
  const context = structuredClone(base);
190
212
  stripHostDuplicatedRequestContextFields(context);
191
213
  for (const key of DYNAMIC_REQUEST_CONTEXT_KEYS)
@@ -194,6 +216,12 @@ export function materializeRequestContext(base, dynamic) {
194
216
  if (Object.hasOwn(dynamic, key))
195
217
  context[key] = structuredClone(dynamic[key]);
196
218
  }
219
+ const skills = agentSkillsForCursor(systemInstructionsRuleText(context), {
220
+ skillToolAdvertised: skillToolAdvertised(options?.tools),
221
+ skillFiles: options?.skillFiles,
222
+ bridgeSkills: options?.bridgeSkills,
223
+ });
224
+ applyAgentSkillsToContext(context, skills);
197
225
  return context;
198
226
  }
199
227
  /** Strip live capability fields before retaining/persisting a conversation base. */
@@ -64,7 +64,12 @@ export function buildDynamicCatalogRoutingInstruction(options) {
64
64
  `- OpenCode host tools that are not in Cursor's native top-level list (including ${extras.join(" and ")}) ` +
65
65
  "are reached through GetDynamicTools / CallDynamicTool (or the host's equivalent dynamic catalog). " +
66
66
  "When a skill matches or project rules name an MCP server, discover and call those tools that way " +
67
- "before Grep/Shell fallbacks. Do not narrate that they are unavailable.",
67
+ "before Grep/Shell fallbacks. Do not narrate that they are unavailable. " +
68
+ // Cursor's server answers lookups and calls in its own `cursor` namespace
69
+ // (built-in tools such as GenerateImage) and rejects any other name there.
70
+ `Host tools are in namespace \`${DEFAULT_TOOL_SERVER}\`` +
71
+ (mcpServers.length > 0 ? " and MCP tools in their server's namespace" : "") +
72
+ "; namespace `cursor` holds only Cursor's built-in tools.",
68
73
  ];
69
74
  if (hasSkill) {
70
75
  // Mirror OpenCode 1 SystemPrompt.skills / OC2 SkillInstructions.render.
@@ -1,4 +1,4 @@
1
- import { buildDynamicRequestContext, buildRequestContext, materializeRequestContext, requestContextBase, withSystemInstructions, } from "./build.js";
1
+ import { buildDynamicRequestContext, buildRequestContext, materializeRequestContext, requestContextBase, resolveSkillLocations, withSystemInstructions, } from "./build.js";
2
2
  import { clearContextEpoch, endContextEpoch, resetContextEpochsForTests } from "./epoch.js";
3
3
  import { clearOverlayHold, resetOverlayHoldsForTests, transferOverlayHold, } from "./overlay.js";
4
4
  import { trace } from "../debug.js";
@@ -174,7 +174,11 @@ export async function getOrBuildRequestContext(conversationId, input, opts) {
174
174
  `authoritative=${input.systemInstructions?.authoritative ?? false}`);
175
175
  }
176
176
  const dynamic = await buildDynamicRequestContext(scoped);
177
- const materialized = rememberMaterialized(conversationId, materializeRequestContext(base, dynamic));
177
+ const skillLocations = await resolveSkillLocations(scoped, scoped.workspaceRoot);
178
+ const materialized = rememberMaterialized(conversationId, materializeRequestContext(base, dynamic, {
179
+ tools: scoped.tools,
180
+ ...skillLocations,
181
+ }));
178
182
  trace(`request_context: materialized conversationId=${conversationId} ` +
179
183
  `tools=${advertisedMetaToolCount(materialized.context)} ` +
180
184
  `reused=${materialized.reused}`);
@@ -4,3 +4,5 @@ export { clearFrozenRequestContext, getFrozenRequestContext, getOrBuildRequestCo
4
4
  export { MAX_OVERLAY_HOLDS, holdCapabilityOverlay, resetOverlayHoldsForTests, } from "./overlay.js";
5
5
  export { admitContextEpoch, appendMidConversationMessage, clearContextEpoch, endContextEpoch, getContextEpoch, MAX_CONTEXT_EPOCHS, resetContextEpochsForTests, type AdmitContextEpochInput, type AdmitContextEpochResult, type ContextEpoch, type ContextSourceSnapshot, } from "./epoch.js";
6
6
  export { HOST_PATH_BRIDGE, getHostCacheDirOverride, opencodeGlobalCacheDir, opencodeGlobalConfigDir, opencodeGlobalDataDir, hostGlobalDataDir, resolveHostCacheDir, setHostCacheDirOverride, } from "./paths.js";
7
+ export { HOST_SKILLS_BRIDGE, setHostSkillsBridgeForTests, type HostSkill, type OpenCodeSkillsBridge, } from "./skills-bridge.js";
8
+ export { agentSkillsForCursor, hostSkillFiles, rememberHostSkillFiles, resetHostSkillFilesForTests, skillToolAdvertised, } from "./skills.js";
@@ -4,3 +4,5 @@ export { clearFrozenRequestContext, getFrozenRequestContext, getOrBuildRequestCo
4
4
  export { MAX_OVERLAY_HOLDS, holdCapabilityOverlay, resetOverlayHoldsForTests, } from "./overlay.js";
5
5
  export { admitContextEpoch, appendMidConversationMessage, clearContextEpoch, endContextEpoch, getContextEpoch, MAX_CONTEXT_EPOCHS, resetContextEpochsForTests, } from "./epoch.js";
6
6
  export { HOST_PATH_BRIDGE, getHostCacheDirOverride, opencodeGlobalCacheDir, opencodeGlobalConfigDir, opencodeGlobalDataDir, hostGlobalDataDir, resolveHostCacheDir, setHostCacheDirOverride, } from "./paths.js";
7
+ export { HOST_SKILLS_BRIDGE, setHostSkillsBridgeForTests, } from "./skills-bridge.js";
8
+ export { agentSkillsForCursor, hostSkillFiles, rememberHostSkillFiles, resetHostSkillFilesForTests, skillToolAdvertised, } from "./skills.js";
@@ -9,7 +9,31 @@ export type OpenCodePathBridge = {
9
9
  /** Optional host-owned cache root; absent means native OpenCode defaults. */
10
10
  globalCacheDir?: () => string;
11
11
  configFileNames?: string[];
12
+ /**
13
+ * Optional host plan file for a session (the host's own plan location, which
14
+ * its `plan_exit` review reads). Absent on an installed bridge means the host
15
+ * defines none; without a bridge OpenCode's own location applies.
16
+ */
17
+ planFile?: (input: {
18
+ worktree: string;
19
+ vcs: boolean;
20
+ created: number;
21
+ slug: string;
22
+ }) => string | undefined;
12
23
  };
24
+ /**
25
+ * The plan file of a session: OpenCode's own `Session.plan` location
26
+ * (`<worktree>/.opencode/plans` in a VCS project, else `<data>/plans`, named
27
+ * `<created>-<slug>.md`), which its plan agent and `plan_exit` use. An injected
28
+ * host path bridge owns host paths, so with one installed only its `planFile`
29
+ * defines the location.
30
+ */
31
+ export declare function hostPlanFilePath(input: {
32
+ worktree: string;
33
+ vcs: boolean;
34
+ created: number;
35
+ slug: string;
36
+ }, env?: HostPathEnv): string | undefined;
13
37
  export declare function opencodeProjectConfigDirs(workspaceRoot: string): string[];
14
38
  export declare function opencodeGlobalConfigDirs(): string[];
15
39
  export declare function opencodeConfigFileNames(): string[];
@@ -41,25 +65,28 @@ export declare function opencodeGlobalDataDir(env?: HostPathEnv): string;
41
65
  /** Host-portable durable data root; falls back to native OpenCode. */
42
66
  export declare function hostGlobalDataDir(env?: HostPathEnv): string;
43
67
  /**
44
- * Directory for host plan files — always `<hostGlobalDataDir()>/plans`, and
45
- * never inside the user's repository.
46
- *
47
- * OpenCode's own `Session.plan` branches on VCS and puts plans in the worktree
48
- * (`<worktree>/.opencode/plans`) for a git project. The provider deliberately
49
- * does *not* mirror that branch. Writing there means a throwaway plan lands in
50
- * the user's tree untracked-but-unignored, and — because OpenCode installs
51
- * `@opencode-ai/plugin` into every `.opencode` directory it discovers walking up
52
- * from the cwd — creating that directory also bootstraps a project-local
53
- * `node_modules`. The provider must add nothing to a repository it did not
54
- * already contain.
68
+ * Select the entrypoint's native OpenCode plan directory. OpenCode 2.0 sets its
69
+ * Plan directory ({@link opencode2PlanDir}); OpenCode 1.x keeps the default.
70
+ * The returned disposer removes only this selection, so an older plugin setup
71
+ * disposed after a newer one leaves the newer selection in place. `undefined`
72
+ * removes all.
73
+ */
74
+ export declare function setNativePlansDir(dir: string | undefined): () => void;
75
+ /**
76
+ * OpenCode 2.0's Plan directory: `<home>/.opencode/plan`, where its Plan agent
77
+ * may write plan files (home is `OPENCODE_TEST_HOME`, else the OS home).
78
+ */
79
+ export declare function opencode2PlanDir(env?: HostPathEnv): string;
80
+ /**
81
+ * Directory for a new plan file when the session's own plan file
82
+ * ({@link hostPlanFilePath}) is not known.
55
83
  *
56
- * The global-data location is not a degraded fallback: it is the branch OpenCode
57
- * itself uses when there is no VCS, and its plan agent allow-lists that path for
58
- * `edit` / `external_directory` alongside the in-worktree one. {@link
59
- * hostGlobalDataDir} carries an optional injected host translation; without a
60
- * bridge it is the native OpenCode data root.
84
+ * An injected host path bridge owns host paths: `<globalDataDir()>/plans`.
85
+ * Otherwise the native OpenCode location of the running entrypoint: OpenCode
86
+ * 2.0's Plan directory, or OpenCode 1.x's no-VCS `Session.plan` base
87
+ * (`<data>/plans`).
61
88
  */
62
- export declare function hostPlansDir(_workspaceRoot?: string): string;
89
+ export declare function hostPlansDir(_workspaceRoot?: string, env?: HostPathEnv): string;
63
90
  /**
64
91
  * Cursor-compatible path slug (`/workspace/a/b` → `workspace-a-b`).
65
92
  * Used for per-workspace metadata under the host cache.