cursor-opencode-provider 0.2.5 → 0.2.7
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 +27 -15
- package/dist/context/build.js +6 -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 +41 -3
- package/dist/context/paths.js +121 -9
- package/dist/debug.d.ts +7 -0
- package/dist/debug.js +42 -0
- package/dist/index.d.ts +6 -0
- package/dist/language-model.d.ts +11 -1
- package/dist/language-model.js +47 -22
- package/dist/plugin-v2.js +4 -0
- package/dist/plugin.js +30 -4
- package/dist/protocol/messages.js +5 -1
- package/dist/protocol/request.d.ts +2 -0
- package/dist/protocol/request.js +5 -5
- package/dist/protocol/tools.d.ts +2 -2
- package/dist/protocol/tools.js +35 -38
- package/dist/session.d.ts +2 -0
- package/dist/shell-timeout.d.ts +48 -1
- package/dist/shell-timeout.js +308 -24
- package/package.json +9 -3
package/dist/shell-timeout.d.ts
CHANGED
|
@@ -1,12 +1,16 @@
|
|
|
1
1
|
/** Cursor agent.v1 TimeoutBehavior enum values. */
|
|
2
2
|
export declare const CURSOR_TIMEOUT_CANCEL = 1;
|
|
3
3
|
export declare const CURSOR_TIMEOUT_BACKGROUND = 2;
|
|
4
|
+
/** Private marker for Cursor `background_shell_spawn_args` detach wrappers. */
|
|
5
|
+
export declare const BACKGROUND_SHELL_MARKER = "__CURSOR_BACKGROUND_SHELL__";
|
|
4
6
|
export type CursorShellPolicy = {
|
|
5
7
|
command: string;
|
|
6
8
|
workingDirectory: string;
|
|
7
9
|
timeoutMs: number;
|
|
8
10
|
timeoutBehavior: number;
|
|
9
11
|
hardTimeoutMs?: number;
|
|
12
|
+
/** Immediate nohup detach for Cursor `background_shell_spawn_args`. */
|
|
13
|
+
backgroundSpawn?: boolean;
|
|
10
14
|
};
|
|
11
15
|
export type CursorShellOutcome = {
|
|
12
16
|
kind: "exit";
|
|
@@ -23,6 +27,13 @@ export type CursorShellOutcome = {
|
|
|
23
27
|
msToWait: number;
|
|
24
28
|
reason: 1;
|
|
25
29
|
};
|
|
30
|
+
/** Track OpenCode's configured shell from the classic config hook. */
|
|
31
|
+
export declare function setCursorShellPath(shell: string | undefined): void;
|
|
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 declare function resolveCursorShellKind(shell?: string | undefined): "bash" | "zsh" | "sh" | "dash" | "other";
|
|
26
37
|
export declare function shellPolicyFromMetadata(metadata: Record<string, unknown> | undefined): CursorShellPolicy | undefined;
|
|
27
38
|
/** Register a Cursor shell request before OpenCode executes its emitted tool call. */
|
|
28
39
|
export declare function registerCursorShellCall(toolCallId: string, metadata: Record<string, unknown> | undefined): void;
|
|
@@ -39,10 +50,46 @@ export declare function registerCursorShellCall(toolCallId: string, metadata: Re
|
|
|
39
50
|
* reap leftover processes — cleanup is left to the user / OS.
|
|
40
51
|
*/
|
|
41
52
|
export declare function buildSoftBackgroundCommand(policy: CursorShellPolicy): string;
|
|
42
|
-
/**
|
|
53
|
+
/**
|
|
54
|
+
* F11 / background_shell_spawn_args helper.
|
|
55
|
+
*
|
|
56
|
+
* OpenCode's bash tool is foreground-only. Detach the requested command inside
|
|
57
|
+
* that one foreground call (`nohup … &`) and print a private marker containing
|
|
58
|
+
* the spawned PID and log path. With stdin and all output redirected, the host
|
|
59
|
+
* shell can return immediately instead of retaining OpenCode's tool pipe.
|
|
60
|
+
*
|
|
61
|
+
* Residual: the detached child is not reaped by this provider after OpenCode
|
|
62
|
+
* completes the tool call; cleanup is left to the user / OS.
|
|
63
|
+
*/
|
|
64
|
+
export declare function buildBackgroundShellCommand(command: string): string;
|
|
65
|
+
/**
|
|
66
|
+
* Prepare OpenCode Bash args before execution when Cursor requested wrapping.
|
|
67
|
+
*
|
|
68
|
+
* bash/zsh source the shell.env injector, so the original command remains in
|
|
69
|
+
* OpenCode's permission/UI state. sh/dash ignore those startup variables; for
|
|
70
|
+
* them, use a short `exec wrapper.sh` command that contains no user payload.
|
|
71
|
+
*
|
|
72
|
+
* background_shell_spawn may already contain the inline non-plugin fallback.
|
|
73
|
+
* The classic hook replaces it with the original command (bash/zsh) or the
|
|
74
|
+
* shorter wrapper-file command (sh/dash), avoiding duplicate execution.
|
|
75
|
+
*/
|
|
43
76
|
export declare function prepareCursorShellArgs(toolCallId: string, args: Record<string, unknown>): void;
|
|
44
77
|
/** Restore the model-facing command in OpenCode's completed tool title. */
|
|
45
78
|
export declare function cursorShellOriginalCommand(toolCallId: string): string | undefined;
|
|
79
|
+
/** Drop injector temp files for a finished/abandoned Cursor shell call. */
|
|
80
|
+
export declare function releaseCursorShellEnv(toolCallId: string): void;
|
|
81
|
+
/**
|
|
82
|
+
* Env vars for OpenCode's shell.env hook. bash/zsh execute the injector; the
|
|
83
|
+
* same materialized wrapper backs the direct-command sh/dash fallback.
|
|
84
|
+
*/
|
|
85
|
+
export declare function cursorShellEnvForCall(toolCallId: string | undefined): Record<string, string> | undefined;
|
|
86
|
+
/**
|
|
87
|
+
* Strip private wrapper sentinels / OpenCode timeout envelopes for display.
|
|
88
|
+
* Does not record outcomes — use {@link captureCursorShellResult} for that.
|
|
89
|
+
*/
|
|
90
|
+
export declare function sanitizeCursorShellDisplayOutput(output: string, policy?: CursorShellPolicy): string;
|
|
91
|
+
/** Sanitize a secondary display string (e.g. Bash `metadata.output`) for a registered call. */
|
|
92
|
+
export declare function sanitizeRegisteredCursorShellOutput(toolCallId: string, output: string): string;
|
|
46
93
|
/**
|
|
47
94
|
* Capture Bash completion in the classic plugin's after hook. Returns the
|
|
48
95
|
* sanitized output that OpenCode should store and render.
|
package/dist/shell-timeout.js
CHANGED
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
import { existsSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { tmpdir } from "node:os";
|
|
3
|
+
import { basename, delimiter, 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,49 @@ 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 or a direct-command fallback. */
|
|
18
|
+
const pendingEnvWraps = new Set();
|
|
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
|
+
}
|
|
45
|
+
function remember(map, key, value, onEvict) {
|
|
13
46
|
map.delete(key);
|
|
14
47
|
map.set(key, value);
|
|
15
48
|
while (map.size > MAX_TRACKED_SHELL_CALLS) {
|
|
16
49
|
const oldest = map.keys().next().value;
|
|
17
50
|
if (!oldest)
|
|
18
51
|
break;
|
|
52
|
+
const evicted = map.get(oldest);
|
|
19
53
|
map.delete(oldest);
|
|
54
|
+
if (evicted !== undefined && onEvict)
|
|
55
|
+
onEvict(evicted);
|
|
20
56
|
}
|
|
21
57
|
}
|
|
22
58
|
function finiteNonNegative(value) {
|
|
@@ -26,7 +62,18 @@ function finiteNonNegative(value) {
|
|
|
26
62
|
return Math.floor(n);
|
|
27
63
|
}
|
|
28
64
|
export function shellPolicyFromMetadata(metadata) {
|
|
29
|
-
if (!metadata
|
|
65
|
+
if (!metadata)
|
|
66
|
+
return undefined;
|
|
67
|
+
if (metadata.background_shell_spawn === true) {
|
|
68
|
+
return {
|
|
69
|
+
command: typeof metadata.command === "string" ? metadata.command : "",
|
|
70
|
+
workingDirectory: typeof metadata.working_directory === "string" ? metadata.working_directory : "",
|
|
71
|
+
timeoutMs: 0,
|
|
72
|
+
timeoutBehavior: 0,
|
|
73
|
+
backgroundSpawn: true,
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
if (metadata.shell_stream !== true)
|
|
30
77
|
return undefined;
|
|
31
78
|
const timeoutMs = finiteNonNegative(metadata.timeout_ms) ?? 30_000;
|
|
32
79
|
const timeoutBehavior = finiteNonNegative(metadata.timeout_behavior) ?? 0;
|
|
@@ -77,29 +124,199 @@ export function buildSoftBackgroundCommand(policy) {
|
|
|
77
124
|
else {
|
|
78
125
|
lines.push('cursor_shell_status=""', 'cursor_shell_watchdog_pid=""');
|
|
79
126
|
}
|
|
80
|
-
lines.push(
|
|
127
|
+
lines.push(
|
|
128
|
+
// Avoid interactive job-control noise ("Terminated: 15 …") when we later
|
|
129
|
+
// reap the watchdog; that text can otherwise land after our private marker
|
|
130
|
+
// and leak into OpenCode's bash UI.
|
|
131
|
+
"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',
|
|
132
|
+
// Reap the watchdog before printing the private marker so any residual
|
|
133
|
+
// shell diagnostics cannot trail the sentinel.
|
|
134
|
+
' 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');
|
|
81
135
|
return lines.join("\n");
|
|
82
136
|
}
|
|
83
|
-
/**
|
|
137
|
+
/**
|
|
138
|
+
* F11 / background_shell_spawn_args helper.
|
|
139
|
+
*
|
|
140
|
+
* OpenCode's bash tool is foreground-only. Detach the requested command inside
|
|
141
|
+
* that one foreground call (`nohup … &`) and print a private marker containing
|
|
142
|
+
* the spawned PID and log path. With stdin and all output redirected, the host
|
|
143
|
+
* shell can return immediately instead of retaining OpenCode's tool pipe.
|
|
144
|
+
*
|
|
145
|
+
* Residual: the detached child is not reaped by this provider after OpenCode
|
|
146
|
+
* completes the tool call; cleanup is left to the user / OS.
|
|
147
|
+
*/
|
|
148
|
+
export function buildBackgroundShellCommand(command) {
|
|
149
|
+
return [
|
|
150
|
+
'bg_log="$(mktemp "${TMPDIR:-/tmp}/cursor-opencode-bg.XXXXXX")" || exit 1',
|
|
151
|
+
`nohup sh -c ${shellQuote(command)} >"$bg_log" 2>&1 </dev/null &`,
|
|
152
|
+
"bg_pid=$!",
|
|
153
|
+
`printf '${BACKGROUND_SHELL_MARKER}%s:%s\\n' "$bg_pid" "$bg_log"`,
|
|
154
|
+
].join("\n");
|
|
155
|
+
}
|
|
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
|
+
}
|
|
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 bash (BASH_ENV) or zsh (.zshenv via ZDOTDIR). `exec`
|
|
170
|
+
// replaces the host shell before OpenCode's `-c <original>` body runs.
|
|
171
|
+
const injector = [
|
|
172
|
+
"unset BASH_ENV ZDOTDIR ENV CURSOR_OPENCODE_WRAP_ACTIVE",
|
|
173
|
+
`exec /bin/sh ${shellQuote(wrapperPath)}`,
|
|
174
|
+
"",
|
|
175
|
+
].join("\n");
|
|
176
|
+
writeFileSync(bashEnvPath, injector, { mode: 0o600 });
|
|
177
|
+
writeFileSync(zshenvPath, injector, { mode: 0o600 });
|
|
178
|
+
return {
|
|
179
|
+
wrapperPath,
|
|
180
|
+
env: {
|
|
181
|
+
BASH_ENV: bashEnvPath,
|
|
182
|
+
ZDOTDIR: dir,
|
|
183
|
+
},
|
|
184
|
+
cleanup: () => {
|
|
185
|
+
try {
|
|
186
|
+
rmSync(dir, { recursive: true, force: true });
|
|
187
|
+
}
|
|
188
|
+
catch {
|
|
189
|
+
// best-effort temp cleanup
|
|
190
|
+
}
|
|
191
|
+
},
|
|
192
|
+
};
|
|
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
|
+
*/
|
|
84
216
|
export function prepareCursorShellArgs(toolCallId, args) {
|
|
85
217
|
const policy = policies.get(toolCallId);
|
|
86
|
-
if (!policy
|
|
218
|
+
if (!policy)
|
|
87
219
|
return;
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
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)}`;
|
|
92
238
|
}
|
|
93
239
|
/** Restore the model-facing command in OpenCode's completed tool title. */
|
|
94
240
|
export function cursorShellOriginalCommand(toolCallId) {
|
|
95
241
|
return policies.get(toolCallId)?.command || undefined;
|
|
96
242
|
}
|
|
243
|
+
/** Drop injector temp files for a finished/abandoned Cursor shell call. */
|
|
244
|
+
export function releaseCursorShellEnv(toolCallId) {
|
|
245
|
+
pendingEnvWraps.delete(toolCallId);
|
|
246
|
+
const active = activeEnvWraps.get(toolCallId);
|
|
247
|
+
if (!active)
|
|
248
|
+
return;
|
|
249
|
+
activeEnvWraps.delete(toolCallId);
|
|
250
|
+
active.cleanup();
|
|
251
|
+
}
|
|
252
|
+
/**
|
|
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.
|
|
255
|
+
*/
|
|
256
|
+
export function cursorShellEnvForCall(toolCallId) {
|
|
257
|
+
if (typeof toolCallId !== "string" || !toolCallId || !pendingEnvWraps.has(toolCallId))
|
|
258
|
+
return undefined;
|
|
259
|
+
const policy = policies.get(toolCallId);
|
|
260
|
+
if (!policy)
|
|
261
|
+
return undefined;
|
|
262
|
+
const wrap = ensureShellEnvWrap(toolCallId, policy);
|
|
263
|
+
if (!wrap)
|
|
264
|
+
return undefined;
|
|
265
|
+
pendingEnvWraps.delete(toolCallId);
|
|
266
|
+
return wrap.env;
|
|
267
|
+
}
|
|
97
268
|
function withoutMarker(output, index) {
|
|
98
269
|
let clean = output.slice(0, index).replace(/[\t ]+$/gm, "").replace(/\n{2,}$/, "\n");
|
|
99
270
|
if (clean.trim() === "" || clean.trim() === "(no output)")
|
|
100
271
|
clean = "";
|
|
101
272
|
return clean;
|
|
102
273
|
}
|
|
274
|
+
/**
|
|
275
|
+
* OpenCode only stores/renders text (`output` / `metadata.output`). Private
|
|
276
|
+
* markers become typed Cursor outcomes, but stripping them alone can leave a
|
|
277
|
+
* blank or partial bash bubble that looks like success. Append a short
|
|
278
|
+
* user-facing status so the UI explains background handoff / timeout.
|
|
279
|
+
*/
|
|
280
|
+
function formatShellOutcomeDisplay(clean, outcome) {
|
|
281
|
+
let notice;
|
|
282
|
+
if (outcome.kind === "backgrounded") {
|
|
283
|
+
notice = outcome.msToWait > 0
|
|
284
|
+
? `Still running in the background (pid ${outcome.pid}) after ${outcome.msToWait}ms.`
|
|
285
|
+
: `Started in the background (pid ${outcome.pid}).`;
|
|
286
|
+
}
|
|
287
|
+
else if (outcome.kind === "timeout") {
|
|
288
|
+
notice = `Timed out after ${outcome.timeoutMs}ms.`;
|
|
289
|
+
}
|
|
290
|
+
if (!notice)
|
|
291
|
+
return clean;
|
|
292
|
+
if (!clean)
|
|
293
|
+
return `${notice}\n`;
|
|
294
|
+
return clean.endsWith("\n") ? `${clean}${notice}\n` : `${clean}\n${notice}\n`;
|
|
295
|
+
}
|
|
296
|
+
/**
|
|
297
|
+
* Find the last private wrapper sentinel.
|
|
298
|
+
*
|
|
299
|
+
* Soft-background wrappers print the marker as the final intentional line, but
|
|
300
|
+
* the host shell can still append job-control diagnostics afterwards (e.g.
|
|
301
|
+
* "Terminated: 15 … nohup sh -c '…cursor-shell-watchdog…'"). Match the sentinel
|
|
302
|
+
* on its own line and discard everything from that point to EOF.
|
|
303
|
+
*/
|
|
304
|
+
function lastPrivateMarker(output, marker, valuePattern) {
|
|
305
|
+
const re = new RegExp(`(?:^|\\r?\\n)(${marker}${valuePattern})`, "g");
|
|
306
|
+
let match;
|
|
307
|
+
let last;
|
|
308
|
+
while ((match = re.exec(output)) !== null) {
|
|
309
|
+
if (match.index === undefined || match[1] === undefined)
|
|
310
|
+
continue;
|
|
311
|
+
const index = match[0].startsWith("\r\n")
|
|
312
|
+
? match.index + 2
|
|
313
|
+
: match[0].startsWith("\n")
|
|
314
|
+
? match.index + 1
|
|
315
|
+
: match.index;
|
|
316
|
+
last = { index, values: match.slice(2) };
|
|
317
|
+
}
|
|
318
|
+
return last;
|
|
319
|
+
}
|
|
103
320
|
function parseOpenCodeTimeout(output) {
|
|
104
321
|
const re = /<shell_metadata>\r?\nshell tool terminated command after exceeding timeout (\d+) ms\.[\s\S]*?<\/shell_metadata>\s*$/;
|
|
105
322
|
const match = re.exec(output);
|
|
@@ -107,10 +324,10 @@ function parseOpenCodeTimeout(output) {
|
|
|
107
324
|
return undefined;
|
|
108
325
|
return { output: withoutMarker(output, match.index), timeoutMs: Number(match[1]) };
|
|
109
326
|
}
|
|
110
|
-
function
|
|
111
|
-
const background =
|
|
112
|
-
if (background
|
|
113
|
-
const pid = Number(background[
|
|
327
|
+
function parseSoftBackgroundOutcome(output, policy) {
|
|
328
|
+
const background = lastPrivateMarker(output, BACKGROUND_MARKER, "(\\d+):([^\\r\\n]+)");
|
|
329
|
+
if (background) {
|
|
330
|
+
const pid = Number(background.values[0]);
|
|
114
331
|
if (Number.isSafeInteger(pid) && pid > 0 && pid <= 0xffff_ffff) {
|
|
115
332
|
return {
|
|
116
333
|
output: withoutMarker(output, background.index),
|
|
@@ -126,43 +343,101 @@ function parseWrapperOutcome(output, policy) {
|
|
|
126
343
|
};
|
|
127
344
|
}
|
|
128
345
|
}
|
|
129
|
-
const timeout =
|
|
130
|
-
if (timeout
|
|
346
|
+
const timeout = lastPrivateMarker(output, TIMEOUT_MARKER, "(\\d+)");
|
|
347
|
+
if (timeout) {
|
|
131
348
|
return {
|
|
132
349
|
output: withoutMarker(output, timeout.index),
|
|
133
|
-
outcome: { kind: "timeout", timeoutMs: Number(timeout[
|
|
350
|
+
outcome: { kind: "timeout", timeoutMs: Number(timeout.values[0]) },
|
|
134
351
|
};
|
|
135
352
|
}
|
|
136
|
-
const exit =
|
|
137
|
-
if (exit
|
|
353
|
+
const exit = lastPrivateMarker(output, EXIT_MARKER, "(-?\\d+)");
|
|
354
|
+
if (exit) {
|
|
138
355
|
return {
|
|
139
356
|
output: withoutMarker(output, exit.index),
|
|
140
|
-
outcome: { kind: "exit", code: Number(exit[
|
|
357
|
+
outcome: { kind: "exit", code: Number(exit.values[0]) },
|
|
141
358
|
};
|
|
142
359
|
}
|
|
143
360
|
return undefined;
|
|
144
361
|
}
|
|
362
|
+
function parseBackgroundSpawnOutcome(output, policy) {
|
|
363
|
+
const match = lastPrivateMarker(output, BACKGROUND_SHELL_MARKER, "(\\d+):([^\\r\\n]+)");
|
|
364
|
+
if (!match)
|
|
365
|
+
return undefined;
|
|
366
|
+
const pid = Number(match.values[0]);
|
|
367
|
+
if (!Number.isSafeInteger(pid) || pid <= 0 || pid > 0xffff_ffff)
|
|
368
|
+
return undefined;
|
|
369
|
+
return {
|
|
370
|
+
output: withoutMarker(output, match.index),
|
|
371
|
+
outcome: {
|
|
372
|
+
kind: "backgrounded",
|
|
373
|
+
shellId: pid,
|
|
374
|
+
pid,
|
|
375
|
+
command: policy?.command ?? "",
|
|
376
|
+
workingDirectory: policy?.workingDirectory ?? "",
|
|
377
|
+
msToWait: 0,
|
|
378
|
+
reason: 1,
|
|
379
|
+
},
|
|
380
|
+
};
|
|
381
|
+
}
|
|
382
|
+
/**
|
|
383
|
+
* Strip private wrapper sentinels / OpenCode timeout envelopes for display.
|
|
384
|
+
* Does not record outcomes — use {@link captureCursorShellResult} for that.
|
|
385
|
+
*/
|
|
386
|
+
export function sanitizeCursorShellDisplayOutput(output, policy) {
|
|
387
|
+
if (policy?.backgroundSpawn) {
|
|
388
|
+
const spawn = parseBackgroundSpawnOutcome(output, policy);
|
|
389
|
+
if (spawn)
|
|
390
|
+
return formatShellOutcomeDisplay(spawn.output, spawn.outcome);
|
|
391
|
+
}
|
|
392
|
+
if (policy?.timeoutBehavior === CURSOR_TIMEOUT_BACKGROUND) {
|
|
393
|
+
const wrapper = parseSoftBackgroundOutcome(output, policy);
|
|
394
|
+
if (wrapper)
|
|
395
|
+
return formatShellOutcomeDisplay(wrapper.output, wrapper.outcome);
|
|
396
|
+
}
|
|
397
|
+
const timeout = parseOpenCodeTimeout(output);
|
|
398
|
+
if (timeout) {
|
|
399
|
+
return formatShellOutcomeDisplay(timeout.output, {
|
|
400
|
+
kind: "timeout",
|
|
401
|
+
timeoutMs: timeout.timeoutMs,
|
|
402
|
+
});
|
|
403
|
+
}
|
|
404
|
+
return output;
|
|
405
|
+
}
|
|
406
|
+
/** Sanitize a secondary display string (e.g. Bash `metadata.output`) for a registered call. */
|
|
407
|
+
export function sanitizeRegisteredCursorShellOutput(toolCallId, output) {
|
|
408
|
+
if (typeof toolCallId !== "string" || !toolCallId)
|
|
409
|
+
return output;
|
|
410
|
+
return sanitizeCursorShellDisplayOutput(output, policies.get(toolCallId));
|
|
411
|
+
}
|
|
145
412
|
/**
|
|
146
413
|
* Capture Bash completion in the classic plugin's after hook. Returns the
|
|
147
414
|
* sanitized output that OpenCode should store and render.
|
|
148
415
|
*/
|
|
149
416
|
export function captureCursorShellResult(toolCallId, output, metadata) {
|
|
150
|
-
if (!toolCallId.startsWith("cursor_"))
|
|
417
|
+
if (typeof toolCallId !== "string" || !toolCallId.startsWith("cursor_"))
|
|
151
418
|
return output;
|
|
152
419
|
const policy = policies.get(toolCallId);
|
|
420
|
+
if (policy?.backgroundSpawn) {
|
|
421
|
+
const spawn = parseBackgroundSpawnOutcome(output, policy);
|
|
422
|
+
if (spawn) {
|
|
423
|
+
remember(outcomes, toolCallId, spawn.outcome);
|
|
424
|
+
return formatShellOutcomeDisplay(spawn.output, spawn.outcome);
|
|
425
|
+
}
|
|
426
|
+
}
|
|
153
427
|
// Private wrapper sentinels are meaningful only for calls we transformed.
|
|
154
428
|
// A normal foreground command is allowed to print the same text verbatim.
|
|
155
429
|
const wrapper = policy?.timeoutBehavior === CURSOR_TIMEOUT_BACKGROUND
|
|
156
|
-
?
|
|
430
|
+
? parseSoftBackgroundOutcome(output, policy)
|
|
157
431
|
: undefined;
|
|
158
432
|
if (wrapper) {
|
|
159
433
|
remember(outcomes, toolCallId, wrapper.outcome);
|
|
160
|
-
return wrapper.output;
|
|
434
|
+
return formatShellOutcomeDisplay(wrapper.output, wrapper.outcome);
|
|
161
435
|
}
|
|
162
436
|
const timeout = parseOpenCodeTimeout(output);
|
|
163
437
|
if (timeout) {
|
|
164
|
-
|
|
165
|
-
|
|
438
|
+
const outcome = { kind: "timeout", timeoutMs: timeout.timeoutMs };
|
|
439
|
+
remember(outcomes, toolCallId, outcome);
|
|
440
|
+
return formatShellOutcomeDisplay(timeout.output, outcome);
|
|
166
441
|
}
|
|
167
442
|
const exitCode = finiteNonNegative(metadata?.exit);
|
|
168
443
|
if (exitCode !== undefined)
|
|
@@ -171,16 +446,25 @@ export function captureCursorShellResult(toolCallId, output, metadata) {
|
|
|
171
446
|
}
|
|
172
447
|
/** Consume the structured result, with an inline fallback when no plugin hook ran. */
|
|
173
448
|
export function consumeCursorShellResult(toolCallId, output) {
|
|
449
|
+
if (typeof toolCallId !== "string" || !toolCallId) {
|
|
450
|
+
return { output };
|
|
451
|
+
}
|
|
174
452
|
let clean = output;
|
|
175
453
|
if (!outcomes.has(toolCallId))
|
|
176
454
|
clean = captureCursorShellResult(toolCallId, output);
|
|
177
455
|
const outcome = outcomes.get(toolCallId);
|
|
178
456
|
outcomes.delete(toolCallId);
|
|
179
457
|
policies.delete(toolCallId);
|
|
458
|
+
releaseCursorShellEnv(toolCallId);
|
|
180
459
|
return { output: clean, outcome };
|
|
181
460
|
}
|
|
182
461
|
/** Test/process cleanup. */
|
|
183
462
|
export function resetCursorShellCalls() {
|
|
463
|
+
for (const wrap of activeEnvWraps.values())
|
|
464
|
+
wrap.cleanup();
|
|
465
|
+
activeEnvWraps.clear();
|
|
466
|
+
pendingEnvWraps.clear();
|
|
184
467
|
policies.clear();
|
|
185
468
|
outcomes.clear();
|
|
469
|
+
configuredShell = undefined;
|
|
186
470
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "cursor-opencode-provider",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.7",
|
|
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
|
+
}
|