cursor-opencode-provider 0.2.3 → 0.2.5
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 +36 -17
- package/SECURITY.md +42 -0
- package/dist/activity.d.ts +15 -0
- package/dist/activity.js +76 -0
- package/dist/auth.d.ts +2 -1
- package/dist/auth.js +19 -13
- package/dist/context/build.js +0 -7
- package/dist/context/rules.d.ts +4 -0
- package/dist/context/rules.js +53 -17
- package/dist/debug.d.ts +9 -0
- package/dist/debug.js +44 -4
- package/dist/errors.d.ts +59 -0
- package/dist/errors.js +199 -0
- package/dist/index.d.ts +14 -0
- package/dist/index.js +4 -0
- package/dist/language-model.d.ts +54 -7
- package/dist/language-model.js +730 -197
- package/dist/models.d.ts +7 -0
- package/dist/models.js +259 -79
- package/dist/plugin.js +38 -11
- package/dist/protocol/exec-variants.d.ts +24 -0
- package/dist/protocol/exec-variants.js +55 -0
- package/dist/protocol/interactions.d.ts +8 -1
- package/dist/protocol/interactions.js +15 -2
- package/dist/protocol/messages.js +109 -26
- package/dist/protocol/request.d.ts +2 -4
- package/dist/protocol/request.js +0 -8
- package/dist/protocol/struct.js +1 -1
- package/dist/protocol/tool-call-bridge.js +13 -1
- package/dist/protocol/tools.d.ts +8 -1
- package/dist/protocol/tools.js +384 -39
- package/dist/session.d.ts +115 -8
- package/dist/session.js +362 -31
- package/dist/shell-timeout.d.ts +57 -0
- package/dist/shell-timeout.js +186 -0
- package/dist/transport/connect.d.ts +37 -1
- package/dist/transport/connect.js +679 -115
- package/package.json +8 -2
package/README.md
CHANGED
|
@@ -17,8 +17,8 @@ OpenCode driving a Cursor-routed Grok model through this provider:
|
|
|
17
17
|
- **OpenCode integration** — registers a `cursor` provider with auth hooks and cached model list
|
|
18
18
|
- **Authentication** — browser OAuth (PKCE), or API key from [cursor.com/settings](https://cursor.com/settings)
|
|
19
19
|
- **Model discovery** — fetches available models from Cursor's API and caches them locally
|
|
20
|
-
- **Streaming** — bidirectional Connect-RPC
|
|
21
|
-
- **Tool calls** — maps Cursor exec-server messages to AI SDK / OpenCode tool-call parts, including the Pi read/bash/edit/write/grep/find/ls request/result field range; mirrors finalized display-only todo/plan state into OpenCode; strips OpenCode's `read` XML envelope (`<path>`/`<content>` + `N:` prefixes) before returning content to Cursor so the model cannot echo the wrapper into writes
|
|
20
|
+
- **Streaming** — bidirectional Connect-RPC Runs with stale-session rotation, health checks, semantic/read-idle deadlines, bounded replay-safe recovery, and activity-aware held tool continuations
|
|
21
|
+
- **Tool calls** — maps Cursor exec-server messages to AI SDK / OpenCode tool-call parts, including native subagent/Task execution (Cursor's `generalPurpose` maps to OpenCode `general`; read-oriented `bugbot` reviews map to `explore`) and the Pi read/bash/edit/write/grep/find/ls request/result field range; enforces the exact current OpenCode agent catalog before emitting any tool call; mirrors finalized display-only todo/plan state into OpenCode; strips OpenCode's `read` XML envelope (`<path>`/`<content>` + `N:` prefixes) before returning content to Cursor so the model cannot echo the wrapper into writes
|
|
22
22
|
- **Thinking / reasoning** — surfaces extended-thinking deltas where the model supports it
|
|
23
23
|
|
|
24
24
|
## Requirements
|
|
@@ -47,7 +47,7 @@ Add the package name to OpenCode config. OpenCode installs npm plugins with Bun
|
|
|
47
47
|
}
|
|
48
48
|
```
|
|
49
49
|
|
|
50
|
-
Pin a version if you want: `"cursor-opencode-provider@0.2.
|
|
50
|
+
Pin a version if you want: `"cursor-opencode-provider@0.2.5"`.
|
|
51
51
|
|
|
52
52
|
### From a local clone
|
|
53
53
|
|
|
@@ -124,7 +124,7 @@ opencode run --model cursor/composer-2.5 "Hello from Cursor via OpenCode"
|
|
|
124
124
|
|
|
125
125
|
Cursor models often expose parameterized variants (effort, thinking, fast, context tier, …). The plugin materializes those as OpenCode **model variants**. In the TUI, pick one from the variant dialog or cycle with OpenCode’s `variant_cycle` keybind (default `ctrl+t`).
|
|
126
126
|
|
|
127
|
-
The selected variant’s Cursor parameter map is forwarded on the Run as `requested_model.parameters` (isolated under `providerOptions.cursor.cursorVariantParameters` so unrelated OpenCode options are not leaked onto the wire).
|
|
127
|
+
The selected variant’s Cursor parameter map is forwarded on the Run as `requested_model.parameters` (isolated under `providerOptions.cursor.cursorVariantParameters` so unrelated OpenCode options are not leaked onto the wire). The provider validates that explicit selection against the current cached tuple; malformed, reordered, or stale selections fail clearly instead of silently falling back to another variant.
|
|
128
128
|
|
|
129
129
|
#### 1M / long context
|
|
130
130
|
|
|
@@ -157,13 +157,15 @@ const cursor = createCursor({
|
|
|
157
157
|
// apiBaseURL: "https://api2.cursor.sh",
|
|
158
158
|
// agentBaseURL: "https://agentn.us.api5.cursor.sh", // explicit Run host override
|
|
159
159
|
// telemetryEnabled: true, // opt in to GetServerConfig telemetry
|
|
160
|
+
// retry: { maxAttempts: 3, baseDelayMs: 500, maxDelayMs: 8_000 },
|
|
161
|
+
// continuation: { heartbeatMs: 5_000, semanticIdleMs: 120_000, hardCapMs: 600_000 },
|
|
160
162
|
})
|
|
161
163
|
|
|
162
164
|
const model = cursor.languageModel("composer-2.5")
|
|
163
165
|
// model implements AI SDK LanguageModelV3 (doStream / doGenerate)
|
|
164
166
|
```
|
|
165
167
|
|
|
166
|
-
Pass either `accessToken` (JWT from OAuth or key exchange) or `apiKey` (raw `sk-...` key). Optional: `apiBaseURL`, `agentBaseURL`, `headers`, `telemetryEnabled`. The older `baseURL` option is still accepted as a legacy alias for `agentBaseURL`.
|
|
168
|
+
Pass either `accessToken` (JWT from OAuth or key exchange) or `apiKey` (raw `sk-...` key). Optional: `apiBaseURL`, `agentBaseURL`, `headers`, `telemetryEnabled`, `retry`, and `continuation`. Retries occur only before visible output or stateful server activity; unsafe replay is surfaced instead of risking duplicate text or tool work. 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`.
|
|
167
169
|
|
|
168
170
|
## Environment variables
|
|
169
171
|
|
|
@@ -173,11 +175,11 @@ Pass either `accessToken` (JWT from OAuth or key exchange) or `apiKey` (raw `sk-
|
|
|
173
175
|
| `CURSOR_API_BASE_URL` | Override API base for auth, model discovery, and `GetServerConfig` agent URL resolution (default `https://api2.cursor.sh`) |
|
|
174
176
|
| `CURSOR_GET_SERVER_CONFIG_TELEMETRY` | Set to `1` or `true` to opt the `GetServerConfig` lookup into telemetry in OpenCode/plugin usage |
|
|
175
177
|
| `CURSOR_PROVIDER_DEBUG` | Set to `1` or `true` to enable wire-level debug logging |
|
|
176
|
-
| `CURSOR_PROVIDER_DEBUG_FILE` | Debug log path (default
|
|
178
|
+
| `CURSOR_PROVIDER_DEBUG_FILE` | Debug log path (default: `debug-<pid>.log` under `$TMPDIR/cursor-provider-logs-<uid>/`) |
|
|
177
179
|
| `XDG_CACHE_HOME` | When set, model/version caches go under `$XDG_CACHE_HOME/opencode/` instead of `~/.cache/opencode/` |
|
|
178
180
|
| `XDG_DATA_HOME` | When set, OpenCode `auth.json` is read from `$XDG_DATA_HOME/opencode/` instead of `~/.local/share/opencode/` |
|
|
179
181
|
|
|
180
|
-
`createCursor({ agentBaseURL })` overrides the agent Run host. When unset, the provider resolves the host from Cursor's `GetServerConfig` API (`agentUrlConfig.agentnUrl`, region-specific — e.g. `agentn.us.api5.cursor.sh`, `agent-gcpp-uswest.api5.cursor.sh`) once per process and holds it in memory (never written to disk), so a held-open Run stream is never repointed mid-session. Explicit agent overrides and GetServerConfig results are validated as HTTPS `*.cursor.sh` hosts (Cursor's agent hostnames vary and may change); non-`cursor.sh` hosts are rejected. The lookup sends `{ "telem_enabled": false }` by default; set `telemetryEnabled: true` in provider config, or `CURSOR_GET_SERVER_CONFIG_TELEMETRY=1` for OpenCode/plugin usage, to opt in. If the lookup fails or does not return a valid Cursor agent host, the model call fails clearly instead of falling back to `agentn.global.api5.cursor.sh`.
|
|
182
|
+
`createCursor({ agentBaseURL })` overrides the agent Run host. When unset, the provider resolves the host from Cursor's `GetServerConfig` API (`agentUrlConfig.agentnUrl`, region-specific — e.g. `agentn.us.api5.cursor.sh`, `agent-gcpp-uswest.api5.cursor.sh`) once per process and holds it in memory (never written to disk), so a held-open Run stream is never repointed mid-session. Explicit agent overrides and GetServerConfig results are validated as HTTPS `*.cursor.sh` hosts (Cursor's agent hostnames vary and may change); non-`cursor.sh` hosts are rejected. Shared HTTP/2 connections are rotated before they become server-aged, while existing Runs may finish on their original connection. The lookup sends `{ "telem_enabled": false }` by default; set `telemetryEnabled: true` in provider config, or `CURSOR_GET_SERVER_CONFIG_TELEMETRY=1` for OpenCode/plugin usage, to opt in. If the lookup fails or does not return a valid Cursor agent host, the model call fails clearly instead of falling back to `agentn.global.api5.cursor.sh`.
|
|
181
183
|
|
|
182
184
|
## Development
|
|
183
185
|
|
|
@@ -186,6 +188,7 @@ bun install # install dependencies
|
|
|
186
188
|
bun run build # compile TypeScript → dist/
|
|
187
189
|
bun run typecheck # type-check without emit
|
|
188
190
|
bun test # run unit tests
|
|
191
|
+
bun run test:node-http2 # Node-specific HTTP/2 detach regression
|
|
189
192
|
bun run test:watch # watch mode
|
|
190
193
|
```
|
|
191
194
|
|
|
@@ -214,6 +217,12 @@ OpenCode
|
|
|
214
217
|
| `src/transport/connect.ts` | HTTP/2 bidi stream and unary RPC calls |
|
|
215
218
|
| `src/protocol/` | Protobuf encode/decode, checksum/device ids, exec + display tool-call mapping (`tool-call-bridge.ts`) |
|
|
216
219
|
|
|
220
|
+
### Injected system guidance
|
|
221
|
+
|
|
222
|
+
The provider adds OpenCode-specific system guidance to normal tool-capable conversations, including tool availability, canonical workspace-path grounding, and preferring `edit` / `write` over shell-based file mutation when those tools are available. Compaction keeps its dedicated prompt unchanged.
|
|
223
|
+
|
|
224
|
+
If this guidance causes issues, update `buildOpenCodeInteractionGuidance` in [`src/language-model.ts`](src/language-model.ts) and its focused coverage in [`test/prompt-history.test.ts`](test/prompt-history.test.ts).
|
|
225
|
+
|
|
217
226
|
## Package exports
|
|
218
227
|
|
|
219
228
|
| Import path | Export |
|
|
@@ -221,8 +230,9 @@ OpenCode
|
|
|
221
230
|
| `cursor-opencode-provider` | `createCursor`, `CursorPlugin` (named + default) |
|
|
222
231
|
| `cursor-opencode-provider/plugin` | `CursorPlugin` (classic Hooks — auth) |
|
|
223
232
|
| `cursor-opencode-provider/plugin/v2` | OpenCode Effect/Promise v2 plugin (`ctx.aisdk.*`) |
|
|
233
|
+
| `cursor-opencode-provider/errors` | Structured provider error classes |
|
|
224
234
|
|
|
225
|
-
`CursorPluginV2`
|
|
235
|
+
The package root intentionally stays plugin-safe for OpenCode's classic loader. `CursorPluginV2` and non-plugin runtime APIs are **not** re-exported from the package root; load them through their dedicated subpaths.
|
|
226
236
|
|
|
227
237
|
## Troubleshooting
|
|
228
238
|
|
|
@@ -232,21 +242,30 @@ OpenCode
|
|
|
232
242
|
| Auth / 401 errors mid-session | Re-login. OAuth and exchanged API-key JWTs refresh automatically when near expiry; a revoked refresh token needs a fresh login. |
|
|
233
243
|
| “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. |
|
|
234
244
|
| Empty or stale model list | Delete `~/.cache/opencode/cursor-models.json` (or under `$XDG_CACHE_HOME/opencode/`) and restart OpenCode. 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. |
|
|
235
|
-
| Stream hangs or HTTP/2 errors |
|
|
236
|
-
| No response / silent 200 + close |
|
|
237
|
-
|
|
|
245
|
+
| Stream hangs or HTTP/2 errors | The provider keeps Cursor's Run open across OpenCode tool calls, rotates aged shared connections, and transparently rebases once from full OpenCode history if the Run ends before `turn_ended`. 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` and `rebasing fresh Run`. Restart OpenCode after rebuilding a local `file://` install. |
|
|
246
|
+
| 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. |
|
|
247
|
+
| 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. |
|
|
248
|
+
| `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. |
|
|
249
|
+
| 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. |
|
|
250
|
+
|
|
251
|
+
## Security
|
|
252
|
+
|
|
253
|
+
Project `instructions` may reference absolute or `~/` paths (OpenCode parity). See [SECURITY.md](./SECURITY.md) for the trust model and `OPENCODE_DISABLE_PROJECT_CONFIG`.
|
|
238
254
|
|
|
239
255
|
## Known limitations
|
|
240
256
|
|
|
241
257
|
- **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.
|
|
242
|
-
- **`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). Same discovery as OpenCode — including `.cursor/` paths only when listed in `instructions`. Cursor-only cloud/sandbox marketplace surfaces are omitted.
|
|
243
|
-
- **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,
|
|
244
|
-
- **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 Pi read/bash/edit/write/grep/find/ls calls use their typed request/result fields instead. Unknown display variants are logged; unknown exec variants fail
|
|
245
|
-
- **
|
|
246
|
-
- **
|
|
258
|
+
- **`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. OpenCode remains the permission authority: its coarse allow/ask/deny configuration is not fabricated into Cursor's unrelated allow/block instruction-list messages.
|
|
259
|
+
- **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.
|
|
260
|
+
- **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.
|
|
261
|
+
- **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.
|
|
262
|
+
- **Background shells are non-interactive** — Cursor's native background-shell spawn is bridged through OpenCode's foreground-only `bash` tool by detaching the requested command, redirecting its output to `${TMPDIR:-/tmp}/cursor-opencode-bg.*`, and returning the real PID in Cursor's typed field-16 result. Shell-stream requests also preserve Cursor's foreground timeout, `timeout_behavior`, and `hard_timeout`: cancellation becomes a typed aborted exit, while a soft timeout requested with background behavior becomes a typed background handoff. OpenCode's internal `<shell_metadata>` envelope and the bridge's private markers are stripped before the result reaches the UI. 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.
|
|
263
|
+
- **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 known-URL fetching to equivalent OpenCode tools only when they are advertised (`question`, `todowrite`, `plan_enter` / `plan_exit`, `webfetch`); native web/PR/MCP/image/SCM requests are declined so they remain behind OpenCode's tools and 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.
|
|
264
|
+
- **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.
|
|
247
265
|
- **Conversation bindings and compaction catalogs are bounded** — process-global per-session bindings, prior tool catalogs, and pending post-compaction rebases use a 256-session LRU bound. Evicting a conversation binding also drops its checkpoint and KV blobs.
|
|
266
|
+
- **Interrupted Runs rebase once** — a remote EOF, Connect end-stream, trailer error, failed continuation write, or closed pending-tool session is never emitted as a successful `stop`. The provider starts one fresh Cursor conversation seeded from the OpenCode prompt, retaining the live user request and trailing tool results as host observations without replaying stale historical tool output, re-advertises the same tools, and preserves compaction's no-execution behavior. A second interruption is returned as an explicit error. Recovery cannot see tokens already streamed to the UI on the interrupted attempt, so mid-generation recovery may briefly duplicate visible text/reasoning even though the rebase prompt asks the model not to repeat completed work.
|
|
248
267
|
- **No fallback models** — if Cursor’s `AvailableModels` API is unreachable and there is no local cache, the provider exposes no models.
|
|
249
268
|
|
|
250
269
|
## License
|
|
251
270
|
|
|
252
|
-
MIT
|
|
271
|
+
MIT
|
package/SECURITY.md
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# Security notes
|
|
2
|
+
|
|
3
|
+
This provider mirrors OpenCode’s trust model for project configuration. That has implications for what ends up in Cursor context.
|
|
4
|
+
|
|
5
|
+
## Project `instructions` can include arbitrary local paths
|
|
6
|
+
|
|
7
|
+
OpenCode’s `opencode.json` / `opencode.jsonc` `instructions` array may list:
|
|
8
|
+
|
|
9
|
+
- relative paths / globs (e.g. `.cursor/rules/*.md`)
|
|
10
|
+
- absolute paths
|
|
11
|
+
- home-relative paths (`~/…`)
|
|
12
|
+
- remote URLs (`https://…`; this provider fetches HTTPS only)
|
|
13
|
+
|
|
14
|
+
OpenCode itself expands `~/`, accepts absolute paths, and injects those file contents into the model prompt for **every** provider. This package does the same discovery when building Cursor `RequestContext.rules`.
|
|
15
|
+
|
|
16
|
+
So a project config like:
|
|
17
|
+
|
|
18
|
+
```json
|
|
19
|
+
{
|
|
20
|
+
"instructions": ["~/.ssh/id_rsa", "/etc/passwd"]
|
|
21
|
+
}
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
can cause those files to be read and sent to the model provider (OpenCode prompt path and/or this provider’s Cursor `RequestContext`). That is intentional OpenCode parity, not a Cursor-only hole. There is no path allowlist that rejects absolute/`~/` instruction paths (matching OpenCode; OpenCode has not treated this as a defect).
|
|
25
|
+
|
|
26
|
+
Treat project `opencode.json` as **trusted**. Do not open untrusted repositories with project config enabled if that is unacceptable.
|
|
27
|
+
|
|
28
|
+
## Mitigation: disable project config
|
|
29
|
+
|
|
30
|
+
To ignore project-level OpenCode config (including project `instructions` and project `AGENTS.md` / `CLAUDE.md` / `CONTEXT.md` discovery), set:
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
export OPENCODE_DISABLE_PROJECT_CONFIG=1
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
OpenCode honors this flag when loading config and assembling prompts. This provider honors the same flag when collecting rules for Cursor `RequestContext`, so project `instructions` are not merged and project instruction files are not auto-discovered. Global config under `~/.config/opencode` (and `~/.claude/CLAUDE.md` when applicable) still applies.
|
|
37
|
+
|
|
38
|
+
Truthy values: `1` or `true` (case-insensitive), same as OpenCode.
|
|
39
|
+
|
|
40
|
+
## Related hardening in this provider
|
|
41
|
+
|
|
42
|
+
- Remote `instructions` URLs are **HTTPS-only** (`http://` is skipped). Redirects (including to a local proxy) are intentional and not blocked.
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export type SessionActivitySource = {
|
|
2
|
+
lastActivityAt(sessionId: string): number | undefined;
|
|
3
|
+
};
|
|
4
|
+
/** Tracks OpenCode message progress and propagates it through subagent ancestry. */
|
|
5
|
+
export declare class SessionActivityTracker implements SessionActivitySource {
|
|
6
|
+
private readonly parentBySession;
|
|
7
|
+
private readonly lastActivityBySession;
|
|
8
|
+
linkSession(sessionId: string, parentId?: string): void;
|
|
9
|
+
recordActivity(sessionId: string, at?: number): void;
|
|
10
|
+
lastActivityAt(sessionId: string): number | undefined;
|
|
11
|
+
removeSession(sessionId: string): void;
|
|
12
|
+
clear(): void;
|
|
13
|
+
private prune;
|
|
14
|
+
}
|
|
15
|
+
export declare const sessionActivity: SessionActivityTracker;
|
package/dist/activity.js
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
const MAX_ANCESTRY_DEPTH = 64;
|
|
2
|
+
const MAX_TRACKED_SESSIONS = 1_024;
|
|
3
|
+
const ACTIVITY_RETENTION_MS = 24 * 60 * 60 * 1_000;
|
|
4
|
+
/** Tracks OpenCode message progress and propagates it through subagent ancestry. */
|
|
5
|
+
export class SessionActivityTracker {
|
|
6
|
+
parentBySession = new Map();
|
|
7
|
+
lastActivityBySession = new Map();
|
|
8
|
+
linkSession(sessionId, parentId) {
|
|
9
|
+
if (!sessionId)
|
|
10
|
+
return;
|
|
11
|
+
this.prune(Date.now());
|
|
12
|
+
if (parentId && parentId !== sessionId)
|
|
13
|
+
this.parentBySession.set(sessionId, parentId);
|
|
14
|
+
else
|
|
15
|
+
this.parentBySession.delete(sessionId);
|
|
16
|
+
const existing = this.lastActivityBySession.get(sessionId);
|
|
17
|
+
if (existing !== undefined)
|
|
18
|
+
this.recordActivity(sessionId, existing);
|
|
19
|
+
this.prune(Date.now());
|
|
20
|
+
}
|
|
21
|
+
recordActivity(sessionId, at = Date.now()) {
|
|
22
|
+
if (!sessionId || !Number.isFinite(at))
|
|
23
|
+
return;
|
|
24
|
+
this.prune(at);
|
|
25
|
+
const visited = new Set();
|
|
26
|
+
let current = sessionId;
|
|
27
|
+
for (let depth = 0; current && depth < MAX_ANCESTRY_DEPTH; depth++) {
|
|
28
|
+
if (visited.has(current))
|
|
29
|
+
return;
|
|
30
|
+
visited.add(current);
|
|
31
|
+
const previous = this.lastActivityBySession.get(current);
|
|
32
|
+
if (previous === undefined || at > previous) {
|
|
33
|
+
// Map insertion order is our least-recently-active eviction order.
|
|
34
|
+
this.lastActivityBySession.delete(current);
|
|
35
|
+
this.lastActivityBySession.set(current, at);
|
|
36
|
+
}
|
|
37
|
+
current = this.parentBySession.get(current);
|
|
38
|
+
}
|
|
39
|
+
this.prune(at);
|
|
40
|
+
}
|
|
41
|
+
lastActivityAt(sessionId) {
|
|
42
|
+
this.prune(Date.now());
|
|
43
|
+
return this.lastActivityBySession.get(sessionId);
|
|
44
|
+
}
|
|
45
|
+
removeSession(sessionId) {
|
|
46
|
+
this.parentBySession.delete(sessionId);
|
|
47
|
+
this.lastActivityBySession.delete(sessionId);
|
|
48
|
+
}
|
|
49
|
+
clear() {
|
|
50
|
+
this.parentBySession.clear();
|
|
51
|
+
this.lastActivityBySession.clear();
|
|
52
|
+
}
|
|
53
|
+
prune(now) {
|
|
54
|
+
const oldestAllowed = now - ACTIVITY_RETENTION_MS;
|
|
55
|
+
for (const [sessionId, activityAt] of this.lastActivityBySession) {
|
|
56
|
+
if (activityAt >= oldestAllowed)
|
|
57
|
+
break;
|
|
58
|
+
this.lastActivityBySession.delete(sessionId);
|
|
59
|
+
this.parentBySession.delete(sessionId);
|
|
60
|
+
}
|
|
61
|
+
while (this.lastActivityBySession.size > MAX_TRACKED_SESSIONS) {
|
|
62
|
+
const oldest = this.lastActivityBySession.keys().next().value;
|
|
63
|
+
if (!oldest)
|
|
64
|
+
break;
|
|
65
|
+
this.lastActivityBySession.delete(oldest);
|
|
66
|
+
this.parentBySession.delete(oldest);
|
|
67
|
+
}
|
|
68
|
+
while (this.parentBySession.size > MAX_TRACKED_SESSIONS) {
|
|
69
|
+
const oldest = this.parentBySession.keys().next().value;
|
|
70
|
+
if (!oldest)
|
|
71
|
+
break;
|
|
72
|
+
this.parentBySession.delete(oldest);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
export const sessionActivity = new SessionActivityTracker();
|
package/dist/auth.d.ts
CHANGED
|
@@ -15,6 +15,8 @@ export declare class AuthTimeoutError extends Error {
|
|
|
15
15
|
}
|
|
16
16
|
export declare function isExpiringSoon(jwt: string, thresholdS?: number): boolean;
|
|
17
17
|
export declare function decodeJwtPayload(jwt: string): Record<string, unknown> | null;
|
|
18
|
+
/** JWT `exp` claim as epoch milliseconds, or null if missing/malformed. */
|
|
19
|
+
export declare function decodeJwtExpiryMs(jwt: string): number | null;
|
|
18
20
|
export declare function useAuthToken(token: string): {
|
|
19
21
|
accessToken: string;
|
|
20
22
|
};
|
|
@@ -45,4 +47,3 @@ export declare function generatePkceParams(): PkceParams;
|
|
|
45
47
|
export declare function generatePkceChallenge(verifier: string): Promise<string>;
|
|
46
48
|
export declare function buildLoginUrl(challenge: string, uuid: string, websiteUrl?: string): string;
|
|
47
49
|
export declare function pollForTokens(uuid: string, verifier: string, baseUrl?: string, signal?: AbortSignal, maxAttempts?: number): Promise<TokenPair>;
|
|
48
|
-
export declare function loginWithBrowser(websiteUrl?: string, apiBaseUrl?: string, signal?: AbortSignal): Promise<TokenPair>;
|
package/dist/auth.js
CHANGED
|
@@ -32,22 +32,35 @@ export class AuthTimeoutError extends Error {
|
|
|
32
32
|
}
|
|
33
33
|
// ── Helpers ──
|
|
34
34
|
export function isExpiringSoon(jwt, thresholdS = 300) {
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
return (payload.exp * 1000 - Date.now()) < thresholdS * 1000;
|
|
38
|
-
}
|
|
39
|
-
catch {
|
|
35
|
+
const payload = decodeJwtPayload(jwt);
|
|
36
|
+
if (!payload || typeof payload.exp !== "number" || !Number.isFinite(payload.exp)) {
|
|
40
37
|
return true;
|
|
41
38
|
}
|
|
39
|
+
return payload.exp * 1000 - Date.now() < thresholdS * 1000;
|
|
42
40
|
}
|
|
43
41
|
export function decodeJwtPayload(jwt) {
|
|
44
42
|
try {
|
|
45
|
-
|
|
43
|
+
const segment = jwt.split(".")[1];
|
|
44
|
+
if (!segment)
|
|
45
|
+
return null;
|
|
46
|
+
const json = Buffer.from(segment, "base64url").toString("utf8");
|
|
47
|
+
const payload = JSON.parse(json);
|
|
48
|
+
if (!payload || typeof payload !== "object" || Array.isArray(payload))
|
|
49
|
+
return null;
|
|
50
|
+
return payload;
|
|
46
51
|
}
|
|
47
52
|
catch {
|
|
48
53
|
return null;
|
|
49
54
|
}
|
|
50
55
|
}
|
|
56
|
+
/** JWT `exp` claim as epoch milliseconds, or null if missing/malformed. */
|
|
57
|
+
export function decodeJwtExpiryMs(jwt) {
|
|
58
|
+
const payload = decodeJwtPayload(jwt);
|
|
59
|
+
if (!payload || typeof payload.exp !== "number" || !Number.isFinite(payload.exp)) {
|
|
60
|
+
return null;
|
|
61
|
+
}
|
|
62
|
+
return payload.exp * 1000;
|
|
63
|
+
}
|
|
51
64
|
function base64url(bytes) {
|
|
52
65
|
return btoa(String.fromCharCode(...bytes))
|
|
53
66
|
.replace(/\+/g, "-")
|
|
@@ -191,10 +204,3 @@ export async function pollForTokens(uuid, verifier, baseUrl = API_BASE, signal,
|
|
|
191
204
|
}
|
|
192
205
|
throw new AuthTimeoutError(`Poll timed out after ${maxAttempts} attempts (~5 min)`);
|
|
193
206
|
}
|
|
194
|
-
// ── Combined login (Mode C, one-shot) ──
|
|
195
|
-
export async function loginWithBrowser(websiteUrl, apiBaseUrl, signal) {
|
|
196
|
-
const params = generatePkceParams();
|
|
197
|
-
const challenge = await generatePkceChallenge(params.verifier);
|
|
198
|
-
const loginUrl = buildLoginUrl(challenge, params.uuid, websiteUrl);
|
|
199
|
-
return await pollForTokens(params.uuid, params.verifier, apiBaseUrl, signal);
|
|
200
|
-
}
|
package/dist/context/build.js
CHANGED
|
@@ -27,11 +27,6 @@ export async function buildRequestContext(input) {
|
|
|
27
27
|
const mcpServerNames = Object.keys(config.mcp ?? {});
|
|
28
28
|
const flat = toolsToDescriptors(tools, providerIdentifier, mcpServerNames);
|
|
29
29
|
const nested = toolsToMcpDescriptors(tools, providerIdentifier, mcpServerNames);
|
|
30
|
-
const permission = config.permission;
|
|
31
|
-
const autoRun = permission === "allow" ||
|
|
32
|
-
(typeof permission === "object" &&
|
|
33
|
-
permission !== null &&
|
|
34
|
-
permission["*"] === "allow");
|
|
35
30
|
const ctx = {
|
|
36
31
|
env: buildEnv(workspaceRoot),
|
|
37
32
|
tools: flat,
|
|
@@ -72,8 +67,6 @@ export async function buildRequestContext(input) {
|
|
|
72
67
|
custom_subagents_info_complete: true,
|
|
73
68
|
mcp_file_system_info_complete: true,
|
|
74
69
|
mcp_info_complete: true,
|
|
75
|
-
user_permissions_auto_run: autoRun,
|
|
76
|
-
project_permissions_auto_run: autoRun,
|
|
77
70
|
};
|
|
78
71
|
if (plugins.length > 0) {
|
|
79
72
|
ctx.hooks_additional_context = plugins
|
package/dist/context/rules.d.ts
CHANGED
|
@@ -9,6 +9,10 @@ export type OpencodeJson = {
|
|
|
9
9
|
plugins?: string[];
|
|
10
10
|
mcp?: Record<string, unknown>;
|
|
11
11
|
};
|
|
12
|
+
/** Same truthy rule as OpenCode's Flag.OPENCODE_DISABLE_PROJECT_CONFIG. */
|
|
13
|
+
export declare function isProjectConfigDisabled(): boolean;
|
|
14
|
+
/** Fetch a remote instruction with one deadline covering headers and body. */
|
|
15
|
+
export declare function fetchRemoteInstruction(url: string, timeoutMs?: number): Promise<string | undefined>;
|
|
12
16
|
export declare function loadMergedConfig(workspaceRoot: string): Promise<OpencodeJson>;
|
|
13
17
|
/**
|
|
14
18
|
* Collect OpenCode instruction files.
|
package/dist/context/rules.js
CHANGED
|
@@ -129,8 +129,40 @@ async function expandGlob(pattern, workspaceRoot) {
|
|
|
129
129
|
await walk(startDir, 0);
|
|
130
130
|
return out;
|
|
131
131
|
}
|
|
132
|
+
/** Same truthy rule as OpenCode's Flag.OPENCODE_DISABLE_PROJECT_CONFIG. */
|
|
133
|
+
export function isProjectConfigDisabled() {
|
|
134
|
+
const value = process.env.OPENCODE_DISABLE_PROJECT_CONFIG?.toLowerCase();
|
|
135
|
+
return value === "true" || value === "1";
|
|
136
|
+
}
|
|
137
|
+
/** Fetch a remote instruction with one deadline covering headers and body. */
|
|
138
|
+
export async function fetchRemoteInstruction(url, timeoutMs = 5000) {
|
|
139
|
+
const ctrl = new AbortController();
|
|
140
|
+
const timer = setTimeout(() => ctrl.abort(), Math.max(1, timeoutMs));
|
|
141
|
+
try {
|
|
142
|
+
const res = await fetch(url, { signal: ctrl.signal });
|
|
143
|
+
if (!res.ok)
|
|
144
|
+
return undefined;
|
|
145
|
+
return await res.text();
|
|
146
|
+
}
|
|
147
|
+
catch {
|
|
148
|
+
return undefined;
|
|
149
|
+
}
|
|
150
|
+
finally {
|
|
151
|
+
clearTimeout(timer);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
132
154
|
export async function loadMergedConfig(workspaceRoot) {
|
|
133
155
|
const globalConfig = await readJsonConfig(opencodeGlobalConfigDir());
|
|
156
|
+
if (isProjectConfigDisabled()) {
|
|
157
|
+
return {
|
|
158
|
+
...globalConfig,
|
|
159
|
+
instructions: [...(globalConfig.instructions ?? [])],
|
|
160
|
+
plugin: [...(globalConfig.plugin ?? [])],
|
|
161
|
+
plugins: [...(globalConfig.plugins ?? [])],
|
|
162
|
+
mcp: { ...(globalConfig.mcp ?? {}) },
|
|
163
|
+
permission: globalConfig.permission,
|
|
164
|
+
};
|
|
165
|
+
}
|
|
134
166
|
const projectConfig = await readJsonConfig(workspaceRoot);
|
|
135
167
|
return {
|
|
136
168
|
...globalConfig,
|
|
@@ -162,33 +194,37 @@ export async function collectRules(workspaceRoot) {
|
|
|
162
194
|
seen.add(resolved);
|
|
163
195
|
rules.push(rule);
|
|
164
196
|
};
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
197
|
+
// Match OpenCode: OPENCODE_DISABLE_PROJECT_CONFIG skips project AGENTS/CLAUDE/CONTEXT
|
|
198
|
+
// discovery and project opencode.json (see loadMergedConfig).
|
|
199
|
+
if (!isProjectConfigDisabled()) {
|
|
200
|
+
for (const name of ["AGENTS.md", "CLAUDE.md", "CONTEXT.md"]) {
|
|
201
|
+
const hit = await findUp(name, workspaceRoot, worktree);
|
|
202
|
+
if (hit) {
|
|
203
|
+
await add(hit);
|
|
204
|
+
break;
|
|
205
|
+
}
|
|
170
206
|
}
|
|
171
207
|
}
|
|
172
208
|
await add(path.join(opencodeGlobalConfigDir(), "AGENTS.md"));
|
|
173
209
|
await add(path.join(homedir(), ".claude", "CLAUDE.md"));
|
|
174
210
|
for (const raw of config.instructions ?? []) {
|
|
175
211
|
if (raw.startsWith("http://") || raw.startsWith("https://")) {
|
|
212
|
+
// HTTPS-only. Redirects (incl. to a local proxy) are intentional — do not
|
|
213
|
+
// disable follow-redirects or reject localhost/private/metadata hosts.
|
|
214
|
+
let remoteUrl;
|
|
176
215
|
try {
|
|
177
|
-
|
|
178
|
-
const t = setTimeout(() => ctrl.abort(), 5000);
|
|
179
|
-
const res = await fetch(raw, { signal: ctrl.signal });
|
|
180
|
-
clearTimeout(t);
|
|
181
|
-
if (!res.ok)
|
|
182
|
-
continue;
|
|
183
|
-
const content = await res.text();
|
|
184
|
-
if (!content.trim() || seen.has(raw))
|
|
185
|
-
continue;
|
|
186
|
-
seen.add(raw);
|
|
187
|
-
rules.push({ fullPath: raw, content });
|
|
216
|
+
remoteUrl = new URL(raw);
|
|
188
217
|
}
|
|
189
218
|
catch {
|
|
190
|
-
|
|
219
|
+
continue;
|
|
191
220
|
}
|
|
221
|
+
if (remoteUrl.protocol !== "https:")
|
|
222
|
+
continue;
|
|
223
|
+
const content = await fetchRemoteInstruction(remoteUrl.href);
|
|
224
|
+
if (!content?.trim() || seen.has(raw))
|
|
225
|
+
continue;
|
|
226
|
+
seen.add(raw);
|
|
227
|
+
rules.push({ fullPath: raw, content });
|
|
192
228
|
continue;
|
|
193
229
|
}
|
|
194
230
|
const expanded = resolveHomeRelative(raw);
|
package/dist/debug.d.ts
CHANGED
|
@@ -1 +1,10 @@
|
|
|
1
|
+
/** Resolve the debug log path (env override or per-uid tmpdir default). */
|
|
2
|
+
export declare function resolveDebugLogPath(): string;
|
|
3
|
+
/**
|
|
4
|
+
* Ensure the log directory is 0o700 and create/truncate the log file as 0o600.
|
|
5
|
+
* Exported for tests; callers normally go through `trace`.
|
|
6
|
+
*/
|
|
7
|
+
export declare function ensureSecureDebugLog(filePath: string, options?: {
|
|
8
|
+
secureParent?: boolean;
|
|
9
|
+
}): void;
|
|
1
10
|
export declare function trace(msg: string): void;
|
package/dist/debug.js
CHANGED
|
@@ -1,20 +1,60 @@
|
|
|
1
1
|
import fs from "node:fs";
|
|
2
|
+
import os from "node:os";
|
|
3
|
+
import path from "node:path";
|
|
2
4
|
// Wire-level diagnostics. Opt in with CURSOR_PROVIDER_DEBUG=1 (or "true").
|
|
3
|
-
//
|
|
5
|
+
// Default path mirrors Cursor CLI: $TMPDIR/cursor-provider-logs-<uid>/debug-<pid>.log
|
|
6
|
+
// with directory mode 0o700 and file mode 0o600. Override with CURSOR_PROVIDER_DEBUG_FILE.
|
|
4
7
|
// Truncated once per process. Tokens / checksums should be redacted by callers.
|
|
5
8
|
const DEBUG_ENABLED = process.env.CURSOR_PROVIDER_DEBUG === "1" ||
|
|
6
9
|
process.env.CURSOR_PROVIDER_DEBUG === "true";
|
|
7
|
-
const DEBUG_FILE = process.env.CURSOR_PROVIDER_DEBUG_FILE || "/tmp/cursor-provider-debug.log";
|
|
8
10
|
let _traceInitialized = false;
|
|
11
|
+
let _debugFile;
|
|
12
|
+
let _debugFileUsesManagedDirectory = false;
|
|
13
|
+
/** Resolve the debug log path (env override or per-uid tmpdir default). */
|
|
14
|
+
export function resolveDebugLogPath() {
|
|
15
|
+
if (process.env.CURSOR_PROVIDER_DEBUG_FILE) {
|
|
16
|
+
return process.env.CURSOR_PROVIDER_DEBUG_FILE;
|
|
17
|
+
}
|
|
18
|
+
const uid = typeof process.getuid === "function" ? process.getuid() : process.pid;
|
|
19
|
+
return path.join(os.tmpdir(), `cursor-provider-logs-${uid}`, `debug-${process.pid}.log`);
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Ensure the log directory is 0o700 and create/truncate the log file as 0o600.
|
|
23
|
+
* Exported for tests; callers normally go through `trace`.
|
|
24
|
+
*/
|
|
25
|
+
export function ensureSecureDebugLog(filePath, options = {}) {
|
|
26
|
+
const dir = path.dirname(filePath);
|
|
27
|
+
const secureParent = options.secureParent ?? true;
|
|
28
|
+
fs.mkdirSync(dir, secureParent
|
|
29
|
+
? { recursive: true, mode: 0o700 }
|
|
30
|
+
: { recursive: true });
|
|
31
|
+
if (secureParent) {
|
|
32
|
+
const stat = fs.lstatSync(dir);
|
|
33
|
+
if (!stat.isDirectory() || stat.isSymbolicLink()) {
|
|
34
|
+
throw new Error(`Debug log directory is not a real directory: ${dir}`);
|
|
35
|
+
}
|
|
36
|
+
if (typeof process.getuid === "function" && stat.uid !== process.getuid()) {
|
|
37
|
+
throw new Error(`Debug log directory is not owned by the current user: ${dir}`);
|
|
38
|
+
}
|
|
39
|
+
fs.chmodSync(dir, 0o700);
|
|
40
|
+
}
|
|
41
|
+
fs.writeFileSync(filePath, "", { mode: 0o600 });
|
|
42
|
+
fs.chmodSync(filePath, 0o600);
|
|
43
|
+
}
|
|
9
44
|
export function trace(msg) {
|
|
10
45
|
if (!DEBUG_ENABLED)
|
|
11
46
|
return;
|
|
12
47
|
try {
|
|
48
|
+
if (!_debugFile) {
|
|
49
|
+
_debugFileUsesManagedDirectory = !process.env.CURSOR_PROVIDER_DEBUG_FILE;
|
|
50
|
+
_debugFile = resolveDebugLogPath();
|
|
51
|
+
}
|
|
13
52
|
if (!_traceInitialized) {
|
|
53
|
+
ensureSecureDebugLog(_debugFile, { secureParent: _debugFileUsesManagedDirectory });
|
|
54
|
+
fs.writeFileSync(_debugFile, `--- cursor-provider debug (pid ${process.pid}) ${new Date().toISOString()} ---\n`, { mode: 0o600 });
|
|
14
55
|
_traceInitialized = true;
|
|
15
|
-
fs.writeFileSync(DEBUG_FILE, `--- cursor-provider debug (pid ${process.pid}) ${new Date().toISOString()} ---\n`);
|
|
16
56
|
}
|
|
17
|
-
fs.appendFileSync(
|
|
57
|
+
fs.appendFileSync(_debugFile, `[${new Date().toISOString()}] ${msg}\n`);
|
|
18
58
|
}
|
|
19
59
|
catch {
|
|
20
60
|
/* ignore */
|
package/dist/errors.d.ts
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
export type CursorErrorOrigin = "local-cancel" | "transport" | "server" | "protocol" | "auth";
|
|
2
|
+
export type CursorErrorDiagnostics = {
|
|
3
|
+
statusCode?: number;
|
|
4
|
+
grpcStatus?: number | string;
|
|
5
|
+
rstCode?: number;
|
|
6
|
+
code?: string;
|
|
7
|
+
retryAfterMs?: number;
|
|
8
|
+
};
|
|
9
|
+
export type CursorProviderErrorOptions = CursorErrorDiagnostics & {
|
|
10
|
+
origin: CursorErrorOrigin;
|
|
11
|
+
transient: boolean;
|
|
12
|
+
replaySafe: boolean;
|
|
13
|
+
cause?: unknown;
|
|
14
|
+
};
|
|
15
|
+
/** Structured provider failure with retry and transport diagnostics. */
|
|
16
|
+
export declare class CursorProviderError extends Error {
|
|
17
|
+
readonly origin: CursorErrorOrigin;
|
|
18
|
+
readonly transient: boolean;
|
|
19
|
+
replaySafe: boolean;
|
|
20
|
+
readonly statusCode?: number;
|
|
21
|
+
readonly grpcStatus?: number | string;
|
|
22
|
+
readonly rstCode?: number;
|
|
23
|
+
readonly code?: string;
|
|
24
|
+
readonly retryAfterMs?: number;
|
|
25
|
+
constructor(message: string, options: CursorProviderErrorOptions);
|
|
26
|
+
}
|
|
27
|
+
export declare class CursorLocalCancellationError extends CursorProviderError {
|
|
28
|
+
constructor(message?: string, cause?: unknown);
|
|
29
|
+
}
|
|
30
|
+
export declare class CursorTransportError extends CursorProviderError {
|
|
31
|
+
constructor(message: string, options?: Omit<CursorProviderErrorOptions, "origin">);
|
|
32
|
+
}
|
|
33
|
+
export declare class CursorServerError extends CursorProviderError {
|
|
34
|
+
constructor(message: string, options: Omit<CursorProviderErrorOptions, "origin">);
|
|
35
|
+
}
|
|
36
|
+
export declare class CursorProtocolError extends CursorProviderError {
|
|
37
|
+
constructor(message: string, options?: Partial<CursorErrorDiagnostics> & {
|
|
38
|
+
cause?: unknown;
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
export declare class CursorAuthError extends CursorProviderError {
|
|
42
|
+
constructor(message?: string, options?: Partial<CursorErrorDiagnostics> & {
|
|
43
|
+
cause?: unknown;
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
export declare class CursorRetryExhaustedError extends CursorProviderError {
|
|
47
|
+
readonly attempts: number;
|
|
48
|
+
constructor(attempts: number, last: CursorProviderError);
|
|
49
|
+
}
|
|
50
|
+
export declare function isTransientGrpcStatus(status: number | string): boolean;
|
|
51
|
+
export declare function isAuthGrpcStatus(status: number | string): boolean;
|
|
52
|
+
export declare function cursorHttpError(operation: string, statusCode: number, diagnostics?: Omit<CursorErrorDiagnostics, "statusCode">): CursorProviderError;
|
|
53
|
+
export declare function cursorGrpcError(operation: string, grpcStatus: number | string, diagnostics?: Omit<CursorErrorDiagnostics, "grpcStatus">): CursorProviderError;
|
|
54
|
+
export declare function errorCode(error: unknown): string | undefined;
|
|
55
|
+
export declare function toCursorProviderError(error: unknown, options?: {
|
|
56
|
+
replaySafe: boolean;
|
|
57
|
+
fallback?: string;
|
|
58
|
+
}): CursorProviderError;
|
|
59
|
+
export declare function retrySuppressedError(cause: CursorProviderError, reason: string, attempt: number, maxAttempts: number): CursorProviderError;
|