cursor-opencode-provider 0.6.3 → 0.6.6
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/README.md +17 -6
- package/dist/auth.js +28 -11
- package/dist/language-model.d.ts +2 -0
- package/dist/language-model.js +280 -69
- package/dist/model-config.d.ts +3 -1
- package/dist/model-config.js +83 -19
- package/dist/models.d.ts +8 -0
- package/dist/models.js +8 -4
- package/dist/opencode2/catalog.js +4 -3
- package/dist/pricing-data.d.ts +51 -11
- package/dist/pricing-data.js +51 -11
- package/dist/pricing.d.ts +10 -4
- package/dist/pricing.js +17 -8
- package/dist/protocol/exec-variants.d.ts +5 -0
- package/dist/protocol/exec-variants.js +17 -2
- package/dist/protocol/git-diff.d.ts +33 -0
- package/dist/protocol/git-diff.js +372 -0
- package/dist/protocol/messages.js +229 -6
- package/dist/protocol/progress-continuation.d.ts +25 -0
- package/dist/protocol/progress-continuation.js +92 -0
- package/dist/protocol/tools.d.ts +6 -0
- package/dist/protocol/tools.js +216 -3
- package/dist/protocol/workspace-grounding.d.ts +1 -0
- package/dist/protocol/workspace-grounding.js +8 -0
- package/dist/session.d.ts +11 -2
- package/dist/session.js +34 -5
- package/dist/usage.d.ts +14 -0
- package/dist/usage.js +26 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -287,6 +287,16 @@ OpenCode’s context limit is static per model entry, while Cursor’s long-cont
|
|
|
287
287
|
|
|
288
288
|
The Run still uses Cursor’s original model id; OpenCode’s synthetic `-1m` id is only for picking and limits.
|
|
289
289
|
|
|
290
|
+
#### Fast variants with distinct prices
|
|
291
|
+
|
|
292
|
+
When Cursor publishes a different Fast rate for a model (currently Composer and Grok), the plugin emits a separate OpenCode entry `<model-id>-fast` (for example `composer-2.5-fast`) using the same pattern as `-1m`:
|
|
293
|
+
|
|
294
|
+
- only the Fast variants in its picker, with `" Fast"` in the catalog name
|
|
295
|
+
- the real Cursor model id in `options.cursorModelId`, plus default Fast variant parameters
|
|
296
|
+
- its own `cost` row (`composer-2.5-fast` is not nested under `composer-2.5`)
|
|
297
|
+
|
|
298
|
+
Claude Fast variants stay on the base entry: Cursor does not publish a separate Fast price for them, so they remain picker variants instead of a second catalog id. Combinations with long context use `<id>-1m-fast`.
|
|
299
|
+
|
|
290
300
|
#### Context limit sources
|
|
291
301
|
|
|
292
302
|
The provider resolves each catalog context limit in this order:
|
|
@@ -372,7 +382,7 @@ bun run generate:pricing # refresh src/pricing-data.ts from Cursor docs
|
|
|
372
382
|
bun run check:pricing # fixture coverage for known model ids
|
|
373
383
|
```
|
|
374
384
|
|
|
375
|
-
|
|
385
|
+
**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`.
|
|
376
386
|
|
|
377
387
|
## Architecture
|
|
378
388
|
|
|
@@ -466,10 +476,10 @@ The package root intentionally stays plugin-safe for OpenCode's classic loader.
|
|
|
466
476
|
| Stream hangs or HTTP/2 errors | The provider keeps Cursor's Run open across OpenCode tool calls, rotates aged shared connections, resumes transient interruptions from the latest eligible Cursor checkpoint, and falls back to a fresh-history rebase only before stateful output when no checkpoint exists. Repeated interruption is surfaced as an error instead of a false successful stop; retry the turn after checking connectivity. With debug logging enabled, look for `Run interrupted`, `resuming … checkpoint`, or `rebasing fresh Run`. Restart OpenCode after rebuilding a local `file://` install. |
|
|
467
477
|
| No response / silent 200 + close | HTTP 200 alone is not a successful agent turn: the provider now requires Cursor's explicit `turn_ended`, captures HTTP/2 trailers/GOAWAY, and recovers once from bare EOF. The Run host still comes from in-memory `GetServerConfig` resolution; set `CURSOR_PROVIDER_DEBUG=1` to confirm the host and termination reason. |
|
|
468
478
|
| Visible `<shell_metadata>` timeout text | Rebuild and restart a local install. Cursor's shell timeout is carried on its exec request; the provider now removes OpenCode's internal timeout envelope before it is rendered or stored, then returns Cursor's typed timeout or background-handoff event instead of treating the text as successful stdout. |
|
|
469
|
-
| `Unsupported Cursor exec variant …` | The error names the canonical Cursor CLI request field, its expected result field, and this provider's handling classification. `handling=unsupported`
|
|
479
|
+
| `Unsupported Cursor exec variant …` | The error names the canonical Cursor CLI request field, its expected result field, and this provider's handling classification. Known `handling=unsupported` variants (Cursor-native capabilities without a safe OpenCode AI SDK bridge) are now soft-denied on the wire with a populated typed error/`rejected`/`allowlisted:false` result and the Run continues — look for `exec: SOFT-DENIED …` in the debug log and a `Workspace root: …` suffix in the deny text — while `unknown request field` still hard-fails as protocol drift; `handling=opencode-tool` or `provider-control` indicates a provider decoder/dispatch regression. Enable the debug log and report the full named error. |
|
|
470
480
|
| Need wire-level logs | Set `CURSOR_PROVIDER_DEBUG=1` (optional `CURSOR_PROVIDER_DEBUG_FILE`; the default is `debug-<pid>.log` under `$TMPDIR/cursor-provider-logs-<uid>/`) and reproduce the issue. Every completed turn emits a `turn usage validation: status=ok|mismatch` line confirming token-accounting integrity and a `cache diagnosis:` line showing warm/cold checkpoint continuity, raw read/write/uncached tokens, cache-read coverage of the prior context, context/category deltas, stable RequestContext hashes, and Run step/tool counts. Checkpoint lines include Cursor's current category totals. Cursor does not expose per-model-call cache splits, so the diagnostic labels that value unavailable instead of inferring it from aggregate `TurnEnded` counters. |
|
|
471
|
-
| OpenCode token totals
|
|
472
|
-
| Context percentage jumps after a long tool turn or compaction | Cursor reports its authoritative context snapshot in checkpoint `token_details`. The provider makes
|
|
481
|
+
| OpenCode token totals stay stuck until the turn ends | OpenCode TUI/GUI display the last assistant message with non-zero output, they do not sum occupancy. Tool-call steps emit Cursor's last-known checkpoint occupancy (with `output > 0` so the footer accepts it) and `providerMetadata.copilot.totalNanoAiu: 0` so those snapshots are not billed. The billed occupancy is emitted once at `TurnEnded`. If a checkpoint has not arrived yet, the previous turn's occupancy stays visible as stale. With no known snapshot, usage stays zero instead of mislabeling estimates or `TurnEnded` as context. Raw request counters remain in `providerMetadata.cursor`. |
|
|
482
|
+
| Context percentage jumps after a long tool turn or compaction | Cursor reports its authoritative context snapshot in checkpoint `token_details`. The provider makes AI SDK input + output equal `usedTokens`, proportionally partitions input into cached/uncached subsets, and exposes `maxTokens`, percentage, and category breakdown under `providerMetadata.cursor.context`. Mid-turn tool steps publish that occupancy as a display-only snapshot; compaction naturally replaces the prior snapshot with Cursor's smaller total. |
|
|
473
483
|
| Cache **write** tokens always show as `0` | Cursor's `TurnEnded` protobuf includes `cache_write` (field 4), but captured Runs set it to `0` even when `cache_read` is large. The provider forwards the request-local value when Cursor supplies one; it cannot infer a missing write count. With debug logging, confirm `turn_ended raw wire fields: … f4:wt0=0`. |
|
|
474
484
|
|
|
475
485
|
## Security
|
|
@@ -479,10 +489,11 @@ Project `instructions` may reference absolute or `~/` paths (OpenCode parity). S
|
|
|
479
489
|
## Known limitations
|
|
480
490
|
|
|
481
491
|
- **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.
|
|
482
|
-
- **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`; output/reasoning remain based on `TurnEnded`, while uncached/cache-read/cache-write input are proportionally normalized to the remaining context input. OpenCode
|
|
492
|
+
- **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.
|
|
483
493
|
- **`request_context` from OpenCode** — each Run sends Cursor `RequestContext` built from OpenCode project context (workspace env, `AGENTS.md` / `instructions`, `.opencode` agents/skills/plugins, git, layout, plus `.claude`/`.agents` skill fallbacks). Expensive workspace state (rules, environment, repository, git, and layout) is frozen per Cursor conversation and persisted for restart recovery. Tools/MCP descriptors, skills, subagents, and plugin metadata are rediscovered and overlaid each Run; unchanged overlays reuse byte-identical encoded context, while enabling/disabling a capability becomes visible on the next turn. 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). Same discovery as OpenCode — including `.cursor/` paths only when listed in `instructions`. 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.
|
|
484
494
|
- **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`; exec still reconstructs the full OpenCode tool id.
|
|
485
|
-
- **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` / `create_plan_tool_call`) into advertised OpenCode `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
|
|
495
|
+
- **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` / `create_plan_tool_call`) into advertised OpenCode `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.
|
|
496
|
+
- **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.
|
|
486
497
|
- **Tool availability is per OpenCode agent** — Cursor can request native capabilities such as Task even when a child or restricted OpenCode agent did not advertise the corresponding host tool. The provider prompts Cursor with the exact current catalog and checks every decoded host-tool exec request against it. An unavailable request is answered on Cursor's correlated typed result channel and is never emitted as OpenCode's `invalid` tool.
|
|
487
498
|
- **Host web tools use collision-safe aliases** — Cursor sees `custom_websearch` / `custom_webfetch`, and the held Run maps each alias back to an executable OpenCode tool with its schema, permission check, and correlated result intact. The plugin registers its search fallback directly as `custom_websearch`, avoiding OpenCode's reserved `websearch` id filter for third-party providers and taking precedence over MCP search providers such as Brave without host environment configuration. Exact host `webfetch` is translated the same way. Cursor's UI-bound native web interactions stay disabled because their approval replies cannot carry OpenCode tool results.
|
|
488
499
|
- **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.
|
package/dist/auth.js
CHANGED
|
@@ -139,9 +139,11 @@ export async function refreshAccessToken(refreshToken, baseUrl = API_BASE) {
|
|
|
139
139
|
// Cache JWTs obtained via API-key exchange so doStream doesn't re-exchange
|
|
140
140
|
// on every turn when the caller only supplied `apiKey`.
|
|
141
141
|
const _apiKeyTokenCache = new Map();
|
|
142
|
+
const inflightBearer = new Map();
|
|
142
143
|
/** Clear the apiKey→JWT cache (tests). */
|
|
143
144
|
export function clearBearerTokenCache() {
|
|
144
145
|
_apiKeyTokenCache.clear();
|
|
146
|
+
inflightBearer.clear();
|
|
145
147
|
}
|
|
146
148
|
/**
|
|
147
149
|
* Resolve a Bearer JWT for Cursor API calls. Prefer an already-exchanged
|
|
@@ -168,19 +170,34 @@ export async function resolveBearerToken(input) {
|
|
|
168
170
|
if (cached && !isExpiringSoon(cached.accessToken)) {
|
|
169
171
|
return cached.accessToken;
|
|
170
172
|
}
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
173
|
+
const inflightKey = `${baseUrl}\0${input.apiKey}`;
|
|
174
|
+
const existing = inflightBearer.get(inflightKey);
|
|
175
|
+
if (existing)
|
|
176
|
+
return existing;
|
|
177
|
+
async function refreshOrExchange() {
|
|
178
|
+
if (cached) {
|
|
179
|
+
try {
|
|
180
|
+
const refreshed = await refreshAccessToken(cached.refreshToken, baseUrl);
|
|
181
|
+
_apiKeyTokenCache.set(input.apiKey, refreshed);
|
|
182
|
+
return refreshed.accessToken;
|
|
183
|
+
}
|
|
184
|
+
catch {
|
|
185
|
+
// Fall through to a fresh exchange.
|
|
186
|
+
}
|
|
179
187
|
}
|
|
188
|
+
const pair = await exchangeApiKey(input.apiKey, baseUrl);
|
|
189
|
+
_apiKeyTokenCache.set(input.apiKey, pair);
|
|
190
|
+
return pair.accessToken;
|
|
191
|
+
}
|
|
192
|
+
const pending = refreshOrExchange();
|
|
193
|
+
inflightBearer.set(inflightKey, pending);
|
|
194
|
+
try {
|
|
195
|
+
return await pending;
|
|
196
|
+
}
|
|
197
|
+
finally {
|
|
198
|
+
if (inflightBearer.get(inflightKey) === pending)
|
|
199
|
+
inflightBearer.delete(inflightKey);
|
|
180
200
|
}
|
|
181
|
-
const pair = await exchangeApiKey(input.apiKey, baseUrl);
|
|
182
|
-
_apiKeyTokenCache.set(input.apiKey, pair);
|
|
183
|
-
return pair.accessToken;
|
|
184
201
|
}
|
|
185
202
|
// ── Mode C: PKCE browser login ──
|
|
186
203
|
async function sha256(data) {
|
package/dist/language-model.d.ts
CHANGED
|
@@ -37,6 +37,8 @@ export type CursorRunRecovery = {
|
|
|
37
37
|
conversationId: string;
|
|
38
38
|
checkpoint: Uint8Array;
|
|
39
39
|
};
|
|
40
|
+
/** Start (or replace) the per-session heartbeat. In-flight writes from a prior attach are ignored. */
|
|
41
|
+
export declare function attachSessionHeartbeat(session: CursorSession): void;
|
|
40
42
|
/**
|
|
41
43
|
* OpenCode re-sends the full tool-result history on every continuation. Prefer
|
|
42
44
|
* the newest result that still has a live pending exec on its tagged session.
|