cursor-opencode-provider 0.2.4 → 0.2.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +15 -11
- package/SECURITY.md +42 -0
- package/dist/auth.d.ts +2 -1
- package/dist/auth.js +19 -13
- 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/language-model.d.ts +18 -4
- package/dist/language-model.js +149 -30
- package/dist/plugin.js +28 -12
- package/dist/protocol/interactions.d.ts +8 -1
- package/dist/protocol/interactions.js +15 -2
- package/dist/protocol/messages.js +7 -2
- package/dist/protocol/request.d.ts +4 -2
- package/dist/protocol/request.js +5 -5
- package/dist/protocol/tool-call-bridge.js +11 -1
- package/dist/protocol/tools.d.ts +2 -2
- package/dist/protocol/tools.js +145 -28
- package/dist/session.d.ts +2 -0
- package/dist/shell-timeout.d.ts +52 -3
- package/dist/shell-timeout.js +277 -23
- package/package.json +3 -2
package/README.md
CHANGED
|
@@ -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.6"`.
|
|
51
51
|
|
|
52
52
|
### From a local clone
|
|
53
53
|
|
|
@@ -165,7 +165,7 @@ const model = cursor.languageModel("composer-2.5")
|
|
|
165
165
|
// model implements AI SDK LanguageModelV3 (doStream / doGenerate)
|
|
166
166
|
```
|
|
167
167
|
|
|
168
|
-
Pass either `accessToken` (JWT from OAuth or key exchange) or `apiKey` (raw `sk-...` key). Optional: `apiBaseURL`, `agentBaseURL`, `headers`, `telemetryEnabled`, `retry`, and `continuation`.
|
|
168
|
+
Pass either `accessToken` (JWT from OAuth or key exchange) or `apiKey` (raw `sk-...` key). Optional: `apiBaseURL`, `agentBaseURL`, `headers`, `telemetryEnabled`, `retry`, and `continuation`. Transient failures resume from the latest checkpoint produced by that Run, matching Cursor CLI; without an eligible checkpoint, retries remain limited to replay-safe attempts so completed text or tool work is not duplicated. 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`.
|
|
169
169
|
|
|
170
170
|
## Environment variables
|
|
171
171
|
|
|
@@ -175,7 +175,7 @@ Pass either `accessToken` (JWT from OAuth or key exchange) or `apiKey` (raw `sk-
|
|
|
175
175
|
| `CURSOR_API_BASE_URL` | Override API base for auth, model discovery, and `GetServerConfig` agent URL resolution (default `https://api2.cursor.sh`) |
|
|
176
176
|
| `CURSOR_GET_SERVER_CONFIG_TELEMETRY` | Set to `1` or `true` to opt the `GetServerConfig` lookup into telemetry in OpenCode/plugin usage |
|
|
177
177
|
| `CURSOR_PROVIDER_DEBUG` | Set to `1` or `true` to enable wire-level debug logging |
|
|
178
|
-
| `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>/`) |
|
|
179
179
|
| `XDG_CACHE_HOME` | When set, model/version caches go under `$XDG_CACHE_HOME/opencode/` instead of `~/.cache/opencode/` |
|
|
180
180
|
| `XDG_DATA_HOME` | When set, OpenCode `auth.json` is read from `$XDG_DATA_HOME/opencode/` instead of `~/.local/share/opencode/` |
|
|
181
181
|
|
|
@@ -219,7 +219,7 @@ OpenCode
|
|
|
219
219
|
|
|
220
220
|
### Injected system guidance
|
|
221
221
|
|
|
222
|
-
The provider adds OpenCode-specific system guidance to normal tool-capable conversations, including tool availability
|
|
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
223
|
|
|
224
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
225
|
|
|
@@ -242,11 +242,15 @@ The package root intentionally stays plugin-safe for OpenCode's classic loader.
|
|
|
242
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. |
|
|
243
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. |
|
|
244
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. |
|
|
245
|
-
| Stream hangs or HTTP/2 errors | The provider keeps Cursor's Run open across OpenCode tool calls, rotates aged shared connections,
|
|
245
|
+
| Stream hangs or HTTP/2 errors | The provider keeps Cursor's Run open across OpenCode tool calls, rotates aged shared connections, resumes transient interruptions from the latest eligible Cursor checkpoint, and falls back to a fresh-history rebase only before stateful output when no checkpoint exists. Repeated interruption is surfaced as an error instead of a false successful stop; retry the turn after checking connectivity. With debug logging enabled, look for `Run interrupted`, `resuming … checkpoint`, or `rebasing fresh Run`. Restart OpenCode after rebuilding a local `file://` install. |
|
|
246
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
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
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
|
|
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`.
|
|
250
254
|
|
|
251
255
|
## Known limitations
|
|
252
256
|
|
|
@@ -255,13 +259,13 @@ The package root intentionally stays plugin-safe for OpenCode's classic loader.
|
|
|
255
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.
|
|
256
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.
|
|
257
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.
|
|
258
|
-
- **Background shells are non-interactive** — Cursor's native background-shell spawn
|
|
259
|
-
- **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. Compaction prompts are unchanged. Unknown future interaction variants fail the turn explicitly instead of hanging the Run stream.
|
|
260
|
-
- **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-
|
|
262
|
+
- **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. The OpenCode UI keeps the original command; wrapping happens via the classic plugin's `shell.env` injectors (`BASH_ENV` / `ZDOTDIR`) so permissions still see the real user command. 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`.
|
|
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.
|
|
261
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.
|
|
262
|
-
- **Interrupted Runs
|
|
266
|
+
- **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.
|
|
263
267
|
- **No fallback models** — if Cursor’s `AvailableModels` API is unreachable and there is no local cache, the provider exposes no models.
|
|
264
268
|
|
|
265
269
|
## License
|
|
266
270
|
|
|
267
|
-
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.
|
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/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/language-model.d.ts
CHANGED
|
@@ -20,10 +20,17 @@ export declare function pumpWithRecovery(input: {
|
|
|
20
20
|
abortSignal?: AbortSignal;
|
|
21
21
|
promptTokens?: number;
|
|
22
22
|
retryPolicy?: CursorRetryPolicy;
|
|
23
|
-
recover: () => Promise<CursorSession>;
|
|
23
|
+
recover: (recovery: CursorRunRecovery) => Promise<CursorSession>;
|
|
24
24
|
onSession?: (session: CursorSession) => void;
|
|
25
25
|
maxRecoveries?: number;
|
|
26
26
|
}): Promise<CursorSession>;
|
|
27
|
+
export type CursorRunRecovery = {
|
|
28
|
+
kind: "rebase";
|
|
29
|
+
} | {
|
|
30
|
+
kind: "resume";
|
|
31
|
+
conversationId: string;
|
|
32
|
+
checkpoint: Uint8Array;
|
|
33
|
+
};
|
|
27
34
|
/**
|
|
28
35
|
* OpenCode re-sends the full tool-result history on every continuation. Prefer
|
|
29
36
|
* the newest result that still has a live pending exec on its tagged session.
|
|
@@ -50,6 +57,9 @@ export declare function pump(session: CursorSession, controller: ReadableStreamD
|
|
|
50
57
|
textId: string;
|
|
51
58
|
reasoningId: string;
|
|
52
59
|
promptTokens?: number;
|
|
60
|
+
requestUsage?: {
|
|
61
|
+
outputChars: number;
|
|
62
|
+
};
|
|
53
63
|
}, abortSignal?: AbortSignal): Promise<void>;
|
|
54
64
|
type ExtractedToolResult = {
|
|
55
65
|
toolCallId: string;
|
|
@@ -87,12 +97,16 @@ export declare function cursorTurnEndedProviderMetadata(te: Record<string, unkno
|
|
|
87
97
|
};
|
|
88
98
|
};
|
|
89
99
|
/**
|
|
90
|
-
* Prior prompt turns for a seed ConversationStateStructure
|
|
91
|
-
*
|
|
92
|
-
* result
|
|
100
|
+
* Prior prompt turns for a seed ConversationStateStructure. Tool results must
|
|
101
|
+
* never be replayed as assistant-authored prose: that teaches the model to
|
|
102
|
+
* counterfeit `Tool result (...)` text instead of emitting a real tool call.
|
|
103
|
+
* Normal rebases omit old results; compaction can retain all results and
|
|
104
|
+
* interrupted continuations retain only the trailing live result suffix as
|
|
105
|
+
* explicit OpenCode-host observations.
|
|
93
106
|
*/
|
|
94
107
|
export declare function extractPromptHistory(prompt: LanguageModelV3CallOptions["prompt"], options?: {
|
|
95
108
|
preserveTrailingUser?: boolean;
|
|
109
|
+
toolResults?: "omit" | "all" | "trailing";
|
|
96
110
|
}): SeedHistoryMessage[];
|
|
97
111
|
/** OpenCode session id header, if present. */
|
|
98
112
|
export declare function opencodeSessionKey(callOptions: LanguageModelV3CallOptions): string | undefined;
|