cursor-opencode-provider 0.7.6 → 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.
- package/CHANGELOG.md +79 -0
- package/README.md +36 -15
- package/SECURITY.md +43 -7
- package/dist/agent-url.js +2 -2
- package/dist/auth-renewal.d.ts +85 -0
- package/dist/auth-renewal.js +284 -0
- package/dist/auth.d.ts +38 -22
- package/dist/auth.js +79 -96
- package/dist/classic-tools.d.ts +15 -0
- package/dist/classic-tools.js +45 -0
- package/dist/context/build.d.ts +62 -7
- package/dist/context/build.js +127 -95
- package/dist/context/dynamic-catalog.d.ts +2 -58
- package/dist/context/dynamic-catalog.js +16 -161
- package/dist/context/epoch.d.ts +6 -4
- package/dist/context/epoch.js +22 -10
- package/dist/context/frozen.js +42 -15
- package/dist/context/index.d.ts +3 -1
- package/dist/context/index.js +3 -1
- package/dist/context/overlay.d.ts +2 -23
- package/dist/context/overlay.js +6 -30
- package/dist/context/paths.d.ts +51 -21
- package/dist/context/paths.js +78 -27
- package/dist/context/rules.d.ts +3 -15
- package/dist/context/rules.js +10 -186
- package/dist/context/skills-bridge.d.ts +22 -0
- package/dist/context/skills-bridge.js +20 -0
- package/dist/context/skills.d.ts +31 -8
- package/dist/context/skills.js +187 -86
- package/dist/debug.d.ts +2 -0
- package/dist/debug.js +4 -0
- package/dist/errors.d.ts +13 -0
- package/dist/errors.js +27 -3
- package/dist/host-agent-mode.d.ts +53 -4
- package/dist/host-agent-mode.js +119 -15
- package/dist/host-plan-file.d.ts +28 -0
- package/dist/host-plan-file.js +76 -0
- package/dist/image-input.d.ts +18 -0
- package/dist/image-input.js +119 -28
- package/dist/image-save.d.ts +10 -2
- package/dist/image-save.js +35 -23
- package/dist/index.d.ts +10 -0
- package/dist/language-model.d.ts +86 -25
- package/dist/language-model.js +1295 -369
- package/dist/model-config.d.ts +9 -0
- package/dist/model-config.js +28 -1
- package/dist/opencode2/catalog.d.ts +1 -0
- package/dist/opencode2/catalog.js +2 -0
- package/dist/opencode2/image-save-tool.d.ts +8 -0
- package/dist/opencode2/image-save-tool.js +84 -0
- package/dist/opencode2/integration.d.ts +30 -7
- package/dist/opencode2/integration.js +75 -46
- package/dist/opencode2/types.d.ts +15 -0
- package/dist/parallel-step.d.ts +65 -0
- package/dist/parallel-step.js +102 -0
- package/dist/plugin-opencode2.js +58 -56
- package/dist/plugin.d.ts +0 -10
- package/dist/plugin.js +207 -127
- package/dist/pricing-data.d.ts +14 -1
- package/dist/pricing-data.js +14 -1
- package/dist/protocol/ask-question.d.ts +12 -1
- package/dist/protocol/ask-question.js +77 -3
- package/dist/protocol/blob-store.js +3 -2
- package/dist/protocol/conversation-bind.js +1 -1
- package/dist/protocol/conversation-persistence.d.ts +2 -0
- package/dist/protocol/conversation-persistence.js +11 -1
- package/dist/protocol/conversation-state.d.ts +2 -0
- package/dist/protocol/conversation-state.js +19 -0
- package/dist/protocol/create-plan.d.ts +47 -25
- package/dist/protocol/create-plan.js +35 -24
- package/dist/protocol/interactions.d.ts +26 -17
- package/dist/protocol/interactions.js +80 -31
- package/dist/protocol/messages.js +96 -18
- package/dist/protocol/request.d.ts +16 -9
- package/dist/protocol/request.js +17 -25
- package/dist/protocol/switch-mode.d.ts +66 -27
- package/dist/protocol/switch-mode.js +147 -73
- package/dist/protocol/token-details.d.ts +2 -0
- package/dist/protocol/token-details.js +11 -1
- package/dist/protocol/tool-call-bridge.d.ts +4 -0
- package/dist/protocol/tool-call-bridge.js +14 -0
- package/dist/protocol/tools.d.ts +33 -11
- package/dist/protocol/tools.js +165 -103
- package/dist/protocol/turn-provenance.d.ts +52 -0
- package/dist/protocol/turn-provenance.js +174 -0
- package/dist/replay-safety.d.ts +6 -0
- package/dist/replay-safety.js +56 -6
- package/dist/session.d.ts +44 -2
- package/dist/session.js +45 -1
- package/dist/shared.d.ts +7 -0
- package/dist/shared.js +7 -0
- package/dist/todo-store.d.ts +2 -0
- package/dist/todo-store.js +19 -0
- package/dist/transport/connect.d.ts +2 -0
- package/dist/transport/connect.js +23 -6
- package/dist/usage.js +23 -6
- package/package.json +3 -2
- package/dist/context/agents.d.ts +0 -12
- package/dist/context/agents.js +0 -136
- package/dist/plan-execution-kickoff.d.ts +0 -57
- 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
|
|
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,
|
|
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
|
|
@@ -132,7 +134,7 @@ The provider never detects or names fork hosts. Compatibility layers own host id
|
|
|
132
134
|
| Model, version, and 24-hour conversation **cache** | `~/.cache/opencode/` | A host bridge may replace the root |
|
|
133
135
|
| Cursor **project metadata** (`agent-tools`, terminals, …) | `~/.cache/opencode/projects/<slug>/` | under `<host-cache>/projects/` |
|
|
134
136
|
| OpenCode **auth** (`auth.json`) | `~/.local/share/opencode/` | `$XDG_DATA_HOME/opencode/` when set |
|
|
135
|
-
| OpenCode **config** (AGENTS, skills, …) | `~/.config/opencode/` |
|
|
137
|
+
| OpenCode **config** (AGENTS, skills, …) | `~/.config/opencode/` | `$XDG_CONFIG_HOME/opencode/` when set, as OpenCode resolves it |
|
|
136
138
|
|
|
137
139
|
### Select a model
|
|
138
140
|
|
|
@@ -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.
|
|
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
|
|
|
@@ -220,7 +232,7 @@ const model = cursor.languageModel("composer-2.5")
|
|
|
220
232
|
// model implements AI SDK LanguageModelV3 (doStream / doGenerate)
|
|
221
233
|
```
|
|
222
234
|
|
|
223
|
-
|
|
235
|
+
Give one credential source. The provider uses the first present and never falls back from one to another: `getAccessToken` (an async function returning the current access token, called for every Run so a long-running process can renew its login; it receives `{ forceRefresh: true }` once after Cursor rejects a token), then `accessToken` (a JWT sent as-is, never renewed), then `apiKey` (a raw `crsr_...` key, exchanged for a JWT and exchanged again near its expiry, the way Cursor CLI renews an API-key login). Optional: `apiBaseURL`, `agentBaseURL`, `cacheDir`, `headers`, `telemetryEnabled`, `retry`, and `continuation`. `cacheDir` pins the host cache root for model/version caches and Cursor project metadata; when omitted, the provider uses an injected structural host path bridge or the native OpenCode XDG cache described in [Paths](#paths-host-cache). Transient failures resume from the latest checkpoint produced by that Run, matching Cursor CLI; without an eligible checkpoint, retries remain limited to replay-safe attempts so completed text or tool work is not duplicated. Once that budget is spent, or replay is unsafe, the terminal error does not re-arm OpenCode's outer retry loop. Pending-tool inactivity is renewed by OpenCode activity from the session or its descendants. The older `baseURL` option is still accepted as a legacy alias for `agentBaseURL`.
|
|
224
236
|
|
|
225
237
|
## Environment variables
|
|
226
238
|
|
|
@@ -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,
|
|
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
|
|
|
@@ -268,9 +280,9 @@ OpenCode
|
|
|
268
280
|
|
|
269
281
|
| Module | Role |
|
|
270
282
|
|--------|------|
|
|
271
|
-
| `src/plugin.ts` | Classic OpenCode hooks: provider registration, OAuth, API key exchange,
|
|
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
|
|
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 |
|
|
@@ -280,7 +292,8 @@ OpenCode
|
|
|
280
292
|
| `src/language-model.ts` | AI SDK `LanguageModelV3` adapter (`doStream`, `doGenerate`) |
|
|
281
293
|
| `src/session.ts` | Held-open agent Run session and pending exec correlation |
|
|
282
294
|
| `src/debug.ts` | Opt-in wire-level debug logging (`CURSOR_PROVIDER_DEBUG`) |
|
|
283
|
-
| `src/auth.ts` | PKCE OAuth, API key exchange,
|
|
295
|
+
| `src/auth.ts` | PKCE OAuth, API key exchange, browser-login session refresh (`/oauth/token`) |
|
|
296
|
+
| `src/auth-renewal.ts` | Credential renewal policy: session and API-key renewal kept separate, backoff, on-demand renewal, Bearer resolution |
|
|
284
297
|
| `src/models.ts` | `AvailableModels` fetch and `cursor-models.json` cache |
|
|
285
298
|
| `src/protocol/conversation-persistence.ts` | Atomic per-session restart snapshots under the 24-hour `cursor-conversations/` cache |
|
|
286
299
|
| `src/context/paths.ts` | Host cache root + Cursor project metadata under `<host-cache>/projects/<slug>/` |
|
|
@@ -293,7 +306,9 @@ OpenCode
|
|
|
293
306
|
|
|
294
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.
|
|
295
308
|
|
|
296
|
-
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 MCP descriptors and are reached through GetDynamicTools / CallDynamicTool. 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
|
|
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.
|
|
297
312
|
|
|
298
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`).
|
|
299
314
|
|
|
@@ -346,7 +361,7 @@ The package root intentionally stays plugin-safe for OpenCode's classic loader.
|
|
|
346
361
|
|---------|-------------|
|
|
347
362
|
| No Cursor models in the picker (OpenCode 1.x) | Confirm Cursor auth (`opencode auth login` → **cursor**). Restart OpenCode — if auth is present and the cache is empty, models are fetched on startup. Confirm `provider.cursor.npm` is the package name (or a built `file://…/dist/index.js`). See [OpenCode 1.x troubleshooting](docs/opencode-1.md#troubleshooting). |
|
|
348
363
|
| No Cursor models in the picker (`opencode2`) | Confirm `/connect` → **Cursor** (or shared `auth.json`). Use a dedicated `OPENCODE_CONFIG_DIR` (not mixed with 1.x). Ensure `$OPENCODE_CONFIG_DIR/plugins/cursor/` is a package directory re-exporting `plugin/opencode2` (not a bare `.js`). After auth, Cursor models appear in the picker from the in-memory inventory — no `opencode.json` model list is required. Filter by provider **Cursor** (`time.released` is `0`, so models sort last). Leftover `providers.cursor` fights that inventory — [safe transition](docs/opencode-2.md#safe-transition). |
|
|
349
|
-
| Auth / 401 errors mid-session |
|
|
364
|
+
| Auth / 401 errors mid-session | Logins renew themselves: a browser login (60-day session) is renewed through Cursor's `/oauth/token` once less than 1272 h of it remain, as Cursor's IDE decides, whenever a request needs a token (there is no background timer, as with OpenCode's own OAuth providers); an API-key login exchanges its stored key again within five minutes of the exchanged JWT's expiry, as Cursor CLI does. The two never stand in for each other. Sign in again when the error says the session was ended, the key was rejected, or a sign-in policy blocks it. An API-key login saved before the key was kept (`metadata.apiKey`) cannot be renewed and needs one fresh API-key sign-in. Renewal attempts and failures go to the debug log (`CURSOR_PROVIDER_DEBUG`). |
|
|
350
365
|
| Local OpenCode 2.0 still runs the published package | Set `CURSOR_OPENCODE2_DEV_ENTRY` to an absolute `…/dist/index.js` path **before** starting the daemon, persist it with `opencode2 service set env CURSOR_OPENCODE2_DEV_ENTRY …`, rebuild (`bun run build`), then `opencode2 service restart`. Loading only `dist/plugin-opencode2.js` is not enough — without the env var, 2.0 still `npm install`s the published package into the host cache. |
|
|
351
366
|
| “Too many connections from different devices” | Device IDs are derived from stable OS identifiers (same approach as the Cursor CLI). Avoid running multiple clients that invent different machine fingerprints for the same account. |
|
|
352
367
|
| Empty or stale model list | Delete `<host-cache>/cursor-models.json` (native default `~/.cache/opencode/`) and restart OpenCode. A compatibility layer may supply a different host-cache root through the structural path bridge. Existing Cursor auth is enough to refill the cache; re-login only if auth itself is broken. Cache TTL is 24h; a failed background refresh keeps serving the previous cache. |
|
|
@@ -365,9 +380,10 @@ Project `instructions` may reference absolute or `~/` paths (OpenCode parity). S
|
|
|
365
380
|
|
|
366
381
|
## Known limitations
|
|
367
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.
|
|
368
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.
|
|
369
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.
|
|
370
|
-
- **`request_context` from OpenCode** — each Run sends Cursor `RequestContext`
|
|
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.
|
|
371
387
|
- **Configured MCP tools keep their upstream server id** — OpenCode builtins and plugin/custom tools are advertised under a synthetic `opencode` MCP server. Tools whose flattened name matches an MCP server in merged `opencode.json` configuration (`github_create_pull_request`, …) are grouped into that server's `mcp_descriptors` / `provider_identifier` (`github`, …). Unknown underscore-containing names stay under `opencode` rather than being guessed incorrectly. Cursor's MCP-state exec probe is answered from the same advertised descriptors before the actual tool request, using the full canonical tool-definition identity required by native `get_mcp_tools` / GetDynamicTools; exec still reconstructs the full OpenCode tool id. That discovery call is Cursor-native (not an OpenCode catalog tool). Oversized catalogs spill through `write_args` into `agent-tools/<uuid>.txt`; `WriteSuccess.path` echoes the requested path so the model receives a real `filePath` instead of an empty spill.
|
|
372
388
|
- **Display completions are notifications, not execution requests** — Cursor `tool_call_*` frames use a typed `ToolCall` oneof. The provider decodes them for diagnostics but only mirrors finalized todo/plan state (`update_todos_tool_call` → `todowrite`, `read_todos_tool_call` → `todoread`, `create_plan_tool_call` → `todowrite`); the completed payload already contains the authoritative final list. Interactive, data-returning, and side-effecting completions are never replayed as new tools because their result could not be returned to Cursor. Exec-backed native subagent/Task and Pi read/bash/edit/write/grep/find/ls calls use their typed request/result fields instead. Unknown display variants are logged. All 37 Cursor CLI exec request/result pairs are inventoried by field and name. Known-but-unsupported variants are soft-denied on the held Run with a populated typed error/`rejected`/`allowlisted:false` result (or `throw` when the CLI has no typed error shape) so the turn continues; unknown future request fields still fail the Run explicitly rather than receiving a guessed response that could deadlock it.
|
|
373
389
|
- **Progress-only fragments can get one continuation** — if a real tool-enabled turn ends with a short progress fragment ("Checking the workspace", "Looking into it now") and no host tool call, the provider opens one follow-up Run with a synthetic user nudge so the model can call a listed tool or finish the answer. Complete answers that merely start with those verbs are not continued. Title/compaction turns (`allowTools=false`) never continue. If the continuation fails, the original `turn_ended` is still reported as a successful stop.
|
|
@@ -376,9 +392,10 @@ Project `instructions` may reference absolute or `~/` paths (OpenCode parity). S
|
|
|
376
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.
|
|
377
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`.
|
|
378
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.
|
|
379
|
-
- **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;
|
|
380
|
-
- **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
|
|
381
|
-
- **
|
|
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.
|
|
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.
|
|
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.
|
|
382
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.
|
|
383
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.
|
|
384
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.
|
|
@@ -387,6 +404,10 @@ Project `instructions` may reference absolute or `~/` paths (OpenCode parity). S
|
|
|
387
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`.
|
|
388
405
|
- **No fallback models** — if Cursor’s `AvailableModels` API is unreachable and there is no local cache, the provider exposes no models.
|
|
389
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
|
+
|
|
390
411
|
## License
|
|
391
412
|
|
|
392
413
|
MIT
|
package/SECURITY.md
CHANGED
|
@@ -1,10 +1,22 @@
|
|
|
1
|
-
# Security
|
|
1
|
+
# Security
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
## Threat Model
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
### Overview
|
|
6
6
|
|
|
7
|
-
OpenCode
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
package/dist/agent-url.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
2
|
import { fetchAgentUrl } from "./transport/connect.js";
|
|
3
|
-
import { trace } from "./debug.js";
|
|
3
|
+
import { errorMessage, trace } from "./debug.js";
|
|
4
4
|
import { CURSOR_API_HOST } from "./shared.js";
|
|
5
5
|
const DEFAULT_API_BASE = `https://${CURSOR_API_HOST}`;
|
|
6
6
|
// In-process memo of the region-specific Run stream origin (agentnUrl). Resolved
|
|
@@ -59,7 +59,7 @@ export async function resolveAgentUrl(token, options = {}) {
|
|
|
59
59
|
return url;
|
|
60
60
|
}
|
|
61
61
|
catch (err) {
|
|
62
|
-
const reason =
|
|
62
|
+
const reason = errorMessage(err);
|
|
63
63
|
trace(`agent-url: GetServerConfig failed (${reason}); no fallback agent host will be used`);
|
|
64
64
|
throw err;
|
|
65
65
|
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
/** Cursor IDE renews a session once less than this remains (`hir = 1272h`). */
|
|
2
|
+
export declare const SESSION_RENEWAL_WINDOW_MS: number;
|
|
3
|
+
/** Never renew a session token younger than this, whatever its lifetime. */
|
|
4
|
+
export declare const SESSION_MIN_AGE_MS = 3600000;
|
|
5
|
+
export type TokenTimes = {
|
|
6
|
+
issuedAtMs?: number;
|
|
7
|
+
expiresAtMs?: number;
|
|
8
|
+
};
|
|
9
|
+
/**
|
|
10
|
+
* Issue and expiry times of a Cursor JWT. Cursor session tokens carry the
|
|
11
|
+
* issue time in a `time` claim (seconds, as a string); standard `iat` is used
|
|
12
|
+
* when present instead.
|
|
13
|
+
*/
|
|
14
|
+
export declare function tokenTimes(token: string): TokenTimes;
|
|
15
|
+
/**
|
|
16
|
+
* When a session token is due for renewal: 1272 h before expiry, but never
|
|
17
|
+
* within an hour of issue. Undefined when the token has no readable expiry.
|
|
18
|
+
*/
|
|
19
|
+
export declare function sessionRenewalDueAt(token: string): number | undefined;
|
|
20
|
+
export type SessionTokens = {
|
|
21
|
+
accessToken: string;
|
|
22
|
+
refreshToken: string;
|
|
23
|
+
};
|
|
24
|
+
export type SessionRenewal = {
|
|
25
|
+
accessToken: string;
|
|
26
|
+
/** True when `accessToken` differs from the stored one and should be persisted. */
|
|
27
|
+
renewed: boolean;
|
|
28
|
+
/** Set while a failed renewal is backing off: when the next attempt is allowed. */
|
|
29
|
+
retryAt?: number;
|
|
30
|
+
};
|
|
31
|
+
/**
|
|
32
|
+
* Follow renewals this process already made, so a caller still holding an
|
|
33
|
+
* older stored credential (persisting failed, or a host that persists only
|
|
34
|
+
* later) gets the newest token instead of renewing again.
|
|
35
|
+
*/
|
|
36
|
+
export declare function latestSessionTokens(tokens: SessionTokens): SessionTokens;
|
|
37
|
+
export declare function isSessionRenewalDue(token: string, now?: number): boolean;
|
|
38
|
+
/**
|
|
39
|
+
* Return a usable session token, renewing it first when it is due (or when
|
|
40
|
+
* `force` is set after Cursor rejected it). Renewal failures that leave the
|
|
41
|
+
* current token valid keep it; a session Cursor ended, or one that expired
|
|
42
|
+
* and could not be renewed, raises `CursorAuthError`.
|
|
43
|
+
*/
|
|
44
|
+
export declare function renewSessionIfDue(tokens: SessionTokens, options?: {
|
|
45
|
+
baseUrl?: string;
|
|
46
|
+
force?: boolean;
|
|
47
|
+
}): Promise<SessionRenewal>;
|
|
48
|
+
export type ApiKeyToken = {
|
|
49
|
+
accessToken: string;
|
|
50
|
+
/** True when `accessToken` differs from the stored `seed` and should be persisted. */
|
|
51
|
+
renewed: boolean;
|
|
52
|
+
};
|
|
53
|
+
/**
|
|
54
|
+
* JWT for a raw `crsr_` API key: the cached or `seed` JWT while it is not
|
|
55
|
+
* within five minutes of expiry, otherwise a fresh exchange. Cached per API
|
|
56
|
+
* base URL and key. Never returns the raw key itself.
|
|
57
|
+
*/
|
|
58
|
+
export declare function resolveApiKeyToken(apiKey: string, options?: {
|
|
59
|
+
baseUrl?: string;
|
|
60
|
+
seed?: string;
|
|
61
|
+
force?: boolean;
|
|
62
|
+
}): Promise<ApiKeyToken>;
|
|
63
|
+
export type AccessTokenRequest = {
|
|
64
|
+
forceRefresh?: boolean;
|
|
65
|
+
};
|
|
66
|
+
/** Host-supplied source of the current Cursor access token. */
|
|
67
|
+
export type AccessTokenProvider = (request?: AccessTokenRequest) => Promise<string>;
|
|
68
|
+
/**
|
|
69
|
+
* Bearer token for a Cursor request. One credential source is used, in this
|
|
70
|
+
* order, and none of them falls back to another:
|
|
71
|
+
*
|
|
72
|
+
* 1. `getAccessToken` — the host resolves (and renews) the token per request.
|
|
73
|
+
* 2. `accessToken` — sent as-is.
|
|
74
|
+
* 3. `apiKey` — a raw `crsr_` key is exchanged and renewed; any other value is
|
|
75
|
+
* a token a host forwarded through the generic API-key slot, sent as-is.
|
|
76
|
+
*/
|
|
77
|
+
export declare function resolveBearerToken(input: {
|
|
78
|
+
getAccessToken?: AccessTokenProvider;
|
|
79
|
+
accessToken?: string;
|
|
80
|
+
apiKey?: string;
|
|
81
|
+
baseUrl?: string;
|
|
82
|
+
forceRefresh?: boolean;
|
|
83
|
+
}): Promise<string>;
|
|
84
|
+
/** Reset all renewal state (tests). */
|
|
85
|
+
export declare function resetAuthRenewalState(): void;
|