cursor-opencode-provider 0.4.3 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +17 -12
- package/dist/context/build.d.ts +8 -0
- package/dist/context/build.js +72 -23
- package/dist/context/frozen.d.ts +29 -0
- package/dist/context/frozen.js +168 -0
- package/dist/context/index.d.ts +2 -1
- package/dist/context/index.js +2 -1
- package/dist/context/rules.d.ts +1 -0
- package/dist/context/rules.js +1 -1
- package/dist/language-model.d.ts +10 -9
- package/dist/language-model.js +387 -118
- package/dist/protocol/blob-reachability.d.ts +15 -0
- package/dist/protocol/blob-reachability.js +140 -0
- package/dist/protocol/blob-store.d.ts +37 -0
- package/dist/protocol/blob-store.js +115 -0
- package/dist/protocol/conversation-bind.d.ts +13 -0
- package/dist/protocol/conversation-bind.js +27 -1
- package/dist/protocol/conversation-persistence.d.ts +32 -0
- package/dist/protocol/conversation-persistence.js +504 -0
- package/dist/protocol/conversation-state.d.ts +16 -0
- package/dist/protocol/conversation-state.js +62 -0
- package/dist/protocol/kv.d.ts +1 -0
- package/dist/protocol/kv.js +2 -0
- package/dist/protocol/messages.d.ts +2 -0
- package/dist/protocol/messages.js +30 -2
- package/dist/protocol/request.d.ts +2 -0
- package/dist/protocol/request.js +2 -0
- package/dist/protocol/token-details.d.ts +33 -0
- package/dist/protocol/token-details.js +155 -0
- package/dist/protocol/tools.d.ts +17 -0
- package/dist/protocol/tools.js +76 -0
- package/dist/session.d.ts +39 -11
- package/dist/shared.d.ts +3 -0
- package/dist/shared.js +3 -0
- package/dist/usage.d.ts +40 -35
- package/dist/usage.js +199 -68
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -243,7 +243,7 @@ Choose the **cursor** provider, then one of:
|
|
|
243
243
|
| **Cursor account (browser login)** | PKCE OAuth — opens cursor.com to sign in |
|
|
244
244
|
| **API key** | Paste a key from [cursor.com/settings](https://cursor.com/settings) (`crsr_...`) |
|
|
245
245
|
|
|
246
|
-
After login, the plugin fetches your available models and writes them to `<host-cache>/cursor-models.json` (default `~/.cache/opencode/`). On later startups, a missing, empty, expired, or old-schema cache is refreshed during config load when Cursor auth is available; an existing stale cache remains usable if refresh fails.
|
|
246
|
+
After login, the plugin fetches your available models and writes them to `<host-cache>/cursor-models.json` (default `~/.cache/opencode/`). On later startups, a missing, empty, expired, or old-schema cache is refreshed during config load when Cursor auth is available; an existing stale cache remains usable if refresh fails. Completed conversation state is stored separately under `<host-cache>/cursor-conversations/`, with one independently replaced gzip-compressed protobuf snapshot per stable OpenCode session. Snapshots are refreshed at each successful Cursor `TurnEnded` and restored after provider restarts; records not updated for more than 24 hours are removed when the provider starts. Cursor's checkpoint is an index of content-addressed KV blobs rather than a self-contained transcript, so the snapshot retains the blobs reachable from the latest checkpoint and discards superseded blobs. This private Cursor state is not a copy of OpenCode's session database.
|
|
247
247
|
|
|
248
248
|
### Paths (host cache)
|
|
249
249
|
|
|
@@ -259,7 +259,7 @@ must not redirect a native OpenCode process into that host's cache.
|
|
|
259
259
|
|
|
260
260
|
| Kind | Default (OpenCode) | Notes |
|
|
261
261
|
|------|--------------------|-------|
|
|
262
|
-
| Model
|
|
262
|
+
| Model, version, and 24-hour conversation **cache** | `~/.cache/opencode/` | MiMo: `$MIMOCODE_HOME/cache` or `~/.cache/mimocode/`; Kilo: `~/.cache/kilo/` |
|
|
263
263
|
| Cursor **project metadata** (`agent-tools`, terminals, …) | `~/.cache/opencode/projects/<slug>/` | under `<host-cache>/projects/` |
|
|
264
264
|
| OpenCode **auth** (`auth.json`) | `~/.local/share/opencode/` | `$XDG_DATA_HOME/opencode/` when set |
|
|
265
265
|
| OpenCode **config** (AGENTS, skills, …) | `~/.config/opencode/` | still OpenCode-named for rule discovery |
|
|
@@ -397,6 +397,7 @@ OpenCode
|
|
|
397
397
|
| `src/debug.ts` | Opt-in wire-level debug logging (`CURSOR_PROVIDER_DEBUG`) |
|
|
398
398
|
| `src/auth.ts` | PKCE OAuth, API key exchange, JWT refresh |
|
|
399
399
|
| `src/models.ts` | `AvailableModels` fetch and `cursor-models.json` cache |
|
|
400
|
+
| `src/protocol/conversation-persistence.ts` | Atomic per-session restart snapshots under the 24-hour `cursor-conversations/` cache |
|
|
400
401
|
| `src/context/paths.ts` | Host cache root + Cursor project metadata under `<host-cache>/projects/<slug>/` |
|
|
401
402
|
| `src/agent-url.ts` | `GetServerConfig` fetch + in-process memo (region-specific Run host) |
|
|
402
403
|
| `src/transport/connect.ts` | HTTP/2 bidi stream and unary RPC calls |
|
|
@@ -410,9 +411,11 @@ If this guidance causes issues, update `buildOpenCodeInteractionGuidance` in [`s
|
|
|
410
411
|
|
|
411
412
|
### Cursor edit handshakes
|
|
412
413
|
|
|
413
|
-
Cursor's legacy `edit_tool_call` is implemented internally as a correlated `read_args` followed by a whole-file `write_args`; the write is transport mechanics, not a new decision to replace OpenCode's `edit` tool.
|
|
414
|
+
Cursor's legacy `edit_tool_call` is implemented internally as a correlated `read_args` followed by a whole-file `write_args`; the write is transport mechanics, not a new decision to replace OpenCode's `edit` tool. For regular files inside the workspace, the provider answers that private prerequisite read directly with the complete file (up to Cursor's own 50 MB edit limit), because routing it through OpenCode's ordinary 50 KB-capped `read` tool makes Cursor calculate a truncated replacement. This shortcut is restricted to the exact path named by the active edit call and rejects symlink escapes. External paths first take OpenCode's permission-aware read path; after that exact read succeeds, the provider upgrades its private result to complete content so an explicitly authorized external edit does not inherit OpenCode's preview cap.
|
|
414
415
|
|
|
415
|
-
The
|
|
416
|
+
The provider retains the correlation and exposes the resulting existing-file mutation as one unique, line-bounded OpenCode `edit`. It still returns Cursor's expected typed `write_result`. New-file creation remains a `write`, and an `apply_patch`-only catalog receives the same targeted change as an `*** Update File:` patch.
|
|
417
|
+
|
|
418
|
+
Ordinary complete native reads also preserve an existing final LF/CRLF. OpenCode's numbered read envelope cannot represent that terminator, and dropping it made exact edit matching fail. This restoration applies only to confirmed-complete, unbounded reads; paged and capped reads keep their existing truncation behavior.
|
|
416
419
|
|
|
417
420
|
### `apply_patch` models (GPT-5 and friends)
|
|
418
421
|
|
|
@@ -459,9 +462,10 @@ The package root intentionally stays plugin-safe for OpenCode's classic loader.
|
|
|
459
462
|
| 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. |
|
|
460
463
|
| 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. |
|
|
461
464
|
| `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` is a known Cursor-native capability without a safe OpenCode AI SDK bridge; `unknown request field` indicates new protocol drift; `handling=opencode-tool` or `provider-control` indicates a provider decoder/dispatch regression. Enable the debug log and report the full named error. |
|
|
462
|
-
| 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. |
|
|
463
|
-
| OpenCode
|
|
464
|
-
|
|
|
465
|
+
| 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. |
|
|
466
|
+
| OpenCode token totals grow once per tool call | Upgrade to a version containing the held-Run usage fix. OpenCode sums every AI SDK step, so the provider emits zero usage at intermediate tool boundaries and emits Cursor's final checkpoint total exactly once when the held Run closes. If the current Run has no token snapshot, the prior checkpoint total remains visible as stale; with no known snapshot, standard usage stays zero instead of mislabeling `TurnEnded` as context. Raw request counters remain in `providerMetadata.cursor`. |
|
|
467
|
+
| Context percentage jumps after a long tool turn or compaction | Cursor reports its authoritative context snapshot in checkpoint `token_details`. The provider makes the final AI SDK input + output equal `usedTokens`, proportionally partitions input into cached/uncached subsets, and exposes `maxTokens`, percentage, and category breakdown under `providerMetadata.cursor.context`. Intermediate held-Run tool boundaries remain zero, and compaction naturally replaces the prior snapshot with Cursor's smaller total. |
|
|
468
|
+
| 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`. |
|
|
465
469
|
|
|
466
470
|
## Security
|
|
467
471
|
|
|
@@ -470,8 +474,8 @@ Project `instructions` may reference absolute or `~/` paths (OpenCode parity). S
|
|
|
470
474
|
## Known limitations
|
|
471
475
|
|
|
472
476
|
- **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.
|
|
473
|
-
-
|
|
474
|
-
- **`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). 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.
|
|
477
|
+
- **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 can therefore display and compact against Cursor's total without double counting cache. 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. Intermediate tool-call boundaries report zero so a held Run is recorded only once. Cursor consistently sends `cache_write=0` in captured Runs, so no alternate write count is available.
|
|
478
|
+
- **`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.
|
|
475
479
|
- **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.
|
|
476
480
|
- **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 and future unknown exec variants fail explicitly rather than receiving a guessed response that could deadlock the Run.
|
|
477
481
|
- **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.
|
|
@@ -479,10 +483,11 @@ Project `instructions` may reference absolute or `~/` paths (OpenCode parity). S
|
|
|
479
483
|
- **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.
|
|
480
484
|
- **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`.
|
|
481
485
|
- **Cursor-native interaction queries remain headless** — Cursor UI/approval *queries* (as distinct from display tool calls) still cannot be surfaced through the AI SDK provider interface. The normal system prompt redirects questions, planning, plan-mode transitions, and available web capabilities to executable host tools (`question`, `todowrite`, `plan_enter` / `plan_exit`, `custom_websearch`, `custom_webfetch`); native web/PR/MCP/image/SCM requests are declined so they remain behind host tool permissions. Separately from display `create_plan_tool_call` → `todowrite` mirroring, interaction `create_plan_request_query` is auto-acked (CLI headless parity) with success and an empty `plan_uri`, so Cursor may treat the plan as accepted without an OpenCode UI confirm. Compaction prompts are unchanged. Unknown future interaction variants fail the turn explicitly instead of hanging the Run stream.
|
|
482
|
-
- **Compaction resets Cursor conversation state** — 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. Ordinary no-tool / `toolChoice:none` calls do not reset conversation state.
|
|
483
|
-
- **Conversation bindings
|
|
486
|
+
- **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.
|
|
487
|
+
- **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, 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 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 compaction catalogs 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.
|
|
488
|
+
- **Oversized checkpoint graphs rebase instead of flooding the Run stream** — before reusing a checkpoint, the provider measures its reachable KV graph. State above 100 MiB, or an incomplete graph with a missing/undecodable reference, rotates to a fresh Cursor conversation seeded from OpenCode's authoritative history. KV, request-context, MCP-state, interaction, and heartbeat writes are ordered per Run and await HTTP/2 drain, so a large blob reply cannot create an unbounded queue later misreported as heartbeat backpressure. Debug logs distinguish checkpoint-envelope bytes, reachable blob bytes, seed size, and encoded Run-request estimate; raw blob bytes are never presented as tokens.
|
|
484
489
|
- **Interrupted Runs resume from checkpoints** — a remote EOF, Connect end-stream, or trailer error is never emitted as a successful `stop`. When the failed Run produced an eligible checkpoint, the provider opens a new RPC for the same conversation and sends that state with `ResumeAction`, so completed text and tool work are not replayed. Before any stateful output, an interruption without a checkpoint can still rebase from OpenCode history. Stateful interruptions without a checkpoint are surfaced because replay would be ambiguous; retry exhaustion remains explicit. A transport closure after `turn_ended` is treated as successful completion.
|
|
485
|
-
- **Large reads are capped at 50 KB by OpenCode, and the cap is reported** — OpenCode's `read` tool stops at `MAX_BYTES = 50 * 1024`, cutting on a whole-line boundary, and appends `(Output capped at 50 KB. Showing lines X-Y. Use offset=N to continue.)`. The provider strips that envelope before returning content to Cursor so the model cannot echo wrappers into a later write, but it re-states the cap: a capped native read appends a `[Partial read: …]` marker after the content, a capped MCP read gets a separate notice content item, and a Pi read carries structured `PiReadExecSuccess.truncation`. 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.
|
|
490
|
+
- **Large reads are capped at 50 KB by OpenCode, and the cap is reported** — OpenCode's `read` tool stops at `MAX_BYTES = 50 * 1024`, cutting on a whole-line boundary, and appends `(Output capped at 50 KB. Showing lines X-Y. Use offset=N to continue.)`. The provider strips that envelope before returning content to Cursor so the model cannot echo wrappers into a later write, but it re-states the cap: a capped native read appends a `[Partial read: …]` marker after the content, a capped MCP read gets a separate notice content item, and a Pi read carries structured `PiReadExecSuccess.truncation`. 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. 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`.
|
|
486
491
|
- **No fallback models** — if Cursor’s `AvailableModels` API is unreachable and there is no local cache, the provider exposes no models.
|
|
487
492
|
|
|
488
493
|
## License
|
package/dist/context/build.d.ts
CHANGED
|
@@ -4,9 +4,17 @@ export type BuildRequestContextInput = {
|
|
|
4
4
|
tools?: OpencodeToolDef[];
|
|
5
5
|
providerIdentifier?: string;
|
|
6
6
|
};
|
|
7
|
+
export declare const DYNAMIC_REQUEST_CONTEXT_KEYS: readonly ["tools", "agent_skills", "custom_subagents", "mcp_file_system_options", "mcp_meta_tool_options", "web_search_enabled", "web_fetch_enabled", "agent_skills_info_complete", "custom_subagents_info_complete", "mcp_file_system_info_complete", "mcp_info_complete", "hooks_additional_context"];
|
|
8
|
+
export type DynamicRequestContextKey = typeof DYNAMIC_REQUEST_CONTEXT_KEYS[number];
|
|
7
9
|
/**
|
|
8
10
|
* Full RequestContext payload for live UMA + exec #10 reply.
|
|
9
11
|
* Sourced from OpenCode discovery (and .claude/.agents skill fallbacks).
|
|
10
12
|
* Honors `instructions` globs the same way OpenCode does (including `.cursor/` if listed).
|
|
11
13
|
*/
|
|
12
14
|
export declare function buildRequestContext(input: BuildRequestContextInput): Promise<Record<string, unknown>>;
|
|
15
|
+
/** Rediscover only capability/plugin sections that may change during a chat. */
|
|
16
|
+
export declare function buildDynamicRequestContext(input: BuildRequestContextInput): Promise<Record<string, unknown>>;
|
|
17
|
+
/** Keep expensive workspace state frozen while replacing every live capability field. */
|
|
18
|
+
export declare function materializeRequestContext(base: Record<string, unknown>, dynamic: Record<string, unknown>): Record<string, unknown>;
|
|
19
|
+
/** Strip live capability fields before retaining/persisting a conversation base. */
|
|
20
|
+
export declare function requestContextBase(context: Record<string, unknown>): Record<string, unknown>;
|
package/dist/context/build.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import path from "node:path";
|
|
2
2
|
import { extractHostSubagentCatalog, toolsToDescriptors, toolsToMcpDescriptors, } from "../protocol/tools.js";
|
|
3
|
-
import { collectRules } from "./rules.js";
|
|
3
|
+
import { collectRules, findGitWorktree, loadMergedConfig, } from "./rules.js";
|
|
4
4
|
import { collectSkills } from "./skills.js";
|
|
5
5
|
import { collectAgents } from "./agents.js";
|
|
6
6
|
import { collectPlugins } from "./plugins.js";
|
|
@@ -9,6 +9,20 @@ import { collectProjectLayout } from "./layout.js";
|
|
|
9
9
|
import { buildEnv } from "./env.js";
|
|
10
10
|
import { ensureOpencodeProjectDir } from "./paths.js";
|
|
11
11
|
import { traceRequestContextPaths } from "../debug.js";
|
|
12
|
+
export const DYNAMIC_REQUEST_CONTEXT_KEYS = [
|
|
13
|
+
"tools",
|
|
14
|
+
"agent_skills",
|
|
15
|
+
"custom_subagents",
|
|
16
|
+
"mcp_file_system_options",
|
|
17
|
+
"mcp_meta_tool_options",
|
|
18
|
+
"web_search_enabled",
|
|
19
|
+
"web_fetch_enabled",
|
|
20
|
+
"agent_skills_info_complete",
|
|
21
|
+
"custom_subagents_info_complete",
|
|
22
|
+
"mcp_file_system_info_complete",
|
|
23
|
+
"mcp_info_complete",
|
|
24
|
+
"hooks_additional_context",
|
|
25
|
+
];
|
|
12
26
|
const DEFAULT_HOST_SUBAGENTS = [
|
|
13
27
|
{
|
|
14
28
|
name: "general",
|
|
@@ -50,15 +64,38 @@ function buildAdvertisedSubagentCatalog(hostSubagents, discoveredAgents) {
|
|
|
50
64
|
*/
|
|
51
65
|
export async function buildRequestContext(input) {
|
|
52
66
|
const workspaceRoot = path.resolve(input.workspaceRoot || process.cwd());
|
|
67
|
+
const { rules, config, worktree } = await collectRules(workspaceRoot);
|
|
68
|
+
const [dynamic, git, layout] = await Promise.all([
|
|
69
|
+
buildDynamicRequestContextFromDiscovery(input, workspaceRoot, worktree, config),
|
|
70
|
+
collectGit(workspaceRoot),
|
|
71
|
+
collectProjectLayout(workspaceRoot),
|
|
72
|
+
]);
|
|
73
|
+
const base = {
|
|
74
|
+
env: buildEnv(workspaceRoot),
|
|
75
|
+
rules: rules.map((r) => ({
|
|
76
|
+
full_path: r.fullPath,
|
|
77
|
+
content: r.content,
|
|
78
|
+
})),
|
|
79
|
+
repository_info: git.repositoryInfo,
|
|
80
|
+
git_repos: git.gitRepos,
|
|
81
|
+
project_layouts: [layout],
|
|
82
|
+
rules_info_complete: true,
|
|
83
|
+
env_info_complete: true,
|
|
84
|
+
repository_info_complete: true,
|
|
85
|
+
git_repo_info_complete: true,
|
|
86
|
+
git_status_info_complete: true,
|
|
87
|
+
};
|
|
88
|
+
const ctx = materializeRequestContext(base, dynamic);
|
|
89
|
+
traceRequestContextPaths("buildRequestContext", ctx);
|
|
90
|
+
return ctx;
|
|
91
|
+
}
|
|
92
|
+
async function buildDynamicRequestContextFromDiscovery(input, workspaceRoot, worktree, config) {
|
|
53
93
|
const providerIdentifier = input.providerIdentifier ?? "opencode";
|
|
54
94
|
const tools = input.tools ?? [];
|
|
55
|
-
const
|
|
56
|
-
const [skills, agents, plugins, git, layout] = await Promise.all([
|
|
95
|
+
const [skills, agents, plugins] = await Promise.all([
|
|
57
96
|
collectSkills(workspaceRoot, worktree),
|
|
58
97
|
collectAgents(workspaceRoot),
|
|
59
98
|
collectPlugins(workspaceRoot, config),
|
|
60
|
-
collectGit(workspaceRoot),
|
|
61
|
-
collectProjectLayout(workspaceRoot),
|
|
62
99
|
]);
|
|
63
100
|
const mcpServerNames = Object.keys(config.mcp ?? {});
|
|
64
101
|
const flat = toolsToDescriptors(tools, providerIdentifier, mcpServerNames);
|
|
@@ -84,16 +121,8 @@ export async function buildRequestContext(input) {
|
|
|
84
121
|
`Delegate to the host-configured ${agent.name} subagent; its host instructions and tools apply.`,
|
|
85
122
|
};
|
|
86
123
|
});
|
|
87
|
-
const
|
|
88
|
-
env: buildEnv(workspaceRoot),
|
|
124
|
+
const dynamic = {
|
|
89
125
|
tools: flat,
|
|
90
|
-
rules: rules.map((r) => ({
|
|
91
|
-
full_path: r.fullPath,
|
|
92
|
-
content: r.content,
|
|
93
|
-
})),
|
|
94
|
-
repository_info: git.repositoryInfo,
|
|
95
|
-
git_repos: git.gitRepos,
|
|
96
|
-
project_layouts: [layout],
|
|
97
126
|
agent_skills: skills.map((s) => ({
|
|
98
127
|
full_path: s.fullPath,
|
|
99
128
|
content: s.content,
|
|
@@ -116,22 +145,42 @@ export async function buildRequestContext(input) {
|
|
|
116
145
|
// custom_web* aliases instead of routing through a query doomed to fail.
|
|
117
146
|
web_search_enabled: false,
|
|
118
147
|
web_fetch_enabled: false,
|
|
119
|
-
// Completeness: true only for sections we actually gathered.
|
|
120
|
-
rules_info_complete: true,
|
|
121
|
-
env_info_complete: true,
|
|
122
|
-
repository_info_complete: true,
|
|
123
|
-
git_repo_info_complete: true,
|
|
124
|
-
git_status_info_complete: true,
|
|
125
148
|
agent_skills_info_complete: true,
|
|
126
149
|
custom_subagents_info_complete: advertisedSubagents.complete,
|
|
127
150
|
mcp_file_system_info_complete: true,
|
|
128
151
|
mcp_info_complete: true,
|
|
129
152
|
};
|
|
130
153
|
if (plugins.length > 0) {
|
|
131
|
-
|
|
154
|
+
dynamic.hooks_additional_context = plugins
|
|
132
155
|
.map((p) => `opencode-plugin:${p.source}:${p.id}`)
|
|
133
156
|
.join("\n");
|
|
134
157
|
}
|
|
135
|
-
|
|
136
|
-
|
|
158
|
+
return dynamic;
|
|
159
|
+
}
|
|
160
|
+
/** Rediscover only capability/plugin sections that may change during a chat. */
|
|
161
|
+
export async function buildDynamicRequestContext(input) {
|
|
162
|
+
const workspaceRoot = path.resolve(input.workspaceRoot || process.cwd());
|
|
163
|
+
const [worktree, config] = await Promise.all([
|
|
164
|
+
findGitWorktree(workspaceRoot),
|
|
165
|
+
loadMergedConfig(workspaceRoot),
|
|
166
|
+
]);
|
|
167
|
+
return buildDynamicRequestContextFromDiscovery(input, workspaceRoot, worktree, config);
|
|
168
|
+
}
|
|
169
|
+
/** Keep expensive workspace state frozen while replacing every live capability field. */
|
|
170
|
+
export function materializeRequestContext(base, dynamic) {
|
|
171
|
+
const context = structuredClone(base);
|
|
172
|
+
for (const key of DYNAMIC_REQUEST_CONTEXT_KEYS)
|
|
173
|
+
delete context[key];
|
|
174
|
+
for (const key of DYNAMIC_REQUEST_CONTEXT_KEYS) {
|
|
175
|
+
if (Object.hasOwn(dynamic, key))
|
|
176
|
+
context[key] = structuredClone(dynamic[key]);
|
|
177
|
+
}
|
|
178
|
+
return context;
|
|
179
|
+
}
|
|
180
|
+
/** Strip live capability fields before retaining/persisting a conversation base. */
|
|
181
|
+
export function requestContextBase(context) {
|
|
182
|
+
const base = structuredClone(context);
|
|
183
|
+
for (const key of DYNAMIC_REQUEST_CONTEXT_KEYS)
|
|
184
|
+
delete base[key];
|
|
185
|
+
return base;
|
|
137
186
|
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { type BuildRequestContextInput } from "./build.js";
|
|
2
|
+
export declare const MAX_FROZEN_REQUEST_CONTEXTS = 256;
|
|
3
|
+
/** Frozen stable RequestContext base for this conversation, if any. */
|
|
4
|
+
export declare function getFrozenRequestContext(conversationId: string): Record<string, unknown> | undefined;
|
|
5
|
+
/** Replace the frozen stable base, stripping any live capability fields. */
|
|
6
|
+
export declare function setFrozenRequestContext(conversationId: string, context: Record<string, unknown>): void;
|
|
7
|
+
/** Drop a conversation's frozen RequestContext (compaction / binding reset). */
|
|
8
|
+
export declare function clearFrozenRequestContext(conversationId: string): void;
|
|
9
|
+
/**
|
|
10
|
+
* Move a stable workspace base across a conversation-id reset.
|
|
11
|
+
*
|
|
12
|
+
* Compaction changes Cursor's state/checkpoint identity, not the OpenCode
|
|
13
|
+
* workspace. Preserve the expensive base and the prior materialized bytes as a
|
|
14
|
+
* comparison seed; getOrBuildRequestContext still rediscovers live capability
|
|
15
|
+
* overlays and only reuses the complete context when those bytes also match.
|
|
16
|
+
*/
|
|
17
|
+
export declare function transferFrozenRequestContext(previousConversationId: string, nextConversationId: string): boolean;
|
|
18
|
+
/** Test helper — wipe all frozen contexts. */
|
|
19
|
+
export declare function resetFrozenRequestContextsForTests(): void;
|
|
20
|
+
/**
|
|
21
|
+
* Return a stable-base + live-overlay RequestContext for `conversationId`.
|
|
22
|
+
* The base is built once; capability sections are rediscovered every Run.
|
|
23
|
+
*/
|
|
24
|
+
export declare function getOrBuildRequestContext(conversationId: string, input: BuildRequestContextInput, opts?: {
|
|
25
|
+
refresh?: boolean;
|
|
26
|
+
}): Promise<{
|
|
27
|
+
context: Record<string, unknown>;
|
|
28
|
+
reused: boolean;
|
|
29
|
+
}>;
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
import { buildDynamicRequestContext, buildRequestContext, materializeRequestContext, requestContextBase, } from "./build.js";
|
|
2
|
+
import { trace } from "../debug.js";
|
|
3
|
+
import { encodeMessage } from "../protocol/messages.js";
|
|
4
|
+
/**
|
|
5
|
+
* Freeze the expensive RequestContext base for the life of a conversation_id.
|
|
6
|
+
*
|
|
7
|
+
* Cursor's current exec-daemon cache treats RequestContext as a baked stable
|
|
8
|
+
* base plus live plugin/tool overlays. Rebuilding volatile git/layout data on
|
|
9
|
+
* every Run shifts the prompt prefix and tanks prompt-cache hits.
|
|
10
|
+
*
|
|
11
|
+
* Skills, subagents, plugin metadata, and tool/MCP capabilities are rebuilt on
|
|
12
|
+
* each Run and overlaid on that base. If their encoded bytes did not change,
|
|
13
|
+
* the exact prior materialized object is reused. This matches Cursor's newer
|
|
14
|
+
* baked-context + live-overlay design without retaining stale capabilities.
|
|
15
|
+
*/
|
|
16
|
+
const byConversationId = new Map();
|
|
17
|
+
const materializedByConversationId = new Map();
|
|
18
|
+
const buildsByConversationId = new Map();
|
|
19
|
+
export const MAX_FROZEN_REQUEST_CONTEXTS = 256;
|
|
20
|
+
function freezeSnapshot(value, seen = new Set()) {
|
|
21
|
+
if (!value || typeof value !== "object")
|
|
22
|
+
return value;
|
|
23
|
+
const object = value;
|
|
24
|
+
if (seen.has(object) || ArrayBuffer.isView(object))
|
|
25
|
+
return value;
|
|
26
|
+
seen.add(object);
|
|
27
|
+
for (const child of Object.values(object))
|
|
28
|
+
freezeSnapshot(child, seen);
|
|
29
|
+
return Object.freeze(value);
|
|
30
|
+
}
|
|
31
|
+
function remember(conversationId, context) {
|
|
32
|
+
// Map insertion order is the LRU order. Refresh existing entries on use.
|
|
33
|
+
byConversationId.delete(conversationId);
|
|
34
|
+
// Clone + recursively freeze so session.requestContext cannot mutate the
|
|
35
|
+
// byte-stable snapshot retained for later Runs in the same conversation.
|
|
36
|
+
byConversationId.set(conversationId, freezeSnapshot(structuredClone(context)));
|
|
37
|
+
materializedByConversationId.delete(conversationId);
|
|
38
|
+
while (byConversationId.size > MAX_FROZEN_REQUEST_CONTEXTS) {
|
|
39
|
+
const oldest = byConversationId.keys().next().value;
|
|
40
|
+
if (!oldest)
|
|
41
|
+
break;
|
|
42
|
+
byConversationId.delete(oldest);
|
|
43
|
+
materializedByConversationId.delete(oldest);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
/** Frozen stable RequestContext base for this conversation, if any. */
|
|
47
|
+
export function getFrozenRequestContext(conversationId) {
|
|
48
|
+
const frozen = byConversationId.get(conversationId);
|
|
49
|
+
if (!frozen)
|
|
50
|
+
return undefined;
|
|
51
|
+
// Touch for LRU.
|
|
52
|
+
byConversationId.delete(conversationId);
|
|
53
|
+
byConversationId.set(conversationId, frozen);
|
|
54
|
+
return frozen;
|
|
55
|
+
}
|
|
56
|
+
/** Replace the frozen stable base, stripping any live capability fields. */
|
|
57
|
+
export function setFrozenRequestContext(conversationId, context) {
|
|
58
|
+
if (!conversationId)
|
|
59
|
+
return;
|
|
60
|
+
remember(conversationId, requestContextBase(context));
|
|
61
|
+
}
|
|
62
|
+
/** Drop a conversation's frozen RequestContext (compaction / binding reset). */
|
|
63
|
+
export function clearFrozenRequestContext(conversationId) {
|
|
64
|
+
byConversationId.delete(conversationId);
|
|
65
|
+
materializedByConversationId.delete(conversationId);
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Move a stable workspace base across a conversation-id reset.
|
|
69
|
+
*
|
|
70
|
+
* Compaction changes Cursor's state/checkpoint identity, not the OpenCode
|
|
71
|
+
* workspace. Preserve the expensive base and the prior materialized bytes as a
|
|
72
|
+
* comparison seed; getOrBuildRequestContext still rediscovers live capability
|
|
73
|
+
* overlays and only reuses the complete context when those bytes also match.
|
|
74
|
+
*/
|
|
75
|
+
export function transferFrozenRequestContext(previousConversationId, nextConversationId) {
|
|
76
|
+
if (!previousConversationId || !nextConversationId)
|
|
77
|
+
return false;
|
|
78
|
+
const base = byConversationId.get(previousConversationId);
|
|
79
|
+
const materialized = materializedByConversationId.get(previousConversationId);
|
|
80
|
+
clearFrozenRequestContext(previousConversationId);
|
|
81
|
+
clearFrozenRequestContext(nextConversationId);
|
|
82
|
+
if (!base)
|
|
83
|
+
return false;
|
|
84
|
+
remember(nextConversationId, base);
|
|
85
|
+
if (materialized) {
|
|
86
|
+
materializedByConversationId.set(nextConversationId, {
|
|
87
|
+
context: materialized.context,
|
|
88
|
+
bytes: Uint8Array.from(materialized.bytes),
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
return true;
|
|
92
|
+
}
|
|
93
|
+
/** Test helper — wipe all frozen contexts. */
|
|
94
|
+
export function resetFrozenRequestContextsForTests() {
|
|
95
|
+
byConversationId.clear();
|
|
96
|
+
materializedByConversationId.clear();
|
|
97
|
+
buildsByConversationId.clear();
|
|
98
|
+
}
|
|
99
|
+
function sameBytes(a, b) {
|
|
100
|
+
if (a.length !== b.length)
|
|
101
|
+
return false;
|
|
102
|
+
for (let i = 0; i < a.length; i++)
|
|
103
|
+
if (a[i] !== b[i])
|
|
104
|
+
return false;
|
|
105
|
+
return true;
|
|
106
|
+
}
|
|
107
|
+
function rememberMaterialized(conversationId, context) {
|
|
108
|
+
const bytes = encodeMessage("RequestContext", context);
|
|
109
|
+
const previous = materializedByConversationId.get(conversationId);
|
|
110
|
+
if (previous && sameBytes(previous.bytes, bytes)) {
|
|
111
|
+
return { context: previous.context, reused: true };
|
|
112
|
+
}
|
|
113
|
+
const frozen = freezeSnapshot(structuredClone(context));
|
|
114
|
+
materializedByConversationId.set(conversationId, {
|
|
115
|
+
context: frozen,
|
|
116
|
+
bytes: Uint8Array.from(bytes),
|
|
117
|
+
});
|
|
118
|
+
return { context: frozen, reused: false };
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* Return a stable-base + live-overlay RequestContext for `conversationId`.
|
|
122
|
+
* The base is built once; capability sections are rediscovered every Run.
|
|
123
|
+
*/
|
|
124
|
+
export async function getOrBuildRequestContext(conversationId, input, opts) {
|
|
125
|
+
if (!opts?.refresh && conversationId) {
|
|
126
|
+
const base = getFrozenRequestContext(conversationId);
|
|
127
|
+
if (base) {
|
|
128
|
+
const dynamic = await buildDynamicRequestContext(input);
|
|
129
|
+
const materialized = rememberMaterialized(conversationId, materializeRequestContext(base, dynamic));
|
|
130
|
+
trace(`request_context: materialized conversationId=${conversationId} ` +
|
|
131
|
+
`tools=${Array.isArray(materialized.context.tools) ? materialized.context.tools.length : 0} ` +
|
|
132
|
+
`reused=${materialized.reused}`);
|
|
133
|
+
return materialized;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
// Two overlapping model calls can open Runs for the same conversation. Share
|
|
137
|
+
// the first build so both Runs receive the same snapshot instead of racing
|
|
138
|
+
// two independently collected git/layout views into the cache.
|
|
139
|
+
if (!opts?.refresh && conversationId) {
|
|
140
|
+
const inFlight = buildsByConversationId.get(conversationId);
|
|
141
|
+
if (inFlight) {
|
|
142
|
+
await inFlight;
|
|
143
|
+
return getOrBuildRequestContext(conversationId, input);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
const build = buildRequestContext(input);
|
|
147
|
+
if (conversationId)
|
|
148
|
+
buildsByConversationId.set(conversationId, build);
|
|
149
|
+
let context;
|
|
150
|
+
try {
|
|
151
|
+
context = await build;
|
|
152
|
+
}
|
|
153
|
+
finally {
|
|
154
|
+
if (conversationId && buildsByConversationId.get(conversationId) === build) {
|
|
155
|
+
buildsByConversationId.delete(conversationId);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
if (conversationId)
|
|
159
|
+
setFrozenRequestContext(conversationId, context);
|
|
160
|
+
// The first full build already contains both base and live overlay.
|
|
161
|
+
const materialized = conversationId
|
|
162
|
+
? rememberMaterialized(conversationId, context)
|
|
163
|
+
: { context: freezeSnapshot(structuredClone(context)), reused: false };
|
|
164
|
+
trace(`request_context: built+frozen conversationId=${conversationId || "(none)"} ` +
|
|
165
|
+
`tools=${Array.isArray(materialized.context.tools) ? materialized.context.tools.length : 0} ` +
|
|
166
|
+
`refresh=${!!opts?.refresh}`);
|
|
167
|
+
return { context: materialized.context, reused: false };
|
|
168
|
+
}
|
package/dist/context/index.d.ts
CHANGED
|
@@ -1,2 +1,3 @@
|
|
|
1
|
-
export { buildRequestContext, type BuildRequestContextInput } from "./build.js";
|
|
1
|
+
export { buildDynamicRequestContext, buildRequestContext, DYNAMIC_REQUEST_CONTEXT_KEYS, materializeRequestContext, requestContextBase, type BuildRequestContextInput, } from "./build.js";
|
|
2
|
+
export { clearFrozenRequestContext, getFrozenRequestContext, getOrBuildRequestContext, MAX_FROZEN_REQUEST_CONTEXTS, resetFrozenRequestContextsForTests, setFrozenRequestContext, transferFrozenRequestContext, } from "./frozen.js";
|
|
2
3
|
export { adoptCompatHostCacheDir, getHostCacheDirOverride, opencodeGlobalCacheDir, opencodeGlobalConfigDir, opencodeGlobalDataDir, resolveHostCacheDir, setHostCacheDirOverride, } from "./paths.js";
|
package/dist/context/index.js
CHANGED
|
@@ -1,2 +1,3 @@
|
|
|
1
|
-
export { buildRequestContext } from "./build.js";
|
|
1
|
+
export { buildDynamicRequestContext, buildRequestContext, DYNAMIC_REQUEST_CONTEXT_KEYS, materializeRequestContext, requestContextBase, } from "./build.js";
|
|
2
|
+
export { clearFrozenRequestContext, getFrozenRequestContext, getOrBuildRequestContext, MAX_FROZEN_REQUEST_CONTEXTS, resetFrozenRequestContextsForTests, setFrozenRequestContext, transferFrozenRequestContext, } from "./frozen.js";
|
|
2
3
|
export { adoptCompatHostCacheDir, getHostCacheDirOverride, opencodeGlobalCacheDir, opencodeGlobalConfigDir, opencodeGlobalDataDir, resolveHostCacheDir, setHostCacheDirOverride, } from "./paths.js";
|
package/dist/context/rules.d.ts
CHANGED
|
@@ -9,6 +9,7 @@ export type OpencodeJson = {
|
|
|
9
9
|
plugins?: string[];
|
|
10
10
|
mcp?: Record<string, unknown>;
|
|
11
11
|
};
|
|
12
|
+
export declare function findGitWorktree(start: string): Promise<string>;
|
|
12
13
|
/** Same truthy rule as OpenCode's Flag.OPENCODE_DISABLE_PROJECT_CONFIG. */
|
|
13
14
|
export declare function isProjectConfigDisabled(): boolean;
|
|
14
15
|
/** Fetch a remote instruction with one deadline covering headers and body. */
|
package/dist/context/rules.js
CHANGED
package/dist/language-model.d.ts
CHANGED
|
@@ -1,10 +1,15 @@
|
|
|
1
1
|
import type { LanguageModelV3, LanguageModelV3CallOptions, LanguageModelV3StreamPart } from "@ai-sdk/provider";
|
|
2
2
|
import type { CreateCursorOptions, CursorRetryOptions } from "./index.js";
|
|
3
3
|
import { type OpencodeToolDef } from "./protocol/tools.js";
|
|
4
|
+
import { cursorContextUsageMetadata, type CursorContextUsageSource, type CursorConversationTokenDetails } from "./protocol/token-details.js";
|
|
5
|
+
import { type ConversationBlobGraphStats } from "./protocol/blob-store.js";
|
|
4
6
|
import { type CursorSession } from "./session.js";
|
|
5
7
|
import { CursorProviderError } from "./errors.js";
|
|
6
8
|
import type { SeedHistoryMessage } from "./protocol/request.js";
|
|
7
9
|
export declare const MAX_TURN_STATE_SESSIONS = 256;
|
|
10
|
+
/** Cursor's own conversation export path treats state above 100 MiB as large. */
|
|
11
|
+
export declare const MAX_CHECKPOINT_BLOB_GRAPH_BYTES: number;
|
|
12
|
+
export declare function checkpointBlobGraphRequiresRebase(stats: ConversationBlobGraphStats, maxBytes?: number): boolean;
|
|
8
13
|
export type CursorRetryPolicy = {
|
|
9
14
|
maxAttempts: number;
|
|
10
15
|
baseDelayMs: number;
|
|
@@ -12,13 +17,14 @@ export type CursorRetryPolicy = {
|
|
|
12
17
|
};
|
|
13
18
|
export declare function resolveRetryPolicy(options: CursorRetryOptions | undefined): CursorRetryPolicy;
|
|
14
19
|
export declare function connectFrameError(payload: string): CursorProviderError;
|
|
20
|
+
/** Restore the last real catalog solely for lifecycle turns such as compaction. */
|
|
21
|
+
export declare function restoreTurnToolCatalog(sessionKey: string, tools: OpencodeToolDef[]): void;
|
|
15
22
|
type V3Part = LanguageModelV3StreamPart;
|
|
16
23
|
export declare function createCursorLanguageModel(modelId: string, providerId: string, options: CreateCursorOptions): LanguageModelV3;
|
|
17
24
|
export declare function pumpWithRecovery(input: {
|
|
18
25
|
initialSession: CursorSession;
|
|
19
26
|
controller: ReadableStreamDefaultController<V3Part>;
|
|
20
27
|
abortSignal?: AbortSignal;
|
|
21
|
-
promptTokens?: number;
|
|
22
28
|
retryPolicy?: CursorRetryPolicy;
|
|
23
29
|
recover: (recovery: CursorRunRecovery) => Promise<CursorSession>;
|
|
24
30
|
onSession?: (session: CursorSession) => void;
|
|
@@ -56,10 +62,6 @@ export declare function deliverContinuationResults(session: CursorSession, trail
|
|
|
56
62
|
export declare function pump(session: CursorSession, controller: ReadableStreamDefaultController<V3Part>, ids: {
|
|
57
63
|
textId: string;
|
|
58
64
|
reasoningId: string;
|
|
59
|
-
promptTokens?: number;
|
|
60
|
-
requestUsage?: {
|
|
61
|
-
outputChars: number;
|
|
62
|
-
};
|
|
63
65
|
}, abortSignal?: AbortSignal): Promise<void>;
|
|
64
66
|
type ExtractedToolResult = {
|
|
65
67
|
toolCallId: string;
|
|
@@ -83,10 +85,8 @@ export declare function extractTrailingToolResults(prompt: LanguageModelV3CallOp
|
|
|
83
85
|
export declare function buildOpenCodeInteractionGuidance(tools: OpencodeToolDef[], isCompaction: boolean, workspaceRoot: string): string | undefined;
|
|
84
86
|
/** Rough char→token estimate for mid-turn usage before TurnEnded arrives. */
|
|
85
87
|
export declare function estimateTokens(chars: number): number;
|
|
86
|
-
/**
|
|
87
|
-
export declare function
|
|
88
|
-
/** Preserve exact Cursor counters as diagnostics when they are cumulative across a multi-step Run. */
|
|
89
|
-
export declare function cursorTurnEndedProviderMetadata(te: Record<string, unknown>): {
|
|
88
|
+
/** Preserve exact request-local Cursor counters as diagnostics. */
|
|
89
|
+
export declare function cursorTurnEndedProviderMetadata(te: Record<string, unknown>, tokenDetails?: CursorConversationTokenDetails, contextSource?: CursorContextUsageSource): {
|
|
90
90
|
cursor: {
|
|
91
91
|
usageVersion: number;
|
|
92
92
|
inputTokensRaw: number;
|
|
@@ -94,6 +94,7 @@ export declare function cursorTurnEndedProviderMetadata(te: Record<string, unkno
|
|
|
94
94
|
cacheReadRaw: number;
|
|
95
95
|
cacheWriteRaw: number;
|
|
96
96
|
reasoningTokensRaw: number;
|
|
97
|
+
context?: ReturnType<typeof cursorContextUsageMetadata>;
|
|
97
98
|
};
|
|
98
99
|
};
|
|
99
100
|
/**
|