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/dist/shell-timeout.js
CHANGED
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { tmpdir } from "node:os";
|
|
3
|
+
import { join } from "node:path";
|
|
1
4
|
/** Cursor agent.v1 TimeoutBehavior enum values. */
|
|
2
5
|
export const CURSOR_TIMEOUT_CANCEL = 1;
|
|
3
6
|
export const CURSOR_TIMEOUT_BACKGROUND = 2;
|
|
@@ -7,16 +10,24 @@ const POLL_INTERVAL_MS = 100;
|
|
|
7
10
|
const BACKGROUND_MARKER = "__CURSOR_SHELL_BACKGROUND__";
|
|
8
11
|
const EXIT_MARKER = "__CURSOR_SHELL_EXIT__";
|
|
9
12
|
const TIMEOUT_MARKER = "__CURSOR_SHELL_TIMEOUT__";
|
|
13
|
+
/** Private marker for Cursor `background_shell_spawn_args` detach wrappers. */
|
|
14
|
+
export const BACKGROUND_SHELL_MARKER = "__CURSOR_BACKGROUND_SHELL__";
|
|
10
15
|
const policies = new Map();
|
|
11
16
|
const outcomes = new Map();
|
|
12
|
-
|
|
17
|
+
/** callIDs that need shell.env injectors (so args.command can stay display-original). */
|
|
18
|
+
const pendingEnvWraps = new Set();
|
|
19
|
+
const activeEnvWraps = new Map();
|
|
20
|
+
function remember(map, key, value, onEvict) {
|
|
13
21
|
map.delete(key);
|
|
14
22
|
map.set(key, value);
|
|
15
23
|
while (map.size > MAX_TRACKED_SHELL_CALLS) {
|
|
16
24
|
const oldest = map.keys().next().value;
|
|
17
25
|
if (!oldest)
|
|
18
26
|
break;
|
|
27
|
+
const evicted = map.get(oldest);
|
|
19
28
|
map.delete(oldest);
|
|
29
|
+
if (evicted !== undefined && onEvict)
|
|
30
|
+
onEvict(evicted);
|
|
20
31
|
}
|
|
21
32
|
}
|
|
22
33
|
function finiteNonNegative(value) {
|
|
@@ -26,7 +37,18 @@ function finiteNonNegative(value) {
|
|
|
26
37
|
return Math.floor(n);
|
|
27
38
|
}
|
|
28
39
|
export function shellPolicyFromMetadata(metadata) {
|
|
29
|
-
if (!metadata
|
|
40
|
+
if (!metadata)
|
|
41
|
+
return undefined;
|
|
42
|
+
if (metadata.background_shell_spawn === true) {
|
|
43
|
+
return {
|
|
44
|
+
command: typeof metadata.command === "string" ? metadata.command : "",
|
|
45
|
+
workingDirectory: typeof metadata.working_directory === "string" ? metadata.working_directory : "",
|
|
46
|
+
timeoutMs: 0,
|
|
47
|
+
timeoutBehavior: 0,
|
|
48
|
+
backgroundSpawn: true,
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
if (metadata.shell_stream !== true)
|
|
30
52
|
return undefined;
|
|
31
53
|
const timeoutMs = finiteNonNegative(metadata.timeout_ms) ?? 30_000;
|
|
32
54
|
const timeoutBehavior = finiteNonNegative(metadata.timeout_behavior) ?? 0;
|
|
@@ -50,9 +72,16 @@ function shellQuote(value) {
|
|
|
50
72
|
return `'${value.replace(/'/g, `'\\''`)}'`;
|
|
51
73
|
}
|
|
52
74
|
/**
|
|
75
|
+
* F11 / soft-background helper.
|
|
76
|
+
*
|
|
53
77
|
* Run a Cursor soft-background command for its foreground window, then leave
|
|
54
|
-
* it detached if still alive. The sentinel is removed by the after
|
|
55
|
-
* OpenCode stores/renders the result.
|
|
78
|
+
* it detached (`nohup`) if still alive. The sentinel is removed by the after
|
|
79
|
+
* hook before OpenCode stores/renders the result.
|
|
80
|
+
*
|
|
81
|
+
* This approximates Cursor's TIMEOUT_BACKGROUND semantics through OpenCode's
|
|
82
|
+
* foreground-only bash tool. Residual: after OpenCode returns, the child (and
|
|
83
|
+
* optional hard-timeout watchdog) may still be running; this provider does not
|
|
84
|
+
* reap leftover processes — cleanup is left to the user / OS.
|
|
56
85
|
*/
|
|
57
86
|
export function buildSoftBackgroundCommand(policy) {
|
|
58
87
|
const polls = Math.ceil(policy.timeoutMs / POLL_INTERVAL_MS);
|
|
@@ -70,15 +99,59 @@ export function buildSoftBackgroundCommand(policy) {
|
|
|
70
99
|
else {
|
|
71
100
|
lines.push('cursor_shell_status=""', 'cursor_shell_watchdog_pid=""');
|
|
72
101
|
}
|
|
73
|
-
lines.push(
|
|
102
|
+
lines.push(
|
|
103
|
+
// Avoid interactive job-control noise ("Terminated: 15 …") when we later
|
|
104
|
+
// reap the watchdog; that text can otherwise land after our private marker
|
|
105
|
+
// and leak into OpenCode's bash UI.
|
|
106
|
+
"set +m 2>/dev/null || true", 'if [ -n "$cursor_shell_watchdog_pid" ]; then disown "$cursor_shell_watchdog_pid" 2>/dev/null || true; fi', 'disown "$cursor_shell_pid" 2>/dev/null || true', "cursor_shell_poll=0", `while [ "$cursor_shell_poll" -lt ${polls} ] && kill -0 "$cursor_shell_pid" 2>/dev/null; do`, ` sleep ${POLL_INTERVAL_MS / 1000}`, " cursor_shell_poll=$((cursor_shell_poll + 1))", "done", 'if kill -0 "$cursor_shell_pid" 2>/dev/null; then', ' cat "$cursor_shell_log"', ` printf '\n${BACKGROUND_MARKER}%s:%s\n' "$cursor_shell_pid" "$cursor_shell_log"`, " exit 0", "fi", 'wait "$cursor_shell_pid" 2>/dev/null', "cursor_shell_code=$?", 'cat "$cursor_shell_log"', 'if [ -n "$cursor_shell_status" ] && [ "$(cat "$cursor_shell_status" 2>/dev/null)" = timeout ]; then',
|
|
107
|
+
// Reap the watchdog before printing the private marker so any residual
|
|
108
|
+
// shell diagnostics cannot trail the sentinel.
|
|
109
|
+
' if [ -n "$cursor_shell_watchdog_pid" ]; then kill "$cursor_shell_watchdog_pid" 2>/dev/null || true; wait "$cursor_shell_watchdog_pid" 2>/dev/null || true; fi', ` printf '\n${TIMEOUT_MARKER}%s\n' ${policy.hardTimeoutMs ?? policy.timeoutMs}`, "else", ' if [ -n "$cursor_shell_watchdog_pid" ]; then kill "$cursor_shell_watchdog_pid" 2>/dev/null || true; wait "$cursor_shell_watchdog_pid" 2>/dev/null || true; fi', ` printf '\n${EXIT_MARKER}%s\n' "$cursor_shell_code"`, "fi", 'rm -f -- "$cursor_shell_log"', 'if [ -n "$cursor_shell_status" ]; then rm -f -- "$cursor_shell_status"; fi');
|
|
74
110
|
return lines.join("\n");
|
|
75
111
|
}
|
|
76
|
-
/**
|
|
112
|
+
/**
|
|
113
|
+
* F11 / background_shell_spawn_args helper.
|
|
114
|
+
*
|
|
115
|
+
* OpenCode's bash tool is foreground-only. Detach the requested command inside
|
|
116
|
+
* that one foreground call (`nohup … &`) and print a private marker containing
|
|
117
|
+
* the spawned PID and log path. With stdin and all output redirected, the host
|
|
118
|
+
* shell can return immediately instead of retaining OpenCode's tool pipe.
|
|
119
|
+
*
|
|
120
|
+
* Residual: the detached child is not reaped by this provider after OpenCode
|
|
121
|
+
* completes the tool call; cleanup is left to the user / OS.
|
|
122
|
+
*/
|
|
123
|
+
export function buildBackgroundShellCommand(command) {
|
|
124
|
+
return [
|
|
125
|
+
'bg_log="$(mktemp "${TMPDIR:-/tmp}/cursor-opencode-bg.XXXXXX")" || exit 1',
|
|
126
|
+
`nohup sh -c ${shellQuote(command)} >"$bg_log" 2>&1 </dev/null &`,
|
|
127
|
+
"bg_pid=$!",
|
|
128
|
+
`printf '${BACKGROUND_SHELL_MARKER}%s:%s\\n' "$bg_pid" "$bg_log"`,
|
|
129
|
+
].join("\n");
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* Prepare OpenCode Bash args before execution when Cursor requested wrapping.
|
|
133
|
+
*
|
|
134
|
+
* Important: do **not** replace `args.command` with the wrapper script.
|
|
135
|
+
* OpenCode's bash UI renders `state.input.command`, and `ctx.metadata()`
|
|
136
|
+
* persists the execute-time `args` object into that field. Mutating
|
|
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
|
+
*/
|
|
77
144
|
export function prepareCursorShellArgs(toolCallId, args) {
|
|
78
145
|
const policy = policies.get(toolCallId);
|
|
79
|
-
if (!policy
|
|
146
|
+
if (!policy)
|
|
80
147
|
return;
|
|
81
|
-
|
|
148
|
+
if (policy.backgroundSpawn) {
|
|
149
|
+
pendingEnvWraps.add(toolCallId);
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
if (policy.timeoutBehavior !== CURSOR_TIMEOUT_BACKGROUND)
|
|
153
|
+
return;
|
|
154
|
+
pendingEnvWraps.add(toolCallId);
|
|
82
155
|
// The wrapper returns just after Cursor's foreground window. OpenCode's own
|
|
83
156
|
// timeout is only an outer safety net and must not win the race.
|
|
84
157
|
args.timeout = Math.max(OPENCODE_TIMEOUT_GRACE_MS, policy.timeoutMs + OPENCODE_TIMEOUT_GRACE_MS);
|
|
@@ -87,12 +160,127 @@ export function prepareCursorShellArgs(toolCallId, args) {
|
|
|
87
160
|
export function cursorShellOriginalCommand(toolCallId) {
|
|
88
161
|
return policies.get(toolCallId)?.command || undefined;
|
|
89
162
|
}
|
|
163
|
+
function writeShellEnvInjector(wrapperBody) {
|
|
164
|
+
const dir = mkdtempSync(join(tmpdir(), "cursor-opencode-wrap-"));
|
|
165
|
+
const wrapperPath = join(dir, "wrapper.sh");
|
|
166
|
+
const bashEnvPath = join(dir, "bashenv.sh");
|
|
167
|
+
const zshenvPath = join(dir, ".zshenv");
|
|
168
|
+
writeFileSync(wrapperPath, `${wrapperBody}\n`, { mode: 0o700 });
|
|
169
|
+
// Sourced by non-interactive bash (BASH_ENV) or zsh (.zshenv via ZDOTDIR).
|
|
170
|
+
// `exec` replaces the host shell so OpenCode's `-c <original>` body never runs.
|
|
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.
|
|
176
|
+
const injector = [
|
|
177
|
+
"unset BASH_ENV ZDOTDIR ENV CURSOR_OPENCODE_WRAP_ACTIVE",
|
|
178
|
+
`exec /bin/sh ${shellQuote(wrapperPath)}`,
|
|
179
|
+
"",
|
|
180
|
+
].join("\n");
|
|
181
|
+
writeFileSync(bashEnvPath, injector, { mode: 0o600 });
|
|
182
|
+
writeFileSync(zshenvPath, injector, { mode: 0o600 });
|
|
183
|
+
return {
|
|
184
|
+
env: {
|
|
185
|
+
BASH_ENV: bashEnvPath,
|
|
186
|
+
ZDOTDIR: dir,
|
|
187
|
+
},
|
|
188
|
+
cleanup: () => {
|
|
189
|
+
try {
|
|
190
|
+
rmSync(dir, { recursive: true, force: true });
|
|
191
|
+
}
|
|
192
|
+
catch {
|
|
193
|
+
// best-effort temp cleanup
|
|
194
|
+
}
|
|
195
|
+
},
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
/** Drop injector temp files for a finished/abandoned Cursor shell call. */
|
|
199
|
+
export function releaseCursorShellEnv(toolCallId) {
|
|
200
|
+
pendingEnvWraps.delete(toolCallId);
|
|
201
|
+
const active = activeEnvWraps.get(toolCallId);
|
|
202
|
+
if (!active)
|
|
203
|
+
return;
|
|
204
|
+
activeEnvWraps.delete(toolCallId);
|
|
205
|
+
active.cleanup();
|
|
206
|
+
}
|
|
207
|
+
/**
|
|
208
|
+
* Env vars for OpenCode's `shell.env` hook so bash/zsh execute the Cursor
|
|
209
|
+
* wrapper while `args.command` (and therefore the bash UI) stay original.
|
|
210
|
+
*/
|
|
211
|
+
export function cursorShellEnvForCall(toolCallId) {
|
|
212
|
+
if (typeof toolCallId !== "string" || !toolCallId || !pendingEnvWraps.has(toolCallId))
|
|
213
|
+
return undefined;
|
|
214
|
+
const policy = policies.get(toolCallId);
|
|
215
|
+
if (!policy)
|
|
216
|
+
return undefined;
|
|
217
|
+
const existing = activeEnvWraps.get(toolCallId);
|
|
218
|
+
if (existing)
|
|
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)
|
|
226
|
+
return undefined;
|
|
227
|
+
const wrap = writeShellEnvInjector(wrapperBody);
|
|
228
|
+
remember(activeEnvWraps, toolCallId, wrap, (evicted) => evicted.cleanup());
|
|
229
|
+
pendingEnvWraps.delete(toolCallId);
|
|
230
|
+
return wrap.env;
|
|
231
|
+
}
|
|
90
232
|
function withoutMarker(output, index) {
|
|
91
233
|
let clean = output.slice(0, index).replace(/[\t ]+$/gm, "").replace(/\n{2,}$/, "\n");
|
|
92
234
|
if (clean.trim() === "" || clean.trim() === "(no output)")
|
|
93
235
|
clean = "";
|
|
94
236
|
return clean;
|
|
95
237
|
}
|
|
238
|
+
/**
|
|
239
|
+
* OpenCode only stores/renders text (`output` / `metadata.output`). Private
|
|
240
|
+
* markers become typed Cursor outcomes, but stripping them alone can leave a
|
|
241
|
+
* blank or partial bash bubble that looks like success. Append a short
|
|
242
|
+
* user-facing status so the UI explains background handoff / timeout.
|
|
243
|
+
*/
|
|
244
|
+
function formatShellOutcomeDisplay(clean, outcome) {
|
|
245
|
+
let notice;
|
|
246
|
+
if (outcome.kind === "backgrounded") {
|
|
247
|
+
notice = outcome.msToWait > 0
|
|
248
|
+
? `Still running in the background (pid ${outcome.pid}) after ${outcome.msToWait}ms.`
|
|
249
|
+
: `Started in the background (pid ${outcome.pid}).`;
|
|
250
|
+
}
|
|
251
|
+
else if (outcome.kind === "timeout") {
|
|
252
|
+
notice = `Timed out after ${outcome.timeoutMs}ms.`;
|
|
253
|
+
}
|
|
254
|
+
if (!notice)
|
|
255
|
+
return clean;
|
|
256
|
+
if (!clean)
|
|
257
|
+
return `${notice}\n`;
|
|
258
|
+
return clean.endsWith("\n") ? `${clean}${notice}\n` : `${clean}\n${notice}\n`;
|
|
259
|
+
}
|
|
260
|
+
/**
|
|
261
|
+
* Find the last private wrapper sentinel.
|
|
262
|
+
*
|
|
263
|
+
* Soft-background wrappers print the marker as the final intentional line, but
|
|
264
|
+
* the host shell can still append job-control diagnostics afterwards (e.g.
|
|
265
|
+
* "Terminated: 15 … nohup sh -c '…cursor-shell-watchdog…'"). Match the sentinel
|
|
266
|
+
* on its own line and discard everything from that point to EOF.
|
|
267
|
+
*/
|
|
268
|
+
function lastPrivateMarker(output, marker, valuePattern) {
|
|
269
|
+
const re = new RegExp(`(?:^|\\r?\\n)(${marker}${valuePattern})`, "g");
|
|
270
|
+
let match;
|
|
271
|
+
let last;
|
|
272
|
+
while ((match = re.exec(output)) !== null) {
|
|
273
|
+
if (match.index === undefined || match[1] === undefined)
|
|
274
|
+
continue;
|
|
275
|
+
const index = match[0].startsWith("\r\n")
|
|
276
|
+
? match.index + 2
|
|
277
|
+
: match[0].startsWith("\n")
|
|
278
|
+
? match.index + 1
|
|
279
|
+
: match.index;
|
|
280
|
+
last = { index, values: match.slice(2) };
|
|
281
|
+
}
|
|
282
|
+
return last;
|
|
283
|
+
}
|
|
96
284
|
function parseOpenCodeTimeout(output) {
|
|
97
285
|
const re = /<shell_metadata>\r?\nshell tool terminated command after exceeding timeout (\d+) ms\.[\s\S]*?<\/shell_metadata>\s*$/;
|
|
98
286
|
const match = re.exec(output);
|
|
@@ -100,10 +288,10 @@ function parseOpenCodeTimeout(output) {
|
|
|
100
288
|
return undefined;
|
|
101
289
|
return { output: withoutMarker(output, match.index), timeoutMs: Number(match[1]) };
|
|
102
290
|
}
|
|
103
|
-
function
|
|
104
|
-
const background =
|
|
105
|
-
if (background
|
|
106
|
-
const pid = Number(background[
|
|
291
|
+
function parseSoftBackgroundOutcome(output, policy) {
|
|
292
|
+
const background = lastPrivateMarker(output, BACKGROUND_MARKER, "(\\d+):([^\\r\\n]+)");
|
|
293
|
+
if (background) {
|
|
294
|
+
const pid = Number(background.values[0]);
|
|
107
295
|
if (Number.isSafeInteger(pid) && pid > 0 && pid <= 0xffff_ffff) {
|
|
108
296
|
return {
|
|
109
297
|
output: withoutMarker(output, background.index),
|
|
@@ -119,43 +307,101 @@ function parseWrapperOutcome(output, policy) {
|
|
|
119
307
|
};
|
|
120
308
|
}
|
|
121
309
|
}
|
|
122
|
-
const timeout =
|
|
123
|
-
if (timeout
|
|
310
|
+
const timeout = lastPrivateMarker(output, TIMEOUT_MARKER, "(\\d+)");
|
|
311
|
+
if (timeout) {
|
|
124
312
|
return {
|
|
125
313
|
output: withoutMarker(output, timeout.index),
|
|
126
|
-
outcome: { kind: "timeout", timeoutMs: Number(timeout[
|
|
314
|
+
outcome: { kind: "timeout", timeoutMs: Number(timeout.values[0]) },
|
|
127
315
|
};
|
|
128
316
|
}
|
|
129
|
-
const exit =
|
|
130
|
-
if (exit
|
|
317
|
+
const exit = lastPrivateMarker(output, EXIT_MARKER, "(-?\\d+)");
|
|
318
|
+
if (exit) {
|
|
131
319
|
return {
|
|
132
320
|
output: withoutMarker(output, exit.index),
|
|
133
|
-
outcome: { kind: "exit", code: Number(exit[
|
|
321
|
+
outcome: { kind: "exit", code: Number(exit.values[0]) },
|
|
134
322
|
};
|
|
135
323
|
}
|
|
136
324
|
return undefined;
|
|
137
325
|
}
|
|
326
|
+
function parseBackgroundSpawnOutcome(output, policy) {
|
|
327
|
+
const match = lastPrivateMarker(output, BACKGROUND_SHELL_MARKER, "(\\d+):([^\\r\\n]+)");
|
|
328
|
+
if (!match)
|
|
329
|
+
return undefined;
|
|
330
|
+
const pid = Number(match.values[0]);
|
|
331
|
+
if (!Number.isSafeInteger(pid) || pid <= 0 || pid > 0xffff_ffff)
|
|
332
|
+
return undefined;
|
|
333
|
+
return {
|
|
334
|
+
output: withoutMarker(output, match.index),
|
|
335
|
+
outcome: {
|
|
336
|
+
kind: "backgrounded",
|
|
337
|
+
shellId: pid,
|
|
338
|
+
pid,
|
|
339
|
+
command: policy?.command ?? "",
|
|
340
|
+
workingDirectory: policy?.workingDirectory ?? "",
|
|
341
|
+
msToWait: 0,
|
|
342
|
+
reason: 1,
|
|
343
|
+
},
|
|
344
|
+
};
|
|
345
|
+
}
|
|
346
|
+
/**
|
|
347
|
+
* Strip private wrapper sentinels / OpenCode timeout envelopes for display.
|
|
348
|
+
* Does not record outcomes — use {@link captureCursorShellResult} for that.
|
|
349
|
+
*/
|
|
350
|
+
export function sanitizeCursorShellDisplayOutput(output, policy) {
|
|
351
|
+
if (policy?.backgroundSpawn) {
|
|
352
|
+
const spawn = parseBackgroundSpawnOutcome(output, policy);
|
|
353
|
+
if (spawn)
|
|
354
|
+
return formatShellOutcomeDisplay(spawn.output, spawn.outcome);
|
|
355
|
+
}
|
|
356
|
+
if (policy?.timeoutBehavior === CURSOR_TIMEOUT_BACKGROUND) {
|
|
357
|
+
const wrapper = parseSoftBackgroundOutcome(output, policy);
|
|
358
|
+
if (wrapper)
|
|
359
|
+
return formatShellOutcomeDisplay(wrapper.output, wrapper.outcome);
|
|
360
|
+
}
|
|
361
|
+
const timeout = parseOpenCodeTimeout(output);
|
|
362
|
+
if (timeout) {
|
|
363
|
+
return formatShellOutcomeDisplay(timeout.output, {
|
|
364
|
+
kind: "timeout",
|
|
365
|
+
timeoutMs: timeout.timeoutMs,
|
|
366
|
+
});
|
|
367
|
+
}
|
|
368
|
+
return output;
|
|
369
|
+
}
|
|
370
|
+
/** Sanitize a secondary display string (e.g. Bash `metadata.output`) for a registered call. */
|
|
371
|
+
export function sanitizeRegisteredCursorShellOutput(toolCallId, output) {
|
|
372
|
+
if (typeof toolCallId !== "string" || !toolCallId)
|
|
373
|
+
return output;
|
|
374
|
+
return sanitizeCursorShellDisplayOutput(output, policies.get(toolCallId));
|
|
375
|
+
}
|
|
138
376
|
/**
|
|
139
377
|
* Capture Bash completion in the classic plugin's after hook. Returns the
|
|
140
378
|
* sanitized output that OpenCode should store and render.
|
|
141
379
|
*/
|
|
142
380
|
export function captureCursorShellResult(toolCallId, output, metadata) {
|
|
143
|
-
if (!toolCallId.startsWith("cursor_"))
|
|
381
|
+
if (typeof toolCallId !== "string" || !toolCallId.startsWith("cursor_"))
|
|
144
382
|
return output;
|
|
145
383
|
const policy = policies.get(toolCallId);
|
|
384
|
+
if (policy?.backgroundSpawn) {
|
|
385
|
+
const spawn = parseBackgroundSpawnOutcome(output, policy);
|
|
386
|
+
if (spawn) {
|
|
387
|
+
remember(outcomes, toolCallId, spawn.outcome);
|
|
388
|
+
return formatShellOutcomeDisplay(spawn.output, spawn.outcome);
|
|
389
|
+
}
|
|
390
|
+
}
|
|
146
391
|
// Private wrapper sentinels are meaningful only for calls we transformed.
|
|
147
392
|
// A normal foreground command is allowed to print the same text verbatim.
|
|
148
393
|
const wrapper = policy?.timeoutBehavior === CURSOR_TIMEOUT_BACKGROUND
|
|
149
|
-
?
|
|
394
|
+
? parseSoftBackgroundOutcome(output, policy)
|
|
150
395
|
: undefined;
|
|
151
396
|
if (wrapper) {
|
|
152
397
|
remember(outcomes, toolCallId, wrapper.outcome);
|
|
153
|
-
return wrapper.output;
|
|
398
|
+
return formatShellOutcomeDisplay(wrapper.output, wrapper.outcome);
|
|
154
399
|
}
|
|
155
400
|
const timeout = parseOpenCodeTimeout(output);
|
|
156
401
|
if (timeout) {
|
|
157
|
-
|
|
158
|
-
|
|
402
|
+
const outcome = { kind: "timeout", timeoutMs: timeout.timeoutMs };
|
|
403
|
+
remember(outcomes, toolCallId, outcome);
|
|
404
|
+
return formatShellOutcomeDisplay(timeout.output, outcome);
|
|
159
405
|
}
|
|
160
406
|
const exitCode = finiteNonNegative(metadata?.exit);
|
|
161
407
|
if (exitCode !== undefined)
|
|
@@ -164,16 +410,24 @@ export function captureCursorShellResult(toolCallId, output, metadata) {
|
|
|
164
410
|
}
|
|
165
411
|
/** Consume the structured result, with an inline fallback when no plugin hook ran. */
|
|
166
412
|
export function consumeCursorShellResult(toolCallId, output) {
|
|
413
|
+
if (typeof toolCallId !== "string" || !toolCallId) {
|
|
414
|
+
return { output };
|
|
415
|
+
}
|
|
167
416
|
let clean = output;
|
|
168
417
|
if (!outcomes.has(toolCallId))
|
|
169
418
|
clean = captureCursorShellResult(toolCallId, output);
|
|
170
419
|
const outcome = outcomes.get(toolCallId);
|
|
171
420
|
outcomes.delete(toolCallId);
|
|
172
421
|
policies.delete(toolCallId);
|
|
422
|
+
releaseCursorShellEnv(toolCallId);
|
|
173
423
|
return { output: clean, outcome };
|
|
174
424
|
}
|
|
175
425
|
/** Test/process cleanup. */
|
|
176
426
|
export function resetCursorShellCalls() {
|
|
427
|
+
for (const wrap of activeEnvWraps.values())
|
|
428
|
+
wrap.cleanup();
|
|
429
|
+
activeEnvWraps.clear();
|
|
430
|
+
pendingEnvWraps.clear();
|
|
177
431
|
policies.clear();
|
|
178
432
|
outcomes.clear();
|
|
179
433
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "cursor-opencode-provider",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.6",
|
|
4
4
|
"description": "Use Cursor subscription models from OpenCode via Cursor's Connect-RPC agent protocol",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -45,6 +45,7 @@
|
|
|
45
45
|
"dist",
|
|
46
46
|
"LICENSE",
|
|
47
47
|
"DISCLAIMER.md",
|
|
48
|
+
"SECURITY.md",
|
|
48
49
|
"README.md"
|
|
49
50
|
],
|
|
50
51
|
"scripts": {
|
|
@@ -68,4 +69,4 @@
|
|
|
68
69
|
"@types/node": "^22.15.3",
|
|
69
70
|
"typescript": "^5.8.3"
|
|
70
71
|
}
|
|
71
|
-
}
|
|
72
|
+
}
|