@nowcrew/daemon 0.5.44 → 0.5.46
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 -6
- package/dist/agent-memory/bridge.js +101 -10
- package/dist/computer-service.js +38 -5
- package/dist/daemon-installation.js +35 -18
- package/dist/daemon-update-eligibility.js +53 -12
- package/dist/daemon-updater.js +1 -1
- package/dist/execution-journal.js +1 -1
- package/dist/execution-protocol.js +3 -11
- package/dist/execution-runner.js +13 -4
- package/dist/external-output.js +4 -0
- package/dist/list-models.js +102 -8
- package/dist/local-executor.js +87 -14
- package/dist/local-memory-diagnostics.js +336 -0
- package/dist/local-memory-telemetry.js +224 -0
- package/dist/machine-info.js +22 -7
- package/dist/main.js +6 -1
- package/dist/normalize.js +14 -4
- package/dist/runtime-capabilities.js +11 -1
- package/dist/runtime-probe.js +26 -0
- package/dist/runtime-startup-gate.js +2 -0
- package/dist/runtimes/codex-app-server-runner.js +10 -0
- package/dist/runtimes/hermes-models.js +117 -0
- package/dist/runtimes/hermes.js +6 -0
- package/dist/runtimes/kimi-acp-runner.js +116 -36
- package/dist/runtimes/opencode-runner.js +122 -0
- package/dist/runtimes/opencode.js +181 -0
- package/dist/serve.js +27 -4
- package/dist/slog.js +21 -3
- package/dist/supervised-runtime.js +22 -1
- package/dist/windows-scheduled-task.js +64 -0
- package/dist/workspace.js +34 -3
- package/package.json +8 -9
|
@@ -3,7 +3,7 @@ import { parseArgs } from "node:util";
|
|
|
3
3
|
import { Readable, Writable } from "node:stream";
|
|
4
4
|
import { pathToFileURL } from "node:url";
|
|
5
5
|
import spawn from "cross-spawn";
|
|
6
|
-
import { PROTOCOL_VERSION, client, methods, ndJsonStream, } from "@agentclientprotocol/sdk";
|
|
6
|
+
import { PROTOCOL_VERSION, RequestError, client, methods, ndJsonStream, } from "@agentclientprotocol/sdk";
|
|
7
7
|
import { augmentedPath } from "../runtime-path.js";
|
|
8
8
|
import { startFirstProgressWatchdog } from "./progress-watchdog.js";
|
|
9
9
|
import { assertKimiLegacyPromptFits, buildKimiArgs } from "./kimi.js";
|
|
@@ -33,13 +33,13 @@ function textContent(content) {
|
|
|
33
33
|
}).join("\n");
|
|
34
34
|
}
|
|
35
35
|
/** Translate stable ACP updates into daemon-owned NDJSON, without exposing thought chunks. */
|
|
36
|
-
export function
|
|
36
|
+
export function mapAcpUpdate(provider, update) {
|
|
37
37
|
if (update.sessionUpdate === "agent_message_chunk" && update.content.type === "text") {
|
|
38
|
-
return [{ type:
|
|
38
|
+
return [{ type: `${provider}.acp.text_delta`, text: update.content.text }];
|
|
39
39
|
}
|
|
40
40
|
if (update.sessionUpdate === "tool_call") {
|
|
41
41
|
return [{
|
|
42
|
-
type:
|
|
42
|
+
type: `${provider}.acp.tool_call`,
|
|
43
43
|
id: update.toolCallId,
|
|
44
44
|
title: update.title,
|
|
45
45
|
...(update.kind === undefined ? {} : { kind: update.kind }),
|
|
@@ -50,7 +50,7 @@ export function mapKimiAcpUpdate(update) {
|
|
|
50
50
|
if (update.sessionUpdate === "tool_call_update") {
|
|
51
51
|
const output = textContent(update.content);
|
|
52
52
|
return [{
|
|
53
|
-
type:
|
|
53
|
+
type: `${provider}.acp.tool_result`,
|
|
54
54
|
id: update.toolCallId,
|
|
55
55
|
...(update.status === undefined ? {} : { status: update.status }),
|
|
56
56
|
...(output ? { content: output } : {}),
|
|
@@ -58,6 +58,9 @@ export function mapKimiAcpUpdate(update) {
|
|
|
58
58
|
}
|
|
59
59
|
return [];
|
|
60
60
|
}
|
|
61
|
+
export function mapKimiAcpUpdate(update) {
|
|
62
|
+
return mapAcpUpdate("kimi", update);
|
|
63
|
+
}
|
|
61
64
|
function safeErrorMessage(error, prompt) {
|
|
62
65
|
const raw = error instanceof Error ? error.message : String(error);
|
|
63
66
|
const redacted = prompt && raw.includes(prompt) ? raw.replaceAll(prompt, "[prompt redacted]") : raw;
|
|
@@ -67,6 +70,20 @@ export function isKimiAuthenticationRequired(error) {
|
|
|
67
70
|
const message = error instanceof Error ? error.message : String(error);
|
|
68
71
|
return /\bauthentication required\b/i.test(message);
|
|
69
72
|
}
|
|
73
|
+
export function isAcpSessionNotFound(error) {
|
|
74
|
+
if (!(error instanceof RequestError) || (error.code !== -32602 && error.code !== -32603)) {
|
|
75
|
+
return false;
|
|
76
|
+
}
|
|
77
|
+
let data = "";
|
|
78
|
+
try {
|
|
79
|
+
data = JSON.stringify(error.data ?? "");
|
|
80
|
+
}
|
|
81
|
+
catch {
|
|
82
|
+
data = String(error.data ?? "");
|
|
83
|
+
}
|
|
84
|
+
return /session not found|no session found|unknown (?:durable )?session/i
|
|
85
|
+
.test(`${error.message} ${data}`);
|
|
86
|
+
}
|
|
70
87
|
export function kimiResumeMethod(capabilities) {
|
|
71
88
|
if (capabilities?.sessionCapabilities?.resume != null)
|
|
72
89
|
return "resume";
|
|
@@ -88,6 +105,36 @@ export function selectKimiPermission(params) {
|
|
|
88
105
|
? { outcome: { outcome: "cancelled" } }
|
|
89
106
|
: { outcome: { outcome: "selected", optionId: allowed.optionId } };
|
|
90
107
|
}
|
|
108
|
+
function selectConfigId(options, category) {
|
|
109
|
+
const normalizedNames = category === "model"
|
|
110
|
+
? new Set(["model"])
|
|
111
|
+
: new Set(["reasoning", "effort", "thought level", "thinking level"]);
|
|
112
|
+
const matches = (options ?? []).filter((option) => (option.type === "select"
|
|
113
|
+
&& (option.category === category
|
|
114
|
+
|| normalizedNames.has(option.id.toLowerCase())
|
|
115
|
+
|| normalizedNames.has(option.name.toLowerCase()))));
|
|
116
|
+
return matches.length === 1 ? matches[0].id : null;
|
|
117
|
+
}
|
|
118
|
+
async function applySessionConfig(context, provider, sessionId, configOptions, options) {
|
|
119
|
+
const modelConfigId = provider === "kimi" && options.model
|
|
120
|
+
? "model"
|
|
121
|
+
: selectConfigId(configOptions, "model");
|
|
122
|
+
if (options.model && modelConfigId !== null) {
|
|
123
|
+
await context.request(methods.agent.session.setConfigOption, {
|
|
124
|
+
sessionId,
|
|
125
|
+
configId: modelConfigId,
|
|
126
|
+
value: options.model,
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
const reasoningConfigId = selectConfigId(configOptions, "thought_level");
|
|
130
|
+
if (options.reasoning && reasoningConfigId !== null) {
|
|
131
|
+
await context.request(methods.agent.session.setConfigOption, {
|
|
132
|
+
sessionId,
|
|
133
|
+
configId: reasoningConfigId,
|
|
134
|
+
value: options.reasoning,
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
}
|
|
91
138
|
async function readPrompt() {
|
|
92
139
|
process.stdin.setEncoding("utf8");
|
|
93
140
|
let prompt = "";
|
|
@@ -116,6 +163,7 @@ async function stopChild(child) {
|
|
|
116
163
|
}
|
|
117
164
|
/** Probe the ACP transport without starting a session or forcing an optional interactive login flow. */
|
|
118
165
|
export async function probeKimiAcp(options, spawnProcess = spawn) {
|
|
166
|
+
const provider = options.provider ?? "kimi";
|
|
119
167
|
const child = spawnProcess(options.bin, ["acp"], {
|
|
120
168
|
cwd: process.cwd(),
|
|
121
169
|
// probe 在 daemon 自身 PATH 下运行,补上用户级 CLI 目录,与 which 探测保持一致。
|
|
@@ -125,12 +173,13 @@ export async function probeKimiAcp(options, spawnProcess = spawn) {
|
|
|
125
173
|
if (child.stdin === null || child.stdout === null || child.stderr === null)
|
|
126
174
|
return false;
|
|
127
175
|
child.stderr.resume();
|
|
128
|
-
const app = client({ name:
|
|
176
|
+
const app = client({ name: `nowcrew-daemon-${provider}-probe` })
|
|
129
177
|
.onRequest(methods.client.session.requestPermission, () => ({
|
|
130
178
|
outcome: { outcome: "cancelled" },
|
|
131
179
|
}))
|
|
132
180
|
.onNotification(methods.client.session.update, () => undefined);
|
|
133
181
|
let timeout;
|
|
182
|
+
let onAbort;
|
|
134
183
|
try {
|
|
135
184
|
const stream = ndJsonStream(Writable.toWeb(child.stdin), Readable.toWeb(child.stdout));
|
|
136
185
|
const connected = app.connectWith(stream, async (context) => {
|
|
@@ -149,6 +198,16 @@ export async function probeKimiAcp(options, spawnProcess = spawn) {
|
|
|
149
198
|
resolve(false);
|
|
150
199
|
}, PROBE_TIMEOUT_MS);
|
|
151
200
|
}),
|
|
201
|
+
new Promise((resolve) => {
|
|
202
|
+
onAbort = () => {
|
|
203
|
+
void stopChild(child);
|
|
204
|
+
resolve(false);
|
|
205
|
+
};
|
|
206
|
+
if (options.signal?.aborted)
|
|
207
|
+
onAbort();
|
|
208
|
+
else
|
|
209
|
+
options.signal?.addEventListener("abort", onAbort, { once: true });
|
|
210
|
+
}),
|
|
152
211
|
]);
|
|
153
212
|
return result;
|
|
154
213
|
}
|
|
@@ -158,10 +217,14 @@ export async function probeKimiAcp(options, spawnProcess = spawn) {
|
|
|
158
217
|
finally {
|
|
159
218
|
if (timeout !== undefined)
|
|
160
219
|
clearTimeout(timeout);
|
|
220
|
+
if (onAbort !== undefined)
|
|
221
|
+
options.signal?.removeEventListener("abort", onAbort);
|
|
161
222
|
await stopChild(child);
|
|
162
223
|
}
|
|
163
224
|
}
|
|
164
225
|
export async function runKimiAcp(options) {
|
|
226
|
+
const provider = options.provider ?? "kimi";
|
|
227
|
+
const displayName = provider === "kimi" ? "Kimi" : "Hermes";
|
|
165
228
|
const prompt = await readPrompt();
|
|
166
229
|
const child = spawn(options.bin, ["acp"], {
|
|
167
230
|
cwd: process.cwd(),
|
|
@@ -170,7 +233,7 @@ export async function runKimiAcp(options) {
|
|
|
170
233
|
});
|
|
171
234
|
let runtimeChild = child;
|
|
172
235
|
if (child.stdin === null || child.stdout === null || child.stderr === null) {
|
|
173
|
-
throw new Error(
|
|
236
|
+
throw new Error(`${displayName} ACP process did not expose stdio`);
|
|
174
237
|
}
|
|
175
238
|
child.stderr.pipe(process.stderr, { end: false });
|
|
176
239
|
let context = null;
|
|
@@ -192,12 +255,12 @@ export async function runKimiAcp(options) {
|
|
|
192
255
|
};
|
|
193
256
|
process.once("SIGTERM", onSignal);
|
|
194
257
|
process.once("SIGINT", onSignal);
|
|
195
|
-
const app = client({ name:
|
|
258
|
+
const app = client({ name: `nowcrew-daemon-${provider}` })
|
|
196
259
|
.onRequest(methods.client.session.requestPermission, ({ params }) => selectKimiPermission(params))
|
|
197
260
|
.onNotification(methods.client.session.update, async ({ params }) => {
|
|
198
261
|
acpSemanticProgress = true;
|
|
199
262
|
firstProgress.observe();
|
|
200
|
-
for (const event of
|
|
263
|
+
for (const event of mapAcpUpdate(provider, params.update))
|
|
201
264
|
await jsonLine(event);
|
|
202
265
|
});
|
|
203
266
|
let firstProgress = startFirstProgressWatchdog(() => undefined);
|
|
@@ -211,29 +274,44 @@ export async function runKimiAcp(options) {
|
|
|
211
274
|
clientCapabilities: {},
|
|
212
275
|
clientInfo: { name: "nowcrew-daemon", version: "1" },
|
|
213
276
|
});
|
|
214
|
-
if (process.env.CREW_KIMI_ACP_DEBUG === "1") {
|
|
215
|
-
process.stderr.write(
|
|
277
|
+
if (provider === "kimi" && process.env.CREW_KIMI_ACP_DEBUG === "1") {
|
|
278
|
+
process.stderr.write(`${displayName} ACP auth methods: ${JSON.stringify(initialized.authMethods ?? [])}\n`);
|
|
216
279
|
}
|
|
280
|
+
let configOptions;
|
|
217
281
|
if (options.resume && options.sessionId) {
|
|
218
282
|
const resumeMethod = kimiResumeMethod(initialized.agentCapabilities);
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
283
|
+
try {
|
|
284
|
+
if (resumeMethod === "resume") {
|
|
285
|
+
const resumed = await nextContext.request(methods.agent.session.resume, {
|
|
286
|
+
sessionId: options.sessionId,
|
|
287
|
+
cwd: process.cwd(),
|
|
288
|
+
mcpServers: [],
|
|
289
|
+
});
|
|
290
|
+
configOptions = resumed.configOptions;
|
|
291
|
+
}
|
|
292
|
+
else if (resumeMethod === "load") {
|
|
293
|
+
const loaded = await nextContext.request(methods.agent.session.load, {
|
|
294
|
+
sessionId: options.sessionId,
|
|
295
|
+
cwd: process.cwd(),
|
|
296
|
+
mcpServers: [],
|
|
297
|
+
});
|
|
298
|
+
configOptions = loaded.configOptions;
|
|
299
|
+
}
|
|
300
|
+
else {
|
|
301
|
+
throw new Error(`${displayName} ACP does not advertise session resume support`);
|
|
302
|
+
}
|
|
303
|
+
sessionId = options.sessionId;
|
|
225
304
|
}
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
305
|
+
catch (error) {
|
|
306
|
+
if (provider !== "hermes" || !isAcpSessionNotFound(error))
|
|
307
|
+
throw error;
|
|
308
|
+
const session = await nextContext.request(methods.agent.session.new, {
|
|
229
309
|
cwd: process.cwd(),
|
|
230
310
|
mcpServers: [],
|
|
231
311
|
});
|
|
312
|
+
sessionId = session.sessionId;
|
|
313
|
+
configOptions = session.configOptions;
|
|
232
314
|
}
|
|
233
|
-
else {
|
|
234
|
-
throw new Error("Kimi ACP does not advertise session resume support");
|
|
235
|
-
}
|
|
236
|
-
sessionId = options.sessionId;
|
|
237
315
|
}
|
|
238
316
|
else {
|
|
239
317
|
const session = await nextContext.request(methods.agent.session.new, {
|
|
@@ -241,15 +319,10 @@ export async function runKimiAcp(options) {
|
|
|
241
319
|
mcpServers: [],
|
|
242
320
|
});
|
|
243
321
|
sessionId = session.sessionId;
|
|
322
|
+
configOptions = session.configOptions;
|
|
244
323
|
}
|
|
245
324
|
await jsonLine({ type: "thread.started", thread_id: sessionId });
|
|
246
|
-
|
|
247
|
-
await nextContext.request(methods.agent.session.setConfigOption, {
|
|
248
|
-
sessionId,
|
|
249
|
-
configId: "model",
|
|
250
|
-
value: options.model,
|
|
251
|
-
});
|
|
252
|
-
}
|
|
325
|
+
await applySessionConfig(nextContext, provider, sessionId, configOptions, options);
|
|
253
326
|
firstProgress = startFirstProgressWatchdog(() => {
|
|
254
327
|
progressTimedOut = true;
|
|
255
328
|
void cancel();
|
|
@@ -261,7 +334,7 @@ export async function runKimiAcp(options) {
|
|
|
261
334
|
});
|
|
262
335
|
firstProgress.stop();
|
|
263
336
|
if (progressTimedOut) {
|
|
264
|
-
process.stderr.write(
|
|
337
|
+
process.stderr.write(`${displayName} produced no semantic progress within the startup window\n`);
|
|
265
338
|
return 1;
|
|
266
339
|
}
|
|
267
340
|
const usage = result.usage;
|
|
@@ -281,10 +354,10 @@ export async function runKimiAcp(options) {
|
|
|
281
354
|
}
|
|
282
355
|
catch (error) {
|
|
283
356
|
if (progressTimedOut) {
|
|
284
|
-
process.stderr.write(
|
|
357
|
+
process.stderr.write(`${displayName} produced no semantic progress within the startup window\n`);
|
|
285
358
|
return 1;
|
|
286
359
|
}
|
|
287
|
-
if (!acpSemanticProgress && isKimiAuthenticationRequired(error)) {
|
|
360
|
+
if (provider === "kimi" && !acpSemanticProgress && isKimiAuthenticationRequired(error)) {
|
|
288
361
|
firstProgress.stop();
|
|
289
362
|
await stopChild(child);
|
|
290
363
|
process.stderr.write("Kimi ACP requires account login; falling back to configured CLI provider transport\n");
|
|
@@ -323,7 +396,7 @@ export async function runKimiAcp(options) {
|
|
|
323
396
|
}
|
|
324
397
|
return code;
|
|
325
398
|
}
|
|
326
|
-
process.stderr.write(
|
|
399
|
+
process.stderr.write(`${displayName} ACP execution failed: ${safeErrorMessage(error, prompt)}\n`);
|
|
327
400
|
return 1;
|
|
328
401
|
}
|
|
329
402
|
finally {
|
|
@@ -337,19 +410,26 @@ function optionsFromArgv(argv) {
|
|
|
337
410
|
const { values } = parseArgs({
|
|
338
411
|
args: [...argv],
|
|
339
412
|
options: {
|
|
413
|
+
provider: { type: "string", default: "kimi" },
|
|
340
414
|
bin: { type: "string" },
|
|
341
415
|
model: { type: "string" },
|
|
416
|
+
reasoning: { type: "string" },
|
|
342
417
|
session: { type: "string" },
|
|
343
418
|
resume: { type: "boolean", default: false },
|
|
344
419
|
},
|
|
345
420
|
});
|
|
346
421
|
if (!values.bin)
|
|
347
422
|
throw new Error("--bin is required");
|
|
423
|
+
if (values.provider !== "kimi" && values.provider !== "hermes") {
|
|
424
|
+
throw new Error("--provider must be kimi or hermes");
|
|
425
|
+
}
|
|
348
426
|
if (values.resume && !values.session)
|
|
349
427
|
throw new Error("--resume requires --session");
|
|
350
428
|
return {
|
|
429
|
+
provider: values.provider,
|
|
351
430
|
bin: values.bin,
|
|
352
431
|
...(values.model ? { model: values.model } : {}),
|
|
432
|
+
...(values.reasoning ? { reasoning: values.reasoning } : {}),
|
|
353
433
|
...(values.session ? { sessionId: values.session } : {}),
|
|
354
434
|
...(values.resume ? { resume: true } : {}),
|
|
355
435
|
};
|
|
@@ -358,7 +438,7 @@ if (process.argv[1] !== undefined && import.meta.url === pathToFileURL(process.a
|
|
|
358
438
|
runKimiAcp(optionsFromArgv(process.argv.slice(2)))
|
|
359
439
|
.then((code) => { process.exitCode = code; })
|
|
360
440
|
.catch((error) => {
|
|
361
|
-
process.stderr.write(`
|
|
441
|
+
process.stderr.write(`ACP runner failed: ${safeErrorMessage(error, "")}\n`);
|
|
362
442
|
process.exitCode = 1;
|
|
363
443
|
});
|
|
364
444
|
}
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import { once } from "node:events";
|
|
2
|
+
import { createInterface } from "node:readline";
|
|
3
|
+
import { parseArgs } from "node:util";
|
|
4
|
+
import { pathToFileURL } from "node:url";
|
|
5
|
+
import spawn from "cross-spawn";
|
|
6
|
+
import { augmentedPath } from "../runtime-path.js";
|
|
7
|
+
import { buildOpenCodeArgs, createOpenCodeEventDecoder } from "./opencode.js";
|
|
8
|
+
const ERROR_CAP = 2_000;
|
|
9
|
+
async function readPrompt() {
|
|
10
|
+
process.stdin.setEncoding("utf8");
|
|
11
|
+
let prompt = "";
|
|
12
|
+
for await (const chunk of process.stdin)
|
|
13
|
+
prompt += String(chunk);
|
|
14
|
+
if (!prompt)
|
|
15
|
+
throw new Error("OpenCode prompt is empty");
|
|
16
|
+
return prompt;
|
|
17
|
+
}
|
|
18
|
+
async function jsonLine(event) {
|
|
19
|
+
if (process.stdout.write(`${JSON.stringify(event)}\n`))
|
|
20
|
+
return;
|
|
21
|
+
await once(process.stdout, "drain");
|
|
22
|
+
}
|
|
23
|
+
export function isOpenCodeSessionNotFound(message) {
|
|
24
|
+
return /(?:session|conversation).*(?:not found|does not exist|unknown|invalid)|(?:not found|unknown|invalid).*(?:session|conversation)/iu
|
|
25
|
+
.test(message);
|
|
26
|
+
}
|
|
27
|
+
async function runAttempt(options, prompt) {
|
|
28
|
+
const cwd = process.cwd();
|
|
29
|
+
const child = spawn(options.bin, buildOpenCodeArgs({ cwd, ...options }), {
|
|
30
|
+
cwd,
|
|
31
|
+
env: { ...process.env, PATH: augmentedPath(), PWD: cwd },
|
|
32
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
33
|
+
});
|
|
34
|
+
if (!child.stdin || !child.stdout || !child.stderr) {
|
|
35
|
+
throw new Error("OpenCode process did not expose stdio");
|
|
36
|
+
}
|
|
37
|
+
child.stderr.pipe(process.stderr, { end: false });
|
|
38
|
+
const childExit = once(child, "close");
|
|
39
|
+
child.stdin.end(prompt);
|
|
40
|
+
const decoder = createOpenCodeEventDecoder();
|
|
41
|
+
const lines = createInterface({ input: child.stdout });
|
|
42
|
+
const pending = [];
|
|
43
|
+
let emittedSemanticOutput = false;
|
|
44
|
+
for await (const line of lines) {
|
|
45
|
+
let event;
|
|
46
|
+
try {
|
|
47
|
+
event = JSON.parse(line);
|
|
48
|
+
}
|
|
49
|
+
catch {
|
|
50
|
+
continue;
|
|
51
|
+
}
|
|
52
|
+
for (const normalized of decoder.push(event)) {
|
|
53
|
+
const type = normalized.type;
|
|
54
|
+
const semantic = type === "opencode.text_delta"
|
|
55
|
+
|| type === "opencode.tool_call"
|
|
56
|
+
|| type === "opencode.tool_result";
|
|
57
|
+
if (!emittedSemanticOutput && !semantic) {
|
|
58
|
+
pending.push(normalized);
|
|
59
|
+
continue;
|
|
60
|
+
}
|
|
61
|
+
if (!emittedSemanticOutput) {
|
|
62
|
+
emittedSemanticOutput = true;
|
|
63
|
+
for (const buffered of pending.splice(0))
|
|
64
|
+
await jsonLine(buffered);
|
|
65
|
+
}
|
|
66
|
+
await jsonLine(normalized);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
const [exitCode, signal] = await childExit;
|
|
70
|
+
const result = decoder.finish();
|
|
71
|
+
const staleSession = options.sessionId !== undefined
|
|
72
|
+
&& !emittedSemanticOutput
|
|
73
|
+
&& result.error !== undefined
|
|
74
|
+
&& isOpenCodeSessionNotFound(result.error);
|
|
75
|
+
if (staleSession)
|
|
76
|
+
return { code: 1, staleSession: true };
|
|
77
|
+
for (const buffered of pending)
|
|
78
|
+
await jsonLine(buffered);
|
|
79
|
+
if (!result.ok) {
|
|
80
|
+
process.stderr.write(`${result.error?.slice(0, ERROR_CAP)}\n`);
|
|
81
|
+
return { code: 1, staleSession: false };
|
|
82
|
+
}
|
|
83
|
+
if (exitCode !== 0)
|
|
84
|
+
return { code: exitCode ?? (signal ? 128 : 1), staleSession: false };
|
|
85
|
+
await jsonLine({ type: "turn.completed", ...(result.usage ? { usage: result.usage } : {}) });
|
|
86
|
+
return { code: 0, staleSession: false };
|
|
87
|
+
}
|
|
88
|
+
export async function runOpenCode(options) {
|
|
89
|
+
const prompt = await readPrompt();
|
|
90
|
+
const first = await runAttempt(options, prompt);
|
|
91
|
+
if (!first.staleSession)
|
|
92
|
+
return first.code;
|
|
93
|
+
const { sessionId: _staleSessionId, ...freshOptions } = options;
|
|
94
|
+
return (await runAttempt(freshOptions, prompt)).code;
|
|
95
|
+
}
|
|
96
|
+
function optionsFromArgv(argv) {
|
|
97
|
+
const { values } = parseArgs({
|
|
98
|
+
args: [...argv],
|
|
99
|
+
options: {
|
|
100
|
+
bin: { type: "string" },
|
|
101
|
+
model: { type: "string" },
|
|
102
|
+
reasoning: { type: "string" },
|
|
103
|
+
session: { type: "string" },
|
|
104
|
+
},
|
|
105
|
+
});
|
|
106
|
+
if (!values.bin)
|
|
107
|
+
throw new Error("--bin is required");
|
|
108
|
+
return {
|
|
109
|
+
bin: values.bin,
|
|
110
|
+
...(values.model ? { model: values.model } : {}),
|
|
111
|
+
...(values.reasoning ? { reasoning: values.reasoning } : {}),
|
|
112
|
+
...(values.session ? { sessionId: values.session } : {}),
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
if (process.argv[1] !== undefined && import.meta.url === pathToFileURL(process.argv[1]).href) {
|
|
116
|
+
runOpenCode(optionsFromArgv(process.argv.slice(2)))
|
|
117
|
+
.then((code) => { process.exitCode = code; })
|
|
118
|
+
.catch((error) => {
|
|
119
|
+
process.stderr.write(`OpenCode runner failed: ${String(error).slice(0, ERROR_CAP)}\n`);
|
|
120
|
+
process.exitCode = 1;
|
|
121
|
+
});
|
|
122
|
+
}
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
import { execFile } from "node:child_process";
|
|
2
|
+
import { promisify } from "node:util";
|
|
3
|
+
import { isWin } from "../platform.js";
|
|
4
|
+
import { augmentedPath } from "../runtime-path.js";
|
|
5
|
+
const execFileP = promisify(execFile);
|
|
6
|
+
const REQUIRED_RUN_FLAGS = [
|
|
7
|
+
"--format",
|
|
8
|
+
"--dangerously-skip-permissions",
|
|
9
|
+
"--dir",
|
|
10
|
+
"--model",
|
|
11
|
+
"--variant",
|
|
12
|
+
"--session",
|
|
13
|
+
];
|
|
14
|
+
export async function probeOpenCodeRun(signal) {
|
|
15
|
+
try {
|
|
16
|
+
const { stdout, stderr } = await execFileP("opencode", ["run", "--help"], {
|
|
17
|
+
shell: isWin(),
|
|
18
|
+
timeout: 8_000,
|
|
19
|
+
killSignal: "SIGKILL",
|
|
20
|
+
maxBuffer: 1024 * 1024,
|
|
21
|
+
signal,
|
|
22
|
+
env: { ...process.env, PATH: augmentedPath() },
|
|
23
|
+
});
|
|
24
|
+
const help = `${stdout}\n${stderr}`;
|
|
25
|
+
return REQUIRED_RUN_FLAGS.every((flag) => help.includes(flag));
|
|
26
|
+
}
|
|
27
|
+
catch {
|
|
28
|
+
return false;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
export function buildOpenCodeArgs(options) {
|
|
32
|
+
return [
|
|
33
|
+
"run", "--format", "json", "--dangerously-skip-permissions",
|
|
34
|
+
"--dir", options.cwd,
|
|
35
|
+
...(options.model ? ["--model", options.model] : []),
|
|
36
|
+
...(options.reasoning ? ["--variant", options.reasoning] : []),
|
|
37
|
+
...(options.sessionId ? ["--session", options.sessionId] : []),
|
|
38
|
+
];
|
|
39
|
+
}
|
|
40
|
+
function outputText(value) {
|
|
41
|
+
if (typeof value === "string")
|
|
42
|
+
return value;
|
|
43
|
+
if (value === undefined || value === null)
|
|
44
|
+
return "";
|
|
45
|
+
try {
|
|
46
|
+
return JSON.stringify(value);
|
|
47
|
+
}
|
|
48
|
+
catch {
|
|
49
|
+
return String(value);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
function reportedUsage(part) {
|
|
53
|
+
if ((part.cost ?? 0) > 0)
|
|
54
|
+
return true;
|
|
55
|
+
const tokens = part.tokens;
|
|
56
|
+
if (!tokens)
|
|
57
|
+
return false;
|
|
58
|
+
return (tokens.input ?? 0) > 0 || (tokens.output ?? 0) > 0
|
|
59
|
+
|| (tokens.reasoning ?? 0) > 0 || (tokens.total ?? 0) > 0
|
|
60
|
+
|| (tokens.cache?.read ?? 0) > 0 || (tokens.cache?.write ?? 0) > 0;
|
|
61
|
+
}
|
|
62
|
+
export function createOpenCodeEventDecoder() {
|
|
63
|
+
let sessionId = null;
|
|
64
|
+
let announcedSession = null;
|
|
65
|
+
let finalText = "";
|
|
66
|
+
let error = null;
|
|
67
|
+
let openStep = false;
|
|
68
|
+
let stepHasContinuationTool = false;
|
|
69
|
+
let awaitingContinuation = false;
|
|
70
|
+
let stepProducedOutput = false;
|
|
71
|
+
let lastStepVoid = false;
|
|
72
|
+
let sawEvent = false;
|
|
73
|
+
let sawStepFinish = false;
|
|
74
|
+
const usage = {
|
|
75
|
+
input_tokens: 0,
|
|
76
|
+
output_tokens: 0,
|
|
77
|
+
cache_read_input_tokens: 0,
|
|
78
|
+
cache_creation_input_tokens: 0,
|
|
79
|
+
};
|
|
80
|
+
return {
|
|
81
|
+
push(raw) {
|
|
82
|
+
const event = (raw ?? {});
|
|
83
|
+
const out = [];
|
|
84
|
+
if (typeof event.type === "string" && event.type)
|
|
85
|
+
sawEvent = true;
|
|
86
|
+
if (typeof event.sessionID === "string" && event.sessionID) {
|
|
87
|
+
sessionId = event.sessionID;
|
|
88
|
+
if (announcedSession !== sessionId) {
|
|
89
|
+
announcedSession = sessionId;
|
|
90
|
+
out.push({ type: "thread.started", thread_id: sessionId });
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
const part = event.part ?? {};
|
|
94
|
+
if (event.type === "step_start") {
|
|
95
|
+
openStep = true;
|
|
96
|
+
awaitingContinuation = false;
|
|
97
|
+
stepHasContinuationTool = false;
|
|
98
|
+
stepProducedOutput = false;
|
|
99
|
+
out.push({ type: "opencode.step_start" });
|
|
100
|
+
}
|
|
101
|
+
else if (event.type === "text" && typeof part.text === "string" && part.text) {
|
|
102
|
+
finalText += part.text;
|
|
103
|
+
stepProducedOutput = true;
|
|
104
|
+
out.push({ type: "opencode.text_delta", text: part.text });
|
|
105
|
+
}
|
|
106
|
+
else if (event.type === "tool_use") {
|
|
107
|
+
stepProducedOutput = true;
|
|
108
|
+
if (part.metadata?.providerExecuted !== true)
|
|
109
|
+
stepHasContinuationTool = true;
|
|
110
|
+
out.push({
|
|
111
|
+
type: "opencode.tool_call",
|
|
112
|
+
...(part.callID ? { id: part.callID } : {}),
|
|
113
|
+
...(part.tool ? { title: part.tool } : {}),
|
|
114
|
+
...(part.state?.input === undefined ? {} : { input: part.state.input }),
|
|
115
|
+
});
|
|
116
|
+
if (part.state?.status === "completed" || part.state?.status === "error") {
|
|
117
|
+
const content = part.state.status === "error" && part.state.error
|
|
118
|
+
? part.state.error
|
|
119
|
+
: outputText(part.state.output);
|
|
120
|
+
out.push({
|
|
121
|
+
type: "opencode.tool_result",
|
|
122
|
+
...(part.callID ? { id: part.callID } : {}),
|
|
123
|
+
status: part.state.status,
|
|
124
|
+
...(content ? { content } : {}),
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
else if (event.type === "step_finish") {
|
|
129
|
+
openStep = false;
|
|
130
|
+
sawStepFinish = true;
|
|
131
|
+
awaitingContinuation = part.reason === "tool-calls"
|
|
132
|
+
|| (Boolean(part.reason) && stepHasContinuationTool);
|
|
133
|
+
stepHasContinuationTool = false;
|
|
134
|
+
if (reportedUsage(part))
|
|
135
|
+
stepProducedOutput = true;
|
|
136
|
+
lastStepVoid = !stepProducedOutput;
|
|
137
|
+
const tokens = part.tokens;
|
|
138
|
+
if (tokens) {
|
|
139
|
+
usage.input_tokens += tokens.input ?? 0;
|
|
140
|
+
usage.output_tokens += tokens.output ?? 0;
|
|
141
|
+
usage.cache_read_input_tokens += tokens.cache?.read ?? 0;
|
|
142
|
+
usage.cache_creation_input_tokens += tokens.cache?.write ?? 0;
|
|
143
|
+
}
|
|
144
|
+
out.push({
|
|
145
|
+
type: "opencode.step_finish",
|
|
146
|
+
...(part.reason ? { stop_reason: part.reason } : {}),
|
|
147
|
+
...(tokens ? { usage: {
|
|
148
|
+
input_tokens: tokens.input ?? 0,
|
|
149
|
+
output_tokens: tokens.output ?? 0,
|
|
150
|
+
cache_read_input_tokens: tokens.cache?.read ?? 0,
|
|
151
|
+
cache_creation_input_tokens: tokens.cache?.write ?? 0,
|
|
152
|
+
} } : {}),
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
else if (event.type === "error") {
|
|
156
|
+
error = event.error?.data?.message || event.error?.name || "unknown OpenCode error";
|
|
157
|
+
out.push({ type: "opencode.error", message: error });
|
|
158
|
+
}
|
|
159
|
+
return out;
|
|
160
|
+
},
|
|
161
|
+
finish() {
|
|
162
|
+
const structuralError = openStep
|
|
163
|
+
? "OpenCode stream ended while a step was still open"
|
|
164
|
+
: awaitingContinuation
|
|
165
|
+
? "OpenCode stream ended before the required continuation"
|
|
166
|
+
: lastStepVoid
|
|
167
|
+
? "OpenCode stream ended on an empty step"
|
|
168
|
+
: !sawEvent || !sawStepFinish
|
|
169
|
+
? "OpenCode stream ended with no events proving completion"
|
|
170
|
+
: null;
|
|
171
|
+
const failure = error ?? structuralError;
|
|
172
|
+
return {
|
|
173
|
+
ok: failure === null,
|
|
174
|
+
...(failure === null ? {} : { error: failure }),
|
|
175
|
+
finalText,
|
|
176
|
+
sessionId,
|
|
177
|
+
...(sawStepFinish ? { usage } : {}),
|
|
178
|
+
};
|
|
179
|
+
},
|
|
180
|
+
};
|
|
181
|
+
}
|