cursor-opencode-provider 0.2.6 → 0.2.8
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 +31 -15
- package/dist/agent-url.d.ts +8 -5
- package/dist/auth.js +59 -27
- package/dist/context/build.js +12 -1
- package/dist/context/env.d.ts +6 -0
- package/dist/context/env.js +34 -2
- package/dist/context/index.d.ts +1 -1
- package/dist/context/index.js +1 -1
- package/dist/context/paths.d.ts +56 -3
- package/dist/context/paths.js +135 -9
- package/dist/deadline.d.ts +8 -0
- package/dist/deadline.js +25 -0
- package/dist/debug.d.ts +7 -0
- package/dist/debug.js +42 -0
- package/dist/index.d.ts +6 -0
- package/dist/language-model.js +133 -74
- package/dist/models.d.ts +2 -0
- package/dist/plugin-v2.js +4 -0
- package/dist/plugin.js +16 -5
- package/dist/protocol/client-version.js +7 -6
- package/dist/protocol/messages.js +6 -0
- package/dist/protocol/struct.d.ts +18 -0
- package/dist/protocol/struct.js +82 -0
- package/dist/protocol/tools.d.ts +18 -0
- package/dist/protocol/tools.js +89 -21
- package/dist/replay-safety.d.ts +23 -0
- package/dist/replay-safety.js +126 -0
- package/dist/session.d.ts +3 -0
- package/dist/shell-timeout.d.ts +15 -10
- package/dist/shell-timeout.js +91 -54
- package/dist/transport/connect.d.ts +2 -0
- package/dist/transport/connect.js +106 -82
- package/dist/web-tools.d.ts +34 -0
- package/dist/web-tools.js +127 -0
- package/package.json +9 -3
package/dist/shell-timeout.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
|
1
|
+
import { existsSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
|
2
2
|
import { tmpdir } from "node:os";
|
|
3
|
-
import { join } from "node:path";
|
|
3
|
+
import { basename, delimiter, join } from "node:path";
|
|
4
4
|
/** Cursor agent.v1 TimeoutBehavior enum values. */
|
|
5
5
|
export const CURSOR_TIMEOUT_CANCEL = 1;
|
|
6
6
|
export const CURSOR_TIMEOUT_BACKGROUND = 2;
|
|
@@ -14,9 +14,34 @@ const TIMEOUT_MARKER = "__CURSOR_SHELL_TIMEOUT__";
|
|
|
14
14
|
export const BACKGROUND_SHELL_MARKER = "__CURSOR_BACKGROUND_SHELL__";
|
|
15
15
|
const policies = new Map();
|
|
16
16
|
const outcomes = new Map();
|
|
17
|
-
/** callIDs that need shell.env injectors
|
|
17
|
+
/** callIDs that need shell.env injectors or a direct-command fallback. */
|
|
18
18
|
const pendingEnvWraps = new Set();
|
|
19
19
|
const activeEnvWraps = new Map();
|
|
20
|
+
let configuredShell;
|
|
21
|
+
/** Track OpenCode's configured shell from the classic config hook. */
|
|
22
|
+
export function setCursorShellPath(shell) {
|
|
23
|
+
configuredShell = shell?.trim() || undefined;
|
|
24
|
+
}
|
|
25
|
+
function executableOnPath(name) {
|
|
26
|
+
for (const dir of (process.env.PATH ?? "").split(delimiter)) {
|
|
27
|
+
if (dir && existsSync(join(dir, name)))
|
|
28
|
+
return true;
|
|
29
|
+
}
|
|
30
|
+
return false;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Mirror the relevant part of OpenCode Shell.acceptable(): fish/nu are denied,
|
|
34
|
+
* then POSIX falls back to bash when installed and `/bin/sh` otherwise.
|
|
35
|
+
*/
|
|
36
|
+
export function resolveCursorShellKind(shell = configuredShell ?? process.env.SHELL) {
|
|
37
|
+
let name = shell ? basename(shell).toLowerCase().replace(/\.exe$/, "") : "";
|
|
38
|
+
if (name === "fish" || name === "nu" || !name) {
|
|
39
|
+
name = executableOnPath("bash") ? "bash" : "sh";
|
|
40
|
+
}
|
|
41
|
+
if (name === "bash" || name === "zsh" || name === "sh" || name === "dash")
|
|
42
|
+
return name;
|
|
43
|
+
return "other";
|
|
44
|
+
}
|
|
20
45
|
function remember(map, key, value, onEvict) {
|
|
21
46
|
map.delete(key);
|
|
22
47
|
map.set(key, value);
|
|
@@ -128,37 +153,12 @@ export function buildBackgroundShellCommand(command) {
|
|
|
128
153
|
`printf '${BACKGROUND_SHELL_MARKER}%s:%s\\n' "$bg_pid" "$bg_log"`,
|
|
129
154
|
].join("\n");
|
|
130
155
|
}
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
* `args.command` therefore leaks the private wrapper into the TUI/GUI.
|
|
138
|
-
*
|
|
139
|
-
* Wrapping is applied later via {@link cursorShellEnvForCall} (`shell.env`),
|
|
140
|
-
* which uses BASH_ENV / ZDOTDIR injectors so bash/zsh `-c <original>` is
|
|
141
|
-
* replaced with the wrapper while the stored/displayed command stays original.
|
|
142
|
-
* Permissions also keep analyzing the real user command.
|
|
143
|
-
*/
|
|
144
|
-
export function prepareCursorShellArgs(toolCallId, args) {
|
|
145
|
-
const policy = policies.get(toolCallId);
|
|
146
|
-
if (!policy)
|
|
147
|
-
return;
|
|
148
|
-
if (policy.backgroundSpawn) {
|
|
149
|
-
pendingEnvWraps.add(toolCallId);
|
|
150
|
-
return;
|
|
151
|
-
}
|
|
152
|
-
if (policy.timeoutBehavior !== CURSOR_TIMEOUT_BACKGROUND)
|
|
153
|
-
return;
|
|
154
|
-
pendingEnvWraps.add(toolCallId);
|
|
155
|
-
// The wrapper returns just after Cursor's foreground window. OpenCode's own
|
|
156
|
-
// timeout is only an outer safety net and must not win the race.
|
|
157
|
-
args.timeout = Math.max(OPENCODE_TIMEOUT_GRACE_MS, policy.timeoutMs + OPENCODE_TIMEOUT_GRACE_MS);
|
|
158
|
-
}
|
|
159
|
-
/** Restore the model-facing command in OpenCode's completed tool title. */
|
|
160
|
-
export function cursorShellOriginalCommand(toolCallId) {
|
|
161
|
-
return policies.get(toolCallId)?.command || undefined;
|
|
156
|
+
function wrapperBodyForPolicy(policy) {
|
|
157
|
+
if (policy.backgroundSpawn)
|
|
158
|
+
return buildBackgroundShellCommand(policy.command);
|
|
159
|
+
if (policy.timeoutBehavior === CURSOR_TIMEOUT_BACKGROUND)
|
|
160
|
+
return buildSoftBackgroundCommand(policy);
|
|
161
|
+
return undefined;
|
|
162
162
|
}
|
|
163
163
|
function writeShellEnvInjector(wrapperBody) {
|
|
164
164
|
const dir = mkdtempSync(join(tmpdir(), "cursor-opencode-wrap-"));
|
|
@@ -166,13 +166,8 @@ function writeShellEnvInjector(wrapperBody) {
|
|
|
166
166
|
const bashEnvPath = join(dir, "bashenv.sh");
|
|
167
167
|
const zshenvPath = join(dir, ".zshenv");
|
|
168
168
|
writeFileSync(wrapperPath, `${wrapperBody}\n`, { mode: 0o700 });
|
|
169
|
-
// Sourced by
|
|
170
|
-
//
|
|
171
|
-
//
|
|
172
|
-
// Do not gate on a sticky env flag: soft-background children inherit the
|
|
173
|
-
// wrapper environment, and a leftover CURSOR_OPENCODE_WRAP_ACTIVE=1 would
|
|
174
|
-
// make later injectors no-op (original `-c` runs unwrapped). Unsetting the
|
|
175
|
-
// injector vars before `exec` is enough to prevent re-entry.
|
|
169
|
+
// Sourced by bash (BASH_ENV) or zsh (.zshenv via ZDOTDIR). `exec`
|
|
170
|
+
// replaces the host shell before OpenCode's `-c <original>` body runs.
|
|
176
171
|
const injector = [
|
|
177
172
|
"unset BASH_ENV ZDOTDIR ENV CURSOR_OPENCODE_WRAP_ACTIVE",
|
|
178
173
|
`exec /bin/sh ${shellQuote(wrapperPath)}`,
|
|
@@ -181,6 +176,7 @@ function writeShellEnvInjector(wrapperBody) {
|
|
|
181
176
|
writeFileSync(bashEnvPath, injector, { mode: 0o600 });
|
|
182
177
|
writeFileSync(zshenvPath, injector, { mode: 0o600 });
|
|
183
178
|
return {
|
|
179
|
+
wrapperPath,
|
|
184
180
|
env: {
|
|
185
181
|
BASH_ENV: bashEnvPath,
|
|
186
182
|
ZDOTDIR: dir,
|
|
@@ -195,6 +191,55 @@ function writeShellEnvInjector(wrapperBody) {
|
|
|
195
191
|
},
|
|
196
192
|
};
|
|
197
193
|
}
|
|
194
|
+
function ensureShellEnvWrap(toolCallId, policy) {
|
|
195
|
+
const existing = activeEnvWraps.get(toolCallId);
|
|
196
|
+
if (existing)
|
|
197
|
+
return existing;
|
|
198
|
+
const wrapperBody = wrapperBodyForPolicy(policy);
|
|
199
|
+
if (!wrapperBody)
|
|
200
|
+
return undefined;
|
|
201
|
+
const wrap = writeShellEnvInjector(wrapperBody);
|
|
202
|
+
remember(activeEnvWraps, toolCallId, wrap, (evicted) => evicted.cleanup());
|
|
203
|
+
return wrap;
|
|
204
|
+
}
|
|
205
|
+
/**
|
|
206
|
+
* Prepare OpenCode Bash args before execution when Cursor requested wrapping.
|
|
207
|
+
*
|
|
208
|
+
* bash/zsh source the shell.env injector, so the original command remains in
|
|
209
|
+
* OpenCode's permission/UI state. sh/dash ignore those startup variables; for
|
|
210
|
+
* them, use a short `exec wrapper.sh` command that contains no user payload.
|
|
211
|
+
*
|
|
212
|
+
* background_shell_spawn may already contain the inline non-plugin fallback.
|
|
213
|
+
* The classic hook replaces it with the original command (bash/zsh) or the
|
|
214
|
+
* shorter wrapper-file command (sh/dash), avoiding duplicate execution.
|
|
215
|
+
*/
|
|
216
|
+
export function prepareCursorShellArgs(toolCallId, args) {
|
|
217
|
+
const policy = policies.get(toolCallId);
|
|
218
|
+
if (!policy)
|
|
219
|
+
return;
|
|
220
|
+
if (!policy.backgroundSpawn && policy.timeoutBehavior !== CURSOR_TIMEOUT_BACKGROUND)
|
|
221
|
+
return;
|
|
222
|
+
pendingEnvWraps.add(toolCallId);
|
|
223
|
+
if (!policy.backgroundSpawn) {
|
|
224
|
+
// The wrapper returns just after Cursor's foreground window. OpenCode's own
|
|
225
|
+
// timeout is only an outer safety net and must not win the race.
|
|
226
|
+
args.timeout = Math.max(OPENCODE_TIMEOUT_GRACE_MS, policy.timeoutMs + OPENCODE_TIMEOUT_GRACE_MS);
|
|
227
|
+
}
|
|
228
|
+
const shellKind = resolveCursorShellKind();
|
|
229
|
+
if (process.platform === "win32" || shellKind === "bash" || shellKind === "zsh") {
|
|
230
|
+
// Native Windows PowerShell/cmd wrapping remains unsupported; do not emit
|
|
231
|
+
// a POSIX /bin/sh command there. Git Bash still uses the env path above.
|
|
232
|
+
args.command = policy.command;
|
|
233
|
+
return;
|
|
234
|
+
}
|
|
235
|
+
const wrap = ensureShellEnvWrap(toolCallId, policy);
|
|
236
|
+
if (wrap)
|
|
237
|
+
args.command = `exec /bin/sh ${shellQuote(wrap.wrapperPath)}`;
|
|
238
|
+
}
|
|
239
|
+
/** Restore the model-facing command in OpenCode's completed tool title. */
|
|
240
|
+
export function cursorShellOriginalCommand(toolCallId) {
|
|
241
|
+
return policies.get(toolCallId)?.command || undefined;
|
|
242
|
+
}
|
|
198
243
|
/** Drop injector temp files for a finished/abandoned Cursor shell call. */
|
|
199
244
|
export function releaseCursorShellEnv(toolCallId) {
|
|
200
245
|
pendingEnvWraps.delete(toolCallId);
|
|
@@ -205,8 +250,8 @@ export function releaseCursorShellEnv(toolCallId) {
|
|
|
205
250
|
active.cleanup();
|
|
206
251
|
}
|
|
207
252
|
/**
|
|
208
|
-
* Env vars for OpenCode's
|
|
209
|
-
*
|
|
253
|
+
* Env vars for OpenCode's shell.env hook. bash/zsh execute the injector; the
|
|
254
|
+
* same materialized wrapper backs the direct-command sh/dash fallback.
|
|
210
255
|
*/
|
|
211
256
|
export function cursorShellEnvForCall(toolCallId) {
|
|
212
257
|
if (typeof toolCallId !== "string" || !toolCallId || !pendingEnvWraps.has(toolCallId))
|
|
@@ -214,18 +259,9 @@ export function cursorShellEnvForCall(toolCallId) {
|
|
|
214
259
|
const policy = policies.get(toolCallId);
|
|
215
260
|
if (!policy)
|
|
216
261
|
return undefined;
|
|
217
|
-
const
|
|
218
|
-
if (
|
|
219
|
-
return existing.env;
|
|
220
|
-
const wrapperBody = policy.backgroundSpawn
|
|
221
|
-
? buildBackgroundShellCommand(policy.command)
|
|
222
|
-
: policy.timeoutBehavior === CURSOR_TIMEOUT_BACKGROUND
|
|
223
|
-
? buildSoftBackgroundCommand(policy)
|
|
224
|
-
: undefined;
|
|
225
|
-
if (!wrapperBody)
|
|
262
|
+
const wrap = ensureShellEnvWrap(toolCallId, policy);
|
|
263
|
+
if (!wrap)
|
|
226
264
|
return undefined;
|
|
227
|
-
const wrap = writeShellEnvInjector(wrapperBody);
|
|
228
|
-
remember(activeEnvWraps, toolCallId, wrap, (evicted) => evicted.cleanup());
|
|
229
265
|
pendingEnvWraps.delete(toolCallId);
|
|
230
266
|
return wrap.env;
|
|
231
267
|
}
|
|
@@ -430,4 +466,5 @@ export function resetCursorShellCalls() {
|
|
|
430
466
|
pendingEnvWraps.clear();
|
|
431
467
|
policies.clear();
|
|
432
468
|
outcomes.clear();
|
|
469
|
+
configuredShell = undefined;
|
|
433
470
|
}
|
|
@@ -5,6 +5,7 @@ export declare function unaryAvailableModels(token: string, options?: {
|
|
|
5
5
|
apiBaseURL?: string;
|
|
6
6
|
baseURL?: string;
|
|
7
7
|
headers?: Record<string, string>;
|
|
8
|
+
timeoutMs?: number;
|
|
8
9
|
}): Promise<Record<string, unknown>>;
|
|
9
10
|
/**
|
|
10
11
|
* Cursor Run stream hosts from GetServerConfig / agentBaseURL.
|
|
@@ -30,6 +31,7 @@ export declare function fetchAgentUrl(token: string, options?: {
|
|
|
30
31
|
apiBaseURL?: string;
|
|
31
32
|
baseURL?: string;
|
|
32
33
|
telemetryEnabled?: boolean;
|
|
34
|
+
timeoutMs?: number;
|
|
33
35
|
}): Promise<string>;
|
|
34
36
|
export type BidiStream = {
|
|
35
37
|
write(msg: Uint8Array): boolean | void;
|
|
@@ -5,8 +5,25 @@ import { getDeviceIds } from "../protocol/device-id.js";
|
|
|
5
5
|
import { resolveClientVersion } from "../protocol/client-version.js";
|
|
6
6
|
import { trace } from "../debug.js";
|
|
7
7
|
import { CursorLocalCancellationError, CursorProtocolError, CursorTransportError, cursorGrpcError, cursorHttpError, errorCode, CursorProviderError, } from "../errors.js";
|
|
8
|
+
import { withAbortDeadline } from "../deadline.js";
|
|
8
9
|
import http2 from "node:http2";
|
|
9
10
|
const API_BASE = `https://${CURSOR_API_HOST}`;
|
|
11
|
+
const DEFAULT_UNARY_TIMEOUT_MS = 5_000;
|
|
12
|
+
const MAX_TIMER_MS = 2_147_483_647;
|
|
13
|
+
function unaryTimeoutMs(operation, value) {
|
|
14
|
+
const timeoutMs = value ?? DEFAULT_UNARY_TIMEOUT_MS;
|
|
15
|
+
if (!Number.isSafeInteger(timeoutMs) || timeoutMs <= 0 || timeoutMs > MAX_TIMER_MS) {
|
|
16
|
+
throw new CursorProtocolError(`${operation} timeout must be a positive integer no greater than ${MAX_TIMER_MS}`, { code: "CURSOR_INVALID_TIMEOUT" });
|
|
17
|
+
}
|
|
18
|
+
return timeoutMs;
|
|
19
|
+
}
|
|
20
|
+
async function withUnaryDeadline(operation, timeoutMs, timeoutCode, run) {
|
|
21
|
+
return withAbortDeadline(timeoutMs, () => new CursorTransportError(`${operation} timed out after ${timeoutMs}ms`, {
|
|
22
|
+
transient: true,
|
|
23
|
+
replaySafe: true,
|
|
24
|
+
code: timeoutCode,
|
|
25
|
+
}), run);
|
|
26
|
+
}
|
|
10
27
|
function resolveApiBaseURL(options) {
|
|
11
28
|
return new URL(options.apiBaseURL ?? options.baseURL ?? API_BASE).origin;
|
|
12
29
|
}
|
|
@@ -33,42 +50,46 @@ export function buildBaseHeaders(token, clientVersion, extra) {
|
|
|
33
50
|
export async function unaryAvailableModels(token, options = {}) {
|
|
34
51
|
const base = resolveApiBaseURL(options);
|
|
35
52
|
const url = `${base}/aiserver.v1.AiService/AvailableModels`;
|
|
36
|
-
const
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
res
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
53
|
+
const timeoutMs = unaryTimeoutMs("AvailableModels", options.timeoutMs);
|
|
54
|
+
return withUnaryDeadline("AvailableModels", timeoutMs, "CURSOR_AVAILABLE_MODELS_TIMEOUT", async (signal) => {
|
|
55
|
+
const clientVersion = await resolveClientVersion();
|
|
56
|
+
const headers = buildBaseHeaders(token, clientVersion, options.headers);
|
|
57
|
+
let res;
|
|
58
|
+
try {
|
|
59
|
+
res = await fetch(url, {
|
|
60
|
+
method: "POST",
|
|
61
|
+
headers: {
|
|
62
|
+
...headers,
|
|
63
|
+
"content-type": "application/json",
|
|
64
|
+
accept: "application/json",
|
|
65
|
+
},
|
|
66
|
+
// Request parameterized effort/context/fast variants like Cursor IDE.
|
|
67
|
+
body: JSON.stringify({
|
|
68
|
+
includeLongContextModels: true,
|
|
69
|
+
useModelParameters: true,
|
|
70
|
+
useCloudAgentEffortModes: true,
|
|
71
|
+
}),
|
|
72
|
+
signal,
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
catch (cause) {
|
|
76
|
+
throw new CursorTransportError("AvailableModels network request failed", {
|
|
77
|
+
transient: true,
|
|
78
|
+
replaySafe: true,
|
|
79
|
+
code: errorCode(cause),
|
|
80
|
+
cause,
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
if (!res.ok) {
|
|
84
|
+
throw await cursorHttpResponseError("AvailableModels failed:", res);
|
|
85
|
+
}
|
|
86
|
+
try {
|
|
87
|
+
return (await res.json());
|
|
88
|
+
}
|
|
89
|
+
catch (cause) {
|
|
90
|
+
throw new CursorProtocolError("AvailableModels returned malformed JSON", { cause });
|
|
91
|
+
}
|
|
92
|
+
});
|
|
72
93
|
}
|
|
73
94
|
// ── Unary (GetServerConfig) ──
|
|
74
95
|
/**
|
|
@@ -116,51 +137,55 @@ export function normalizeAgentRunOrigin(raw) {
|
|
|
116
137
|
export async function fetchAgentUrl(token, options = {}) {
|
|
117
138
|
const base = resolveApiBaseURL(options);
|
|
118
139
|
const url = `${base}${SERVER_CONFIG_PATH}`;
|
|
119
|
-
const
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
res
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
140
|
+
const timeoutMs = unaryTimeoutMs("GetServerConfig", options.timeoutMs);
|
|
141
|
+
return withUnaryDeadline("GetServerConfig", timeoutMs, "CURSOR_AGENT_URL_TIMEOUT", async (signal) => {
|
|
142
|
+
const clientVersion = await resolveClientVersion();
|
|
143
|
+
// Match Cursor CLI: GetServerConfig uses base headers only — session headers belong on Run.
|
|
144
|
+
const headers = buildBaseHeaders(token, clientVersion);
|
|
145
|
+
trace(`GetServerConfig POST ${url}`);
|
|
146
|
+
let res;
|
|
147
|
+
try {
|
|
148
|
+
res = await fetch(url, {
|
|
149
|
+
method: "POST",
|
|
150
|
+
headers: {
|
|
151
|
+
...headers,
|
|
152
|
+
"content-type": "application/json",
|
|
153
|
+
accept: "application/json",
|
|
154
|
+
},
|
|
155
|
+
body: JSON.stringify({ telem_enabled: options.telemetryEnabled ?? false }),
|
|
156
|
+
signal,
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
catch (cause) {
|
|
160
|
+
throw new CursorTransportError("GetServerConfig network request failed", {
|
|
161
|
+
transient: true,
|
|
162
|
+
replaySafe: true,
|
|
163
|
+
code: errorCode(cause),
|
|
164
|
+
cause,
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
if (!res.ok) {
|
|
168
|
+
throw await cursorHttpResponseError("GetServerConfig failed:", res);
|
|
169
|
+
}
|
|
170
|
+
let body;
|
|
171
|
+
try {
|
|
172
|
+
body = (await res.json());
|
|
173
|
+
}
|
|
174
|
+
catch (cause) {
|
|
175
|
+
throw new CursorProtocolError("GetServerConfig returned malformed JSON", { cause });
|
|
176
|
+
}
|
|
177
|
+
const cfg = body.agentUrlConfig;
|
|
178
|
+
const raw = cfg?.agentnUrl || cfg?.agentUrl;
|
|
179
|
+
const normalized = normalizeAgentRunOrigin(raw);
|
|
180
|
+
trace(`GetServerConfig reply: agentnUrl=${cfg?.agentnUrl ?? "<missing>"} ` +
|
|
181
|
+
`agentUrl=${cfg?.agentUrl ?? "<missing>"} → ${normalized ?? "<invalid>"}`);
|
|
182
|
+
if (!normalized) {
|
|
183
|
+
throw new CursorProtocolError(raw
|
|
184
|
+
? "GetServerConfig returned an invalid Cursor agent URL"
|
|
185
|
+
: "GetServerConfig response missing agentUrlConfig.agentnUrl");
|
|
186
|
+
}
|
|
187
|
+
return normalized;
|
|
188
|
+
});
|
|
164
189
|
}
|
|
165
190
|
export class CursorRunInterruptedError extends CursorTransportError {
|
|
166
191
|
constructor(message = "Cursor Run ended before turn_ended", options) {
|
|
@@ -217,7 +242,6 @@ const CONNECT_TIMEOUT_MS = 15_000;
|
|
|
217
242
|
const DEFAULT_SESSION_PING_TIMEOUT_MS = 5_000;
|
|
218
243
|
const DEFAULT_READ_IDLE_MS = 120_000;
|
|
219
244
|
const WRITE_DRAIN_TIMEOUT_CODE = "CURSOR_WRITE_DRAIN_TIMEOUT";
|
|
220
|
-
const MAX_TIMER_MS = 2_147_483_647;
|
|
221
245
|
export const HTTP2_SESSION_MAX_AGE_MS = 15 * 60_000;
|
|
222
246
|
export function shouldReuseHttp2Session(state, createdAt, now = Date.now()) {
|
|
223
247
|
return !state.destroyed && !state.closed && now - createdAt < HTTP2_SESSION_MAX_AGE_MS;
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { type ToolContext, type ToolResult } from "@opencode-ai/plugin";
|
|
2
|
+
export type OpenCodeWebSearchArgs = {
|
|
3
|
+
query: string;
|
|
4
|
+
numResults?: number;
|
|
5
|
+
livecrawl?: "fallback" | "preferred";
|
|
6
|
+
type?: "auto" | "fast" | "deep";
|
|
7
|
+
contextMaxCharacters?: number;
|
|
8
|
+
};
|
|
9
|
+
export declare function parseOpenCodeWebSearchResponse(raw: string): string | undefined;
|
|
10
|
+
export declare function executeOpenCodeWebSearch(args: OpenCodeWebSearchArgs, context: ToolContext, fetchImpl?: typeof fetch): Promise<ToolResult>;
|
|
11
|
+
export declare const openCodeWebSearchTool: {
|
|
12
|
+
description: string;
|
|
13
|
+
args: {
|
|
14
|
+
query: import("zod").ZodString;
|
|
15
|
+
numResults: import("zod").ZodOptional<import("zod").ZodNumber>;
|
|
16
|
+
livecrawl: import("zod").ZodOptional<import("zod").ZodEnum<{
|
|
17
|
+
fallback: "fallback";
|
|
18
|
+
preferred: "preferred";
|
|
19
|
+
}>>;
|
|
20
|
+
type: import("zod").ZodOptional<import("zod").ZodEnum<{
|
|
21
|
+
fast: "fast";
|
|
22
|
+
auto: "auto";
|
|
23
|
+
deep: "deep";
|
|
24
|
+
}>>;
|
|
25
|
+
contextMaxCharacters: import("zod").ZodOptional<import("zod").ZodNumber>;
|
|
26
|
+
};
|
|
27
|
+
execute(args: {
|
|
28
|
+
query: string;
|
|
29
|
+
numResults?: number | undefined;
|
|
30
|
+
livecrawl?: "fallback" | "preferred" | undefined;
|
|
31
|
+
type?: "fast" | "auto" | "deep" | undefined;
|
|
32
|
+
contextMaxCharacters?: number | undefined;
|
|
33
|
+
}, context: ToolContext): Promise<ToolResult>;
|
|
34
|
+
};
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import { tool } from "@opencode-ai/plugin";
|
|
2
|
+
const EXA_MCP_URL = "https://mcp.exa.ai/mcp";
|
|
3
|
+
const WEB_SEARCH_TIMEOUT_MS = 25_000;
|
|
4
|
+
function exaMcpUrl() {
|
|
5
|
+
const apiKey = process.env.EXA_API_KEY;
|
|
6
|
+
if (!apiKey)
|
|
7
|
+
return EXA_MCP_URL;
|
|
8
|
+
const url = new URL(EXA_MCP_URL);
|
|
9
|
+
url.searchParams.set("exaApiKey", apiKey);
|
|
10
|
+
return url.href;
|
|
11
|
+
}
|
|
12
|
+
function mcpText(value) {
|
|
13
|
+
if (!value || typeof value !== "object")
|
|
14
|
+
return undefined;
|
|
15
|
+
const result = value.result;
|
|
16
|
+
if (!result || typeof result !== "object")
|
|
17
|
+
return undefined;
|
|
18
|
+
const content = result.content;
|
|
19
|
+
if (!Array.isArray(content))
|
|
20
|
+
return undefined;
|
|
21
|
+
for (const item of content) {
|
|
22
|
+
if (item &&
|
|
23
|
+
typeof item === "object" &&
|
|
24
|
+
item.type === "text" &&
|
|
25
|
+
typeof item.text === "string") {
|
|
26
|
+
return item.text;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
return undefined;
|
|
30
|
+
}
|
|
31
|
+
export function parseOpenCodeWebSearchResponse(raw) {
|
|
32
|
+
const trimmed = raw.trim();
|
|
33
|
+
if (!trimmed)
|
|
34
|
+
return undefined;
|
|
35
|
+
try {
|
|
36
|
+
const text = mcpText(JSON.parse(trimmed));
|
|
37
|
+
if (text)
|
|
38
|
+
return text;
|
|
39
|
+
}
|
|
40
|
+
catch {
|
|
41
|
+
// MCP may respond as an SSE stream instead of one JSON object.
|
|
42
|
+
}
|
|
43
|
+
for (const line of raw.split("\n")) {
|
|
44
|
+
if (!line.startsWith("data: "))
|
|
45
|
+
continue;
|
|
46
|
+
try {
|
|
47
|
+
const text = mcpText(JSON.parse(line.slice(6)));
|
|
48
|
+
if (text)
|
|
49
|
+
return text;
|
|
50
|
+
}
|
|
51
|
+
catch {
|
|
52
|
+
// Ignore non-JSON SSE events.
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
return undefined;
|
|
56
|
+
}
|
|
57
|
+
export async function executeOpenCodeWebSearch(args, context, fetchImpl = fetch) {
|
|
58
|
+
await context.ask({
|
|
59
|
+
permission: "websearch",
|
|
60
|
+
patterns: [args.query],
|
|
61
|
+
always: ["*"],
|
|
62
|
+
metadata: {
|
|
63
|
+
query: args.query,
|
|
64
|
+
numResults: args.numResults,
|
|
65
|
+
livecrawl: args.livecrawl,
|
|
66
|
+
type: args.type,
|
|
67
|
+
contextMaxCharacters: args.contextMaxCharacters,
|
|
68
|
+
provider: "exa",
|
|
69
|
+
},
|
|
70
|
+
});
|
|
71
|
+
const controller = new AbortController();
|
|
72
|
+
const abort = () => controller.abort(context.abort.reason);
|
|
73
|
+
if (context.abort.aborted)
|
|
74
|
+
abort();
|
|
75
|
+
else
|
|
76
|
+
context.abort.addEventListener("abort", abort, { once: true });
|
|
77
|
+
const timeout = setTimeout(() => controller.abort(new Error("Web search timed out")), WEB_SEARCH_TIMEOUT_MS);
|
|
78
|
+
try {
|
|
79
|
+
const response = await fetchImpl(exaMcpUrl(), {
|
|
80
|
+
method: "POST",
|
|
81
|
+
headers: {
|
|
82
|
+
accept: "application/json, text/event-stream",
|
|
83
|
+
"content-type": "application/json",
|
|
84
|
+
},
|
|
85
|
+
body: JSON.stringify({
|
|
86
|
+
jsonrpc: "2.0",
|
|
87
|
+
id: 1,
|
|
88
|
+
method: "tools/call",
|
|
89
|
+
params: {
|
|
90
|
+
name: "web_search_exa",
|
|
91
|
+
arguments: {
|
|
92
|
+
query: args.query,
|
|
93
|
+
type: args.type ?? "auto",
|
|
94
|
+
numResults: args.numResults ?? 8,
|
|
95
|
+
livecrawl: args.livecrawl ?? "fallback",
|
|
96
|
+
contextMaxCharacters: args.contextMaxCharacters,
|
|
97
|
+
},
|
|
98
|
+
},
|
|
99
|
+
}),
|
|
100
|
+
signal: controller.signal,
|
|
101
|
+
});
|
|
102
|
+
const raw = await response.text();
|
|
103
|
+
if (!response.ok)
|
|
104
|
+
throw new Error(`Web search failed (${response.status}): ${raw.slice(0, 500)}`);
|
|
105
|
+
const output = parseOpenCodeWebSearchResponse(raw) ?? "No search results found. Please try a different query.";
|
|
106
|
+
return {
|
|
107
|
+
title: `Exa Web Search: ${args.query}`,
|
|
108
|
+
output,
|
|
109
|
+
metadata: { provider: "exa" },
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
finally {
|
|
113
|
+
clearTimeout(timeout);
|
|
114
|
+
context.abort.removeEventListener("abort", abort);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
export const openCodeWebSearchTool = tool({
|
|
118
|
+
description: "Search the web for current information using OpenCode's web search backend.",
|
|
119
|
+
args: {
|
|
120
|
+
query: tool.schema.string().describe("Web search query"),
|
|
121
|
+
numResults: tool.schema.number().int().min(1).max(20).optional(),
|
|
122
|
+
livecrawl: tool.schema.enum(["fallback", "preferred"]).optional(),
|
|
123
|
+
type: tool.schema.enum(["auto", "fast", "deep"]).optional(),
|
|
124
|
+
contextMaxCharacters: tool.schema.number().int().positive().optional(),
|
|
125
|
+
},
|
|
126
|
+
execute: executeOpenCodeWebSearch,
|
|
127
|
+
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "cursor-opencode-provider",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.8",
|
|
4
4
|
"description": "Use Cursor subscription models from OpenCode via Cursor's Connect-RPC agent protocol",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -61,7 +61,13 @@
|
|
|
61
61
|
"protobufjs": "^7.4.0"
|
|
62
62
|
},
|
|
63
63
|
"peerDependencies": {
|
|
64
|
-
"@opencode-ai/plugin": "^1.17.13"
|
|
64
|
+
"@opencode-ai/plugin": "^1.17.13",
|
|
65
|
+
"@opencode-compat/profile": ">=0.1.0"
|
|
66
|
+
},
|
|
67
|
+
"peerDependenciesMeta": {
|
|
68
|
+
"@opencode-compat/profile": {
|
|
69
|
+
"optional": true
|
|
70
|
+
}
|
|
65
71
|
},
|
|
66
72
|
"devDependencies": {
|
|
67
73
|
"@opencode-ai/plugin": "^1.17.13",
|
|
@@ -69,4 +75,4 @@
|
|
|
69
75
|
"@types/node": "^22.15.3",
|
|
70
76
|
"typescript": "^5.8.3"
|
|
71
77
|
}
|
|
72
|
-
}
|
|
78
|
+
}
|