@co0ontty/wand 4.46.0 → 4.47.0
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/dist/build-info.json +3 -3
- package/dist/distribution-manager.d.ts +17 -0
- package/dist/distribution-manager.js +80 -17
- package/dist/server-update-routes.d.ts +5 -0
- package/dist/server-update-routes.js +15 -0
- package/dist/server.js +8 -2
- package/dist/structured-claude-adapter.d.ts +3 -1
- package/dist/structured-claude-adapter.js +48 -93
- package/dist/structured-codex-adapter.d.ts +5 -0
- package/dist/structured-codex-adapter.js +40 -78
- package/dist/structured-exec-host.d.ts +76 -0
- package/dist/structured-exec-host.js +117 -0
- package/dist/structured-exec-pump.d.ts +37 -0
- package/dist/structured-exec-pump.js +127 -0
- package/dist/structured-grok-adapter.d.ts +3 -1
- package/dist/structured-grok-adapter.js +28 -67
- package/dist/structured-opencode-adapter.d.ts +3 -1
- package/dist/structured-opencode-adapter.js +37 -79
- package/dist/structured-pi-adapter.d.ts +3 -1
- package/dist/structured-pi-adapter.js +27 -39
- package/dist/structured-qoder-adapter.d.ts +3 -1
- package/dist/structured-qoder-adapter.js +49 -87
- package/dist/structured-session-manager.d.ts +9 -1
- package/dist/structured-session-manager.js +399 -12
- package/dist/terminal-daemon-client.d.ts +15 -1
- package/dist/terminal-daemon-client.js +261 -0
- package/dist/terminal-daemon-protocol.d.ts +10 -3
- package/dist/terminal-daemon-protocol.js +1 -1
- package/dist/terminal-daemon-server.js +178 -0
- package/dist/web-ui/content/scripts.js +57 -57
- package/dist/web-ui/content/styles.css +1 -1
- package/dist/web-ui/embedded-assets.d.ts +1 -1
- package/dist/web-ui/embedded-assets.js +3 -3
- package/package.json +1 -1
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import { randomUUID } from "node:crypto";
|
|
3
|
+
/**
|
|
4
|
+
* Ownership seam for structured CLI runs, mirroring TerminalHost for PTYs.
|
|
5
|
+
* Persistent adapters (terminald) outlive web restarts; the in-process adapter
|
|
6
|
+
* deliberately keeps the legacy "die with the server" lifecycle.
|
|
7
|
+
*/
|
|
8
|
+
/** Per-stream replay log cap inside the daemon. Reducers need full history. */
|
|
9
|
+
export const STRUCTURED_RUN_LOG_MAX_CHARS = 8 * 1024 * 1024;
|
|
10
|
+
/** Resolve a stable daemon-side key for a session's active structured run. */
|
|
11
|
+
export function structuredRunId(sessionId) {
|
|
12
|
+
return `structured:${sessionId}`;
|
|
13
|
+
}
|
|
14
|
+
class InProcessStructuredExecProcess {
|
|
15
|
+
runId;
|
|
16
|
+
child;
|
|
17
|
+
incarnationId = randomUUID();
|
|
18
|
+
stdoutSeq = 0;
|
|
19
|
+
stderrSeq = 0;
|
|
20
|
+
constructor(runId, child) {
|
|
21
|
+
this.runId = runId;
|
|
22
|
+
this.child = child;
|
|
23
|
+
}
|
|
24
|
+
get pid() {
|
|
25
|
+
return this.child.pid ?? -1;
|
|
26
|
+
}
|
|
27
|
+
interrupt(signal) {
|
|
28
|
+
try {
|
|
29
|
+
this.child.kill((signal ?? "SIGTERM"));
|
|
30
|
+
}
|
|
31
|
+
catch { /* best-effort */ }
|
|
32
|
+
}
|
|
33
|
+
onStream(listener) {
|
|
34
|
+
const stdoutHandler = (chunk) => listener({ stream: "stdout", data: chunk.toString(), seq: ++this.stdoutSeq });
|
|
35
|
+
const stderrHandler = (chunk) => listener({ stream: "stderr", data: chunk.toString(), seq: ++this.stderrSeq });
|
|
36
|
+
this.child.stdout?.on("data", stdoutHandler);
|
|
37
|
+
this.child.stderr?.on("data", stderrHandler);
|
|
38
|
+
return {
|
|
39
|
+
dispose: () => {
|
|
40
|
+
this.child.stdout?.off("data", stdoutHandler);
|
|
41
|
+
this.child.stderr?.off("data", stderrHandler);
|
|
42
|
+
},
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
onExit(listener) {
|
|
46
|
+
let settled = false;
|
|
47
|
+
const closeHandler = (code, signal) => {
|
|
48
|
+
if (settled)
|
|
49
|
+
return;
|
|
50
|
+
settled = true;
|
|
51
|
+
listener({ exitCode: code, signal: signal === null || signal === undefined ? null : osSignalNumber(signal) });
|
|
52
|
+
};
|
|
53
|
+
const errorHandler = () => {
|
|
54
|
+
if (settled)
|
|
55
|
+
return;
|
|
56
|
+
settled = true;
|
|
57
|
+
listener({ exitCode: null, signal: null });
|
|
58
|
+
};
|
|
59
|
+
this.child.on("close", closeHandler);
|
|
60
|
+
this.child.on("error", errorHandler);
|
|
61
|
+
return {
|
|
62
|
+
dispose: () => {
|
|
63
|
+
this.child.off("close", closeHandler);
|
|
64
|
+
this.child.off("error", errorHandler);
|
|
65
|
+
},
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
/** Legacy/local adapter and deterministic test adapter for the structured seam. */
|
|
70
|
+
export class InProcessStructuredExecHost {
|
|
71
|
+
persistent = false;
|
|
72
|
+
processes = new Map();
|
|
73
|
+
async spawnStructured(request) {
|
|
74
|
+
const existing = this.processes.get(request.runId);
|
|
75
|
+
if (existing && existing.pid > 0)
|
|
76
|
+
return existing;
|
|
77
|
+
const wantsStdin = typeof request.stdinData === "string";
|
|
78
|
+
const child = spawn(request.file, request.args, {
|
|
79
|
+
cwd: request.cwd,
|
|
80
|
+
env: request.env,
|
|
81
|
+
stdio: wantsStdin ? ["pipe", "pipe", "pipe"] : ["ignore", "pipe", "pipe"],
|
|
82
|
+
});
|
|
83
|
+
if (wantsStdin)
|
|
84
|
+
child.stdin?.end(request.stdinData);
|
|
85
|
+
const wrapped = new InProcessStructuredExecProcess(request.runId, child);
|
|
86
|
+
this.processes.set(request.runId, wrapped);
|
|
87
|
+
wrapped.onExit(() => {
|
|
88
|
+
// Keep exited records until forgetRun so attachRun can still answer.
|
|
89
|
+
});
|
|
90
|
+
return wrapped;
|
|
91
|
+
}
|
|
92
|
+
async attachRun() {
|
|
93
|
+
return null;
|
|
94
|
+
}
|
|
95
|
+
async adoptRun() {
|
|
96
|
+
return null;
|
|
97
|
+
}
|
|
98
|
+
async listRuns() {
|
|
99
|
+
return [];
|
|
100
|
+
}
|
|
101
|
+
forgetRun(runId) {
|
|
102
|
+
const wrapped = this.processes.get(runId);
|
|
103
|
+
this.processes.delete(runId);
|
|
104
|
+
if (wrapped)
|
|
105
|
+
wrapped.interrupt();
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
/** Map a NodeJS.Signals name to the numeric value used by waitpid-style APIs. */
|
|
109
|
+
export function osSignalNumber(signal) {
|
|
110
|
+
const table = {
|
|
111
|
+
SIGHUP: 1, SIGINT: 2, SIGQUIT: 3, SIGILL: 4, SIGTRAP: 5, SIGABRT: 6,
|
|
112
|
+
SIGBUS: 7, SIGFPE: 8, SIGKILL: 9, SIGUSR1: 10, SIGSEGV: 11, SIGUSR2: 12,
|
|
113
|
+
SIGPIPE: 13, SIGALRM: 14, SIGTERM: 15, SIGCHLD: 17, SIGCONT: 18,
|
|
114
|
+
SIGSTOP: 19, SIGTSTP: 20,
|
|
115
|
+
};
|
|
116
|
+
return table[signal] ?? 0;
|
|
117
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import type { StructuredExecHost } from "./structured-exec-host.js";
|
|
3
|
+
import type { StructuredRunnerExecution, StructuredRunnerObserver, StructuredRunnerResult, StructuredRunnerTurnState } from "./structured-runner.js";
|
|
4
|
+
/**
|
|
5
|
+
* Shared pump for CLI-backed structured runners. One implementation drives both
|
|
6
|
+
* ownership modes: a local ChildProcess (legacy lifecycle, test injection) and
|
|
7
|
+
* a daemon-backed StructuredExecProcess whose streams survive web restarts.
|
|
8
|
+
* Per-provider differences stay in the adapter via processLine/finalize hooks.
|
|
9
|
+
*/
|
|
10
|
+
export interface StructuredCliPumpContext<S extends StructuredRunnerTurnState> {
|
|
11
|
+
state: S;
|
|
12
|
+
stderr: string;
|
|
13
|
+
observer: StructuredRunnerObserver;
|
|
14
|
+
/** Ask the underlying CLI to stop early (e.g. Claude ask-user-question). */
|
|
15
|
+
requestStop(): void;
|
|
16
|
+
}
|
|
17
|
+
export interface StructuredCliPumpOptions<S extends StructuredRunnerTurnState> {
|
|
18
|
+
sessionId: string;
|
|
19
|
+
file: string;
|
|
20
|
+
args: string[];
|
|
21
|
+
cwd: string;
|
|
22
|
+
env: NodeJS.ProcessEnv;
|
|
23
|
+
/** Written to stdin once then closed; omit for stdio-ignore CLIs. */
|
|
24
|
+
stdinData?: string;
|
|
25
|
+
observer: StructuredRunnerObserver;
|
|
26
|
+
/** When set and persistent, the run is owned by terminald instead of us. */
|
|
27
|
+
execHost?: StructuredExecHost;
|
|
28
|
+
/** Local/test spawn injection; ignored when the execHost takes over. */
|
|
29
|
+
spawnProcess?: typeof spawn;
|
|
30
|
+
/** Fresh per-run state (or reducer-owned state) for the pump context. */
|
|
31
|
+
createState(): S;
|
|
32
|
+
processLine(line: string, ctx: StructuredCliPumpContext<S>): void;
|
|
33
|
+
/** Raw stdout text hook (e.g. Claude stdoutTail bookkeeping). */
|
|
34
|
+
onStdoutText?(text: string): void;
|
|
35
|
+
finalize(ctx: StructuredCliPumpContext<S>, exitCode: number | null, signal: NodeJS.Signals | null, spawnError?: NodeJS.ErrnoException): StructuredRunnerResult;
|
|
36
|
+
}
|
|
37
|
+
export declare function startStructuredCli<S extends StructuredRunnerTurnState>(options: StructuredCliPumpOptions<S>): StructuredRunnerExecution;
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
export function startStructuredCli(options) {
|
|
3
|
+
const spawnedAt = new Date().toISOString();
|
|
4
|
+
const observer = options.observer;
|
|
5
|
+
const ctx = {
|
|
6
|
+
state: options.createState(),
|
|
7
|
+
stderr: "",
|
|
8
|
+
observer,
|
|
9
|
+
requestStop: () => source?.interrupt(),
|
|
10
|
+
};
|
|
11
|
+
let source = null;
|
|
12
|
+
let sourceReady;
|
|
13
|
+
let lineBuffer = "";
|
|
14
|
+
let settled = false;
|
|
15
|
+
let resolveCompletion;
|
|
16
|
+
const completion = new Promise((resolve) => {
|
|
17
|
+
resolveCompletion = resolve;
|
|
18
|
+
});
|
|
19
|
+
const finish = (exitCode, signal, spawnError) => {
|
|
20
|
+
if (settled)
|
|
21
|
+
return;
|
|
22
|
+
settled = true;
|
|
23
|
+
if (lineBuffer.trim())
|
|
24
|
+
options.processLine(lineBuffer, ctx);
|
|
25
|
+
lineBuffer = "";
|
|
26
|
+
resolveCompletion(options.finalize(ctx, exitCode, signal, spawnError));
|
|
27
|
+
};
|
|
28
|
+
const handleStdoutText = (text) => {
|
|
29
|
+
if (!observer.isActive())
|
|
30
|
+
return;
|
|
31
|
+
observer.onStdout?.(text);
|
|
32
|
+
options.onStdoutText?.(text);
|
|
33
|
+
lineBuffer += text;
|
|
34
|
+
const lines = lineBuffer.split("\n");
|
|
35
|
+
lineBuffer = lines.pop() ?? "";
|
|
36
|
+
for (const line of lines)
|
|
37
|
+
options.processLine(line, ctx);
|
|
38
|
+
};
|
|
39
|
+
const handleStderrText = (text) => {
|
|
40
|
+
if (!observer.isActive())
|
|
41
|
+
return;
|
|
42
|
+
observer.onStderr?.(text);
|
|
43
|
+
ctx.stderr += text;
|
|
44
|
+
};
|
|
45
|
+
const useRemoteHost = options.execHost?.persistent === true;
|
|
46
|
+
if (useRemoteHost) {
|
|
47
|
+
const host = options.execHost;
|
|
48
|
+
const request = {
|
|
49
|
+
runId: `structured:${options.sessionId}`,
|
|
50
|
+
file: options.file,
|
|
51
|
+
args: options.args,
|
|
52
|
+
cwd: options.cwd,
|
|
53
|
+
env: options.env,
|
|
54
|
+
...(options.stdinData !== undefined ? { stdinData: options.stdinData } : {}),
|
|
55
|
+
};
|
|
56
|
+
let pendingInterrupt = null;
|
|
57
|
+
sourceReady = host.spawnStructured(request).then((handle) => {
|
|
58
|
+
source = { pid: handle.pid, interrupt: () => handle.interrupt() };
|
|
59
|
+
if (pendingInterrupt !== null)
|
|
60
|
+
handle.interrupt(pendingInterrupt);
|
|
61
|
+
wireRemoteHandle(handle, handleStdoutText, handleStderrText, finish);
|
|
62
|
+
}, () => {
|
|
63
|
+
// Daemon vanished between health check and spawn; surface as spawn error.
|
|
64
|
+
finish(null, null, Object.assign(new Error(`terminal daemon unavailable for ${options.file}`), { code: "EDAEMON" }));
|
|
65
|
+
});
|
|
66
|
+
source = {
|
|
67
|
+
pid: null,
|
|
68
|
+
interrupt: (signal) => {
|
|
69
|
+
pendingInterrupt = signal ?? "SIGTERM";
|
|
70
|
+
void sourceReady.then(() => source?.interrupt());
|
|
71
|
+
},
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
else {
|
|
75
|
+
const spawnProcess = options.spawnProcess ?? spawn;
|
|
76
|
+
const wantsStdin = typeof options.stdinData === "string";
|
|
77
|
+
const child = spawnProcess(options.file, options.args, {
|
|
78
|
+
cwd: options.cwd,
|
|
79
|
+
env: options.env,
|
|
80
|
+
stdio: wantsStdin ? ["pipe", "pipe", "pipe"] : ["ignore", "pipe", "pipe"],
|
|
81
|
+
});
|
|
82
|
+
if (wantsStdin)
|
|
83
|
+
child.stdin?.end(options.stdinData);
|
|
84
|
+
source = { pid: child.pid ?? null, interrupt: () => child.kill("SIGTERM") };
|
|
85
|
+
child.stdout?.on("data", (chunk) => handleStdoutText(chunk.toString()));
|
|
86
|
+
child.stderr?.on("data", (chunk) => handleStderrText(chunk.toString()));
|
|
87
|
+
child.on("error", (error) => finish(null, null, error));
|
|
88
|
+
child.on("close", (exitCode, signalName) => finish(exitCode, signalName === null || signalName === undefined ? null : signalName));
|
|
89
|
+
sourceReady = Promise.resolve();
|
|
90
|
+
}
|
|
91
|
+
return {
|
|
92
|
+
args: options.args,
|
|
93
|
+
spawnedAt,
|
|
94
|
+
get pid() {
|
|
95
|
+
return source?.pid ?? null;
|
|
96
|
+
},
|
|
97
|
+
completion,
|
|
98
|
+
interrupt: () => {
|
|
99
|
+
try {
|
|
100
|
+
source?.interrupt();
|
|
101
|
+
}
|
|
102
|
+
catch { /* best-effort external interruption */ }
|
|
103
|
+
},
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
function wireRemoteHandle(handle, onStdout, onStderr, finish) {
|
|
107
|
+
handle.onStream((event) => {
|
|
108
|
+
if (event.stream === "stdout")
|
|
109
|
+
onStdout(event.data);
|
|
110
|
+
else
|
|
111
|
+
onStderr(event.data);
|
|
112
|
+
});
|
|
113
|
+
handle.onExit((event) => {
|
|
114
|
+
finish(event.exitCode, numericToSignalName(event.signal));
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
function numericToSignalName(signal) {
|
|
118
|
+
if (signal === null || signal === 0)
|
|
119
|
+
return null;
|
|
120
|
+
const names = {
|
|
121
|
+
1: "SIGHUP", 2: "SIGINT", 3: "SIGQUIT", 4: "SIGILL", 5: "SIGTRAP", 6: "SIGABRT",
|
|
122
|
+
7: "SIGBUS", 8: "SIGFPE", 9: "SIGKILL", 10: "SIGUSR1", 11: "SIGSEGV", 12: "SIGUSR2",
|
|
123
|
+
13: "SIGPIPE", 14: "SIGALRM", 15: "SIGTERM", 17: "SIGCHLD", 18: "SIGCONT",
|
|
124
|
+
19: "SIGSTOP", 20: "SIGTSTP",
|
|
125
|
+
};
|
|
126
|
+
return names[signal] ?? "SIGTERM";
|
|
127
|
+
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { spawn } from "node:child_process";
|
|
2
|
+
import type { StructuredExecHost } from "./structured-exec-host.js";
|
|
2
3
|
import type { StructuredRunnerAdapter, StructuredRunnerContext, StructuredRunnerExecution, StructuredRunnerObserver, StructuredRunnerTurnState } from "./structured-runner.js";
|
|
3
4
|
import type { SessionSnapshot } from "./types.js";
|
|
4
5
|
export type GrokTurnState = StructuredRunnerTurnState;
|
|
@@ -7,6 +8,7 @@ export declare function buildGrokArgs(session: SessionSnapshot, prompt: string):
|
|
|
7
8
|
export declare function applyGrokEvent(state: GrokTurnState, event: Record<string, unknown>): string | null;
|
|
8
9
|
export declare class GrokRunner implements StructuredRunnerAdapter {
|
|
9
10
|
private readonly spawnProcess;
|
|
10
|
-
|
|
11
|
+
private readonly execHost?;
|
|
12
|
+
constructor(spawnProcess?: typeof spawn, execHost?: StructuredExecHost | undefined);
|
|
11
13
|
start(context: StructuredRunnerContext, observer: StructuredRunnerObserver): StructuredRunnerExecution;
|
|
12
14
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { spawn } from "node:child_process";
|
|
2
|
+
import { startStructuredCli } from "./structured-exec-pump.js";
|
|
2
3
|
import { thinkingEffortToGrokEffort } from "./structured-provider-common.js";
|
|
3
4
|
function asRecord(value) {
|
|
4
5
|
return value && typeof value === "object" && !Array.isArray(value)
|
|
@@ -70,87 +71,47 @@ export function applyGrokEvent(state, event) {
|
|
|
70
71
|
}
|
|
71
72
|
export class GrokRunner {
|
|
72
73
|
spawnProcess;
|
|
73
|
-
|
|
74
|
+
execHost;
|
|
75
|
+
constructor(spawnProcess = spawn, execHost) {
|
|
74
76
|
this.spawnProcess = spawnProcess;
|
|
77
|
+
this.execHost = execHost;
|
|
75
78
|
}
|
|
76
79
|
start(context, observer) {
|
|
77
80
|
const args = buildGrokArgs(context.session, context.prompt);
|
|
78
|
-
const spawnedAt = new Date().toISOString();
|
|
79
|
-
const child = this.spawnProcess("grok", args, {
|
|
80
|
-
cwd: context.session.cwd,
|
|
81
|
-
env: context.env,
|
|
82
|
-
stdio: ["ignore", "pipe", "pipe"],
|
|
83
|
-
});
|
|
84
81
|
const state = {
|
|
85
82
|
blocks: [],
|
|
86
83
|
result: "",
|
|
87
84
|
sessionId: context.session.claudeSessionId,
|
|
88
85
|
model: context.session.selectedModel ?? context.session.structuredState?.model,
|
|
89
86
|
};
|
|
90
|
-
let lineBuffer = "";
|
|
91
|
-
let stderr = "";
|
|
92
87
|
let primaryError = null;
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
const event = JSON.parse(trimmed);
|
|
105
|
-
observer.onEvent?.(event);
|
|
106
|
-
primaryError = applyGrokEvent(state, event) ?? primaryError;
|
|
107
|
-
observer.onUpdate(state);
|
|
108
|
-
}
|
|
109
|
-
catch { /* Grok diagnostics belong on stderr; ignore non-JSON stdout defensively. */ }
|
|
110
|
-
};
|
|
111
|
-
const completion = new Promise((resolve) => {
|
|
112
|
-
child.stdout?.on("data", (chunk) => {
|
|
113
|
-
if (!observer.isActive())
|
|
114
|
-
return;
|
|
115
|
-
const text = chunk.toString();
|
|
116
|
-
observer.onStdout?.(text);
|
|
117
|
-
lineBuffer += text;
|
|
118
|
-
const lines = lineBuffer.split("\n");
|
|
119
|
-
lineBuffer = lines.pop() ?? "";
|
|
120
|
-
for (const line of lines)
|
|
121
|
-
processLine(line);
|
|
122
|
-
});
|
|
123
|
-
child.stderr?.on("data", (chunk) => {
|
|
88
|
+
return startStructuredCli({
|
|
89
|
+
sessionId: context.session.id,
|
|
90
|
+
file: "grok",
|
|
91
|
+
args,
|
|
92
|
+
cwd: context.session.cwd,
|
|
93
|
+
env: context.env,
|
|
94
|
+
observer,
|
|
95
|
+
execHost: this.execHost,
|
|
96
|
+
spawnProcess: this.spawnProcess,
|
|
97
|
+
createState: () => state,
|
|
98
|
+
processLine: (line) => {
|
|
124
99
|
if (!observer.isActive())
|
|
125
100
|
return;
|
|
126
|
-
const
|
|
127
|
-
|
|
128
|
-
stderr += text;
|
|
129
|
-
});
|
|
130
|
-
child.on("error", (error) => {
|
|
131
|
-
if (settled)
|
|
101
|
+
const trimmed = line.trim();
|
|
102
|
+
if (!trimmed)
|
|
132
103
|
return;
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
})
|
|
104
|
+
try {
|
|
105
|
+
const event = JSON.parse(trimmed);
|
|
106
|
+
observer.onEvent?.(event);
|
|
107
|
+
primaryError = applyGrokEvent(state, event) ?? primaryError;
|
|
108
|
+
observer.onUpdate(state);
|
|
109
|
+
}
|
|
110
|
+
catch { /* Grok diagnostics belong on stderr; ignore non-JSON stdout defensively. */ }
|
|
111
|
+
},
|
|
112
|
+
finalize: (ctx, exitCode, signal, spawnError) => ({
|
|
113
|
+
state, exitCode, signal, stderr: ctx.stderr, primaryError, ...(spawnError ? { spawnError } : {}),
|
|
114
|
+
}),
|
|
144
115
|
});
|
|
145
|
-
return {
|
|
146
|
-
args,
|
|
147
|
-
spawnedAt,
|
|
148
|
-
pid: child.pid ?? null,
|
|
149
|
-
completion,
|
|
150
|
-
interrupt: () => { try {
|
|
151
|
-
child.kill("SIGTERM");
|
|
152
|
-
}
|
|
153
|
-
catch { /* best effort */ } },
|
|
154
|
-
};
|
|
155
116
|
}
|
|
156
117
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { spawn } from "node:child_process";
|
|
2
|
+
import type { StructuredExecHost } from "./structured-exec-host.js";
|
|
2
3
|
import type { StructuredRunnerAdapter, StructuredRunnerContext, StructuredRunnerExecution, StructuredRunnerObserver, StructuredRunnerTurnState } from "./structured-runner.js";
|
|
3
4
|
import type { SessionSnapshot } from "./types.js";
|
|
4
5
|
export type OpenCodeTurnState = StructuredRunnerTurnState;
|
|
@@ -8,6 +9,7 @@ export declare function applyOpenCodeEvent(turnState: OpenCodeTurnState, event:
|
|
|
8
9
|
/** Production adapter for the external `opencode run --format json` process. */
|
|
9
10
|
export declare class OpenCodeRunner implements StructuredRunnerAdapter {
|
|
10
11
|
private readonly spawnProcess;
|
|
11
|
-
|
|
12
|
+
private readonly execHost?;
|
|
13
|
+
constructor(spawnProcess?: typeof spawn, execHost?: StructuredExecHost | undefined);
|
|
12
14
|
start(context: StructuredRunnerContext, observer: StructuredRunnerObserver): StructuredRunnerExecution;
|
|
13
15
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { randomUUID } from "node:crypto";
|
|
2
2
|
import { spawn } from "node:child_process";
|
|
3
|
+
import { startStructuredCli } from "./structured-exec-pump.js";
|
|
3
4
|
import { thinkingEffortToOpenCodeVariant } from "./structured-provider-common.js";
|
|
4
5
|
function asRecord(value) {
|
|
5
6
|
return value && typeof value === "object" && !Array.isArray(value)
|
|
@@ -117,18 +118,13 @@ export function applyOpenCodeEvent(turnState, event, createId = randomUUID) {
|
|
|
117
118
|
/** Production adapter for the external `opencode run --format json` process. */
|
|
118
119
|
export class OpenCodeRunner {
|
|
119
120
|
spawnProcess;
|
|
120
|
-
|
|
121
|
+
execHost;
|
|
122
|
+
constructor(spawnProcess = spawn, execHost) {
|
|
121
123
|
this.spawnProcess = spawnProcess;
|
|
124
|
+
this.execHost = execHost;
|
|
122
125
|
}
|
|
123
126
|
start(context, observer) {
|
|
124
127
|
const args = buildOpenCodeArgs(context.session);
|
|
125
|
-
const spawnedAt = new Date().toISOString();
|
|
126
|
-
const child = this.spawnProcess("opencode", args, {
|
|
127
|
-
cwd: context.session.cwd,
|
|
128
|
-
env: context.env,
|
|
129
|
-
stdio: ["pipe", "pipe", "pipe"],
|
|
130
|
-
});
|
|
131
|
-
child.stdin?.end(context.prompt);
|
|
132
128
|
const state = {
|
|
133
129
|
blocks: [],
|
|
134
130
|
result: "",
|
|
@@ -136,83 +132,45 @@ export class OpenCodeRunner {
|
|
|
136
132
|
model: context.session.selectedModel ?? context.session.structuredState?.model,
|
|
137
133
|
usage: undefined,
|
|
138
134
|
};
|
|
139
|
-
let lineBuffer = "";
|
|
140
|
-
let stderr = "";
|
|
141
135
|
let primaryError = null;
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
const trimmed = line.trim();
|
|
155
|
-
if (!trimmed)
|
|
156
|
-
return;
|
|
157
|
-
let event;
|
|
158
|
-
try {
|
|
159
|
-
event = JSON.parse(trimmed);
|
|
160
|
-
}
|
|
161
|
-
catch {
|
|
162
|
-
return;
|
|
163
|
-
}
|
|
164
|
-
observer.onEvent?.(event);
|
|
165
|
-
const error = applyOpenCodeEvent(state, event);
|
|
166
|
-
if (error)
|
|
167
|
-
primaryError = error;
|
|
168
|
-
observer.onUpdate(state);
|
|
169
|
-
};
|
|
170
|
-
const completion = new Promise((resolve) => {
|
|
171
|
-
child.stdout?.on("data", (chunk) => {
|
|
172
|
-
if (!observer.isActive())
|
|
173
|
-
return;
|
|
174
|
-
const text = chunk.toString();
|
|
175
|
-
observer.onStdout?.(text);
|
|
176
|
-
lineBuffer += text;
|
|
177
|
-
const lines = lineBuffer.split("\n");
|
|
178
|
-
lineBuffer = lines.pop() ?? "";
|
|
179
|
-
for (const line of lines)
|
|
180
|
-
processLine(line);
|
|
181
|
-
});
|
|
182
|
-
child.stderr?.on("data", (chunk) => {
|
|
136
|
+
return startStructuredCli({
|
|
137
|
+
sessionId: context.session.id,
|
|
138
|
+
file: "opencode",
|
|
139
|
+
args,
|
|
140
|
+
cwd: context.session.cwd,
|
|
141
|
+
env: context.env,
|
|
142
|
+
stdinData: context.prompt,
|
|
143
|
+
observer,
|
|
144
|
+
execHost: this.execHost,
|
|
145
|
+
spawnProcess: this.spawnProcess,
|
|
146
|
+
createState: () => state,
|
|
147
|
+
processLine: (line) => {
|
|
183
148
|
if (!observer.isActive())
|
|
184
149
|
return;
|
|
185
|
-
const
|
|
186
|
-
|
|
187
|
-
stderr += text;
|
|
188
|
-
});
|
|
189
|
-
child.on("error", (error) => {
|
|
190
|
-
if (settled)
|
|
191
|
-
return;
|
|
192
|
-
settled = true;
|
|
193
|
-
resolve(result(null, null, error));
|
|
194
|
-
});
|
|
195
|
-
child.on("close", (exitCode, signal) => {
|
|
196
|
-
if (settled)
|
|
150
|
+
const trimmed = line.trim();
|
|
151
|
+
if (!trimmed)
|
|
197
152
|
return;
|
|
198
|
-
|
|
199
|
-
if (lineBuffer.trim())
|
|
200
|
-
processLine(lineBuffer);
|
|
201
|
-
lineBuffer = "";
|
|
202
|
-
resolve(result(exitCode, signal));
|
|
203
|
-
});
|
|
204
|
-
});
|
|
205
|
-
return {
|
|
206
|
-
args,
|
|
207
|
-
spawnedAt,
|
|
208
|
-
pid: child.pid ?? null,
|
|
209
|
-
completion,
|
|
210
|
-
interrupt: () => {
|
|
153
|
+
let event;
|
|
211
154
|
try {
|
|
212
|
-
|
|
155
|
+
event = JSON.parse(trimmed);
|
|
213
156
|
}
|
|
214
|
-
catch {
|
|
157
|
+
catch {
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
observer.onEvent?.(event);
|
|
161
|
+
const error = applyOpenCodeEvent(state, event);
|
|
162
|
+
if (error)
|
|
163
|
+
primaryError = error;
|
|
164
|
+
observer.onUpdate(state);
|
|
215
165
|
},
|
|
216
|
-
|
|
166
|
+
finalize: (ctx, exitCode, signal, spawnError) => ({
|
|
167
|
+
state,
|
|
168
|
+
exitCode,
|
|
169
|
+
signal,
|
|
170
|
+
stderr: ctx.stderr,
|
|
171
|
+
primaryError,
|
|
172
|
+
...(spawnError ? { spawnError } : {}),
|
|
173
|
+
}),
|
|
174
|
+
});
|
|
217
175
|
}
|
|
218
176
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { spawn } from "node:child_process";
|
|
2
|
+
import type { StructuredExecHost } from "./structured-exec-host.js";
|
|
2
3
|
import type { StructuredRunnerAdapter, StructuredRunnerContext, StructuredRunnerExecution, StructuredRunnerObserver, StructuredRunnerTurnState } from "./structured-runner.js";
|
|
3
4
|
import type { SessionSnapshot } from "./types.js";
|
|
4
5
|
export declare function buildPiArgs(session: SessionSnapshot, prompt: string): string[];
|
|
@@ -6,6 +7,7 @@ export declare function piToolName(name: string): string;
|
|
|
6
7
|
export declare function applyPiEvent(state: StructuredRunnerTurnState, event: Record<string, unknown>): string | null;
|
|
7
8
|
export declare class PiRunner implements StructuredRunnerAdapter {
|
|
8
9
|
private readonly spawnProcess;
|
|
9
|
-
|
|
10
|
+
private readonly execHost?;
|
|
11
|
+
constructor(spawnProcess?: typeof spawn, execHost?: StructuredExecHost | undefined);
|
|
10
12
|
start(context: StructuredRunnerContext, observer: StructuredRunnerObserver): StructuredRunnerExecution;
|
|
11
13
|
}
|