crewx-agent-cli 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 +46 -6
- package/dist/acp-worker.d.ts +3 -0
- package/dist/acp-worker.d.ts.map +1 -0
- package/dist/acp-worker.js +176 -0
- package/dist/acp-worker.js.map +1 -0
- package/dist/adapters.d.ts +13 -10
- package/dist/adapters.d.ts.map +1 -1
- package/dist/adapters.js +571 -311
- package/dist/adapters.js.map +1 -1
- package/dist/assignment-scheduler.d.ts +29 -0
- package/dist/assignment-scheduler.d.ts.map +1 -0
- package/dist/assignment-scheduler.js +137 -0
- package/dist/assignment-scheduler.js.map +1 -0
- package/dist/daemon.d.ts +1 -0
- package/dist/daemon.d.ts.map +1 -1
- package/dist/daemon.js +185 -122
- package/dist/daemon.js.map +1 -1
- package/dist/index.d.ts +7 -7
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +291 -221
- package/dist/index.js.map +1 -1
- package/package.json +7 -4
package/dist/adapters.js
CHANGED
|
@@ -1,31 +1,55 @@
|
|
|
1
|
-
import { spawn } from
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
1
|
+
import { spawn, } from "node:child_process";
|
|
2
|
+
import { createHash } from "node:crypto";
|
|
3
|
+
import { once } from "node:events";
|
|
4
|
+
import { chmod, mkdtemp, realpath, rm, writeFile } from "node:fs/promises";
|
|
5
|
+
import { tmpdir } from "node:os";
|
|
6
|
+
import { join } from "node:path";
|
|
7
|
+
import { fileURLToPath } from "node:url";
|
|
8
|
+
import { ACPX_HARNESSES, AgentAdapterSchema, } from "crewx-agent-protocol";
|
|
9
|
+
import { CLI_VERSION, DEFAULT_RUN_TIMEOUT_MS, MAX_AGENT_OUTPUT_BYTES, } from "./constants.js";
|
|
10
|
+
import { CliError } from "./errors.js";
|
|
9
11
|
const DEFAULT_RUNTIME_PROBE_TIMEOUT_MS = 5_000;
|
|
10
12
|
const OPENCLAW_RUNTIME_PROBE_TIMEOUT_MS = 30_000;
|
|
11
13
|
const OPENCLAW_SETUP_TIMEOUT_MS = 60_000;
|
|
14
|
+
const ACPX_PROBE_COMMANDS = {
|
|
15
|
+
gemini: "gemini",
|
|
16
|
+
cursor: "cursor-agent",
|
|
17
|
+
copilot: "copilot",
|
|
18
|
+
droid: "droid",
|
|
19
|
+
"fast-agent": "uvx",
|
|
20
|
+
"grok-build": "grok",
|
|
21
|
+
iflow: "iflow",
|
|
22
|
+
kilocode: "npx",
|
|
23
|
+
kimi: "kimi",
|
|
24
|
+
kiro: "kiro-cli-chat",
|
|
25
|
+
mux: "npx",
|
|
26
|
+
opencode: "npx",
|
|
27
|
+
pool: "pool",
|
|
28
|
+
qoder: "qodercli",
|
|
29
|
+
qwen: "qwen",
|
|
30
|
+
trae: "traecli",
|
|
31
|
+
zeroclaw: "zeroclaw",
|
|
32
|
+
};
|
|
12
33
|
export function adapterProbePlan(adapter) {
|
|
13
|
-
return adapter ===
|
|
14
|
-
? { args: [
|
|
15
|
-
: adapter ===
|
|
16
|
-
? {
|
|
17
|
-
|
|
34
|
+
return adapter === "hermes"
|
|
35
|
+
? { args: ["acp", "--check"], timeoutMs: DEFAULT_RUNTIME_PROBE_TIMEOUT_MS }
|
|
36
|
+
: adapter === "openclaw"
|
|
37
|
+
? {
|
|
38
|
+
args: ["agents", "list", "--json"],
|
|
39
|
+
timeoutMs: OPENCLAW_RUNTIME_PROBE_TIMEOUT_MS,
|
|
40
|
+
}
|
|
41
|
+
: { args: ["--version"], timeoutMs: DEFAULT_RUNTIME_PROBE_TIMEOUT_MS };
|
|
18
42
|
}
|
|
19
43
|
const CLAUDE_STANDARD_CREWX_TOOLS = [
|
|
20
|
-
|
|
21
|
-
|
|
44
|
+
"WebSearch",
|
|
45
|
+
"WebFetch",
|
|
22
46
|
'Bash(node "$CREWX_CLI_PATH" task *)',
|
|
23
47
|
'Bash(node "$CREWX_CLI_PATH" doc *)',
|
|
24
48
|
'Bash(node "$CREWX_CLI_PATH" memory *)',
|
|
25
49
|
'Bash(node "$CREWX_CLI_PATH" integration *)',
|
|
26
50
|
];
|
|
27
51
|
function terminateChild(child, signal) {
|
|
28
|
-
if (process.platform !==
|
|
52
|
+
if (process.platform !== "win32" && child.pid) {
|
|
29
53
|
try {
|
|
30
54
|
process.kill(-child.pid, signal);
|
|
31
55
|
return;
|
|
@@ -45,7 +69,7 @@ function observeChild(child) {
|
|
|
45
69
|
let closed = false;
|
|
46
70
|
return {
|
|
47
71
|
close: new Promise((resolve) => {
|
|
48
|
-
child.once(
|
|
72
|
+
child.once("close", (exitCode, signal) => {
|
|
49
73
|
closed = true;
|
|
50
74
|
resolve([exitCode, signal]);
|
|
51
75
|
});
|
|
@@ -61,7 +85,7 @@ function destroyChildPipes(child) {
|
|
|
61
85
|
async function terminateAndWait(child, lifecycle, graceMs) {
|
|
62
86
|
if (lifecycle.isClosed() || child.exitCode !== null)
|
|
63
87
|
return;
|
|
64
|
-
terminateChild(child,
|
|
88
|
+
terminateChild(child, "SIGTERM");
|
|
65
89
|
let graceTimer;
|
|
66
90
|
await Promise.race([
|
|
67
91
|
lifecycle.close,
|
|
@@ -73,7 +97,7 @@ async function terminateAndWait(child, lifecycle, graceMs) {
|
|
|
73
97
|
clearTimeout(graceTimer);
|
|
74
98
|
if (lifecycle.isClosed() || child.exitCode !== null)
|
|
75
99
|
return;
|
|
76
|
-
terminateChild(child,
|
|
100
|
+
terminateChild(child, "SIGKILL");
|
|
77
101
|
let killTimer;
|
|
78
102
|
await Promise.race([
|
|
79
103
|
lifecycle.close,
|
|
@@ -89,7 +113,7 @@ async function terminateAndWait(child, lifecycle, graceMs) {
|
|
|
89
113
|
export function parseAdapter(value) {
|
|
90
114
|
const result = AgentAdapterSchema.safeParse(value);
|
|
91
115
|
if (!result.success) {
|
|
92
|
-
throw new CliError(`Unknown adapter "${value}".
|
|
116
|
+
throw new CliError(`Unknown adapter "${value}". Run \`crewx doctor --json\` to list supported harnesses.`);
|
|
93
117
|
}
|
|
94
118
|
return result.data;
|
|
95
119
|
}
|
|
@@ -97,105 +121,107 @@ export function parseAdapter(value) {
|
|
|
97
121
|
export function buildAdapterInvocation(adapter, profile = {}, codingCommand) {
|
|
98
122
|
if (codingCommand?.trim())
|
|
99
123
|
return buildCustomInvocation(codingCommand);
|
|
100
|
-
const modelArgs = profile.model ? [
|
|
101
|
-
const restricted = profile.chatOnly === true || profile.permissionPreset ===
|
|
102
|
-
const codexPermissionArgs = profile.permissionPreset ===
|
|
103
|
-
? [
|
|
124
|
+
const modelArgs = profile.model ? ["--model", profile.model] : [];
|
|
125
|
+
const restricted = profile.chatOnly === true || profile.permissionPreset === "read_only";
|
|
126
|
+
const codexPermissionArgs = profile.permissionPreset === "full_access"
|
|
127
|
+
? ["--dangerously-bypass-approvals-and-sandbox"]
|
|
104
128
|
: [
|
|
105
|
-
|
|
129
|
+
"-c",
|
|
106
130
|
'approval_policy="never"',
|
|
107
|
-
|
|
108
|
-
restricted ?
|
|
131
|
+
"--sandbox",
|
|
132
|
+
restricted ? "read-only" : "workspace-write",
|
|
109
133
|
];
|
|
110
134
|
switch (adapter) {
|
|
111
|
-
case
|
|
135
|
+
case "codex":
|
|
112
136
|
return {
|
|
113
|
-
command:
|
|
137
|
+
command: "codex",
|
|
114
138
|
args: [
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
139
|
+
"--search",
|
|
140
|
+
"exec",
|
|
141
|
+
"--json",
|
|
142
|
+
"--color",
|
|
143
|
+
"never",
|
|
144
|
+
"--skip-git-repo-check",
|
|
145
|
+
"--ephemeral",
|
|
122
146
|
...modelArgs,
|
|
123
147
|
...codexPermissionArgs,
|
|
124
|
-
|
|
148
|
+
"-",
|
|
125
149
|
],
|
|
126
|
-
output:
|
|
127
|
-
prompt: { type:
|
|
150
|
+
output: "jsonl",
|
|
151
|
+
prompt: { type: "stdin" },
|
|
128
152
|
};
|
|
129
|
-
case
|
|
153
|
+
case "claude":
|
|
130
154
|
return {
|
|
131
|
-
command:
|
|
155
|
+
command: "claude",
|
|
132
156
|
args: [
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
157
|
+
"--print",
|
|
158
|
+
"--output-format",
|
|
159
|
+
"stream-json",
|
|
160
|
+
"--verbose",
|
|
161
|
+
"--no-session-persistence",
|
|
138
162
|
...modelArgs,
|
|
139
|
-
...(!restricted && profile.permissionPreset !==
|
|
140
|
-
? [
|
|
163
|
+
...(!restricted && profile.permissionPreset !== "full_access"
|
|
164
|
+
? ["--allowedTools", ...CLAUDE_STANDARD_CREWX_TOOLS]
|
|
141
165
|
: []),
|
|
142
166
|
...(restricted
|
|
143
|
-
? [
|
|
144
|
-
: profile.permissionPreset ===
|
|
145
|
-
? [
|
|
146
|
-
: [
|
|
167
|
+
? ["--permission-mode", "plan"]
|
|
168
|
+
: profile.permissionPreset === "full_access"
|
|
169
|
+
? ["--dangerously-skip-permissions"]
|
|
170
|
+
: ["--permission-mode", "acceptEdits"]),
|
|
147
171
|
],
|
|
148
|
-
output:
|
|
149
|
-
prompt: { type:
|
|
172
|
+
output: "jsonl",
|
|
173
|
+
prompt: { type: "stdin" },
|
|
150
174
|
};
|
|
151
|
-
case
|
|
175
|
+
case "pi":
|
|
152
176
|
return {
|
|
153
|
-
command:
|
|
177
|
+
command: "pi",
|
|
154
178
|
args: [
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
179
|
+
"--mode",
|
|
180
|
+
"json",
|
|
181
|
+
"--print",
|
|
182
|
+
"--no-approve",
|
|
183
|
+
"--no-session",
|
|
160
184
|
...modelArgs,
|
|
161
|
-
...(restricted ? [
|
|
185
|
+
...(restricted ? ["--tools", "read,grep,find,ls"] : []),
|
|
162
186
|
],
|
|
163
|
-
output:
|
|
164
|
-
prompt: { type:
|
|
187
|
+
output: "jsonl",
|
|
188
|
+
prompt: { type: "stdin" },
|
|
165
189
|
};
|
|
166
|
-
case
|
|
190
|
+
case "hermes":
|
|
167
191
|
return {
|
|
168
|
-
command:
|
|
169
|
-
args: [
|
|
170
|
-
output:
|
|
171
|
-
prompt: { type:
|
|
192
|
+
command: "hermes",
|
|
193
|
+
args: ["acp"],
|
|
194
|
+
output: "jsonl",
|
|
195
|
+
prompt: { type: "stdin" },
|
|
172
196
|
};
|
|
173
|
-
case
|
|
197
|
+
case "openclaw":
|
|
174
198
|
if (!profile.runtimeAgentId?.trim()) {
|
|
175
|
-
throw new CliError(
|
|
199
|
+
throw new CliError("OpenClaw requires an explicit configured agent ID.");
|
|
176
200
|
}
|
|
177
201
|
return {
|
|
178
|
-
command:
|
|
202
|
+
command: "openclaw",
|
|
179
203
|
args: [
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
204
|
+
"agent",
|
|
205
|
+
"--local",
|
|
206
|
+
"--agent",
|
|
183
207
|
profile.runtimeAgentId.trim(),
|
|
184
|
-
|
|
185
|
-
profile.sessionKey ??
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
208
|
+
"--session-key",
|
|
209
|
+
profile.sessionKey ?? "main",
|
|
210
|
+
"--json",
|
|
211
|
+
"--timeout",
|
|
212
|
+
"0",
|
|
189
213
|
...modelArgs,
|
|
190
214
|
],
|
|
191
|
-
output:
|
|
192
|
-
prompt: { type:
|
|
215
|
+
output: "json",
|
|
216
|
+
prompt: { type: "file", flag: "--message-file" },
|
|
193
217
|
};
|
|
218
|
+
default:
|
|
219
|
+
throw new CliError(`${adapter} is available through the ACP runtime, not the native command adapter.`);
|
|
194
220
|
}
|
|
195
221
|
}
|
|
196
222
|
export function parseCodingCommand(value) {
|
|
197
223
|
const tokens = [];
|
|
198
|
-
let token =
|
|
224
|
+
let token = "";
|
|
199
225
|
let quote;
|
|
200
226
|
let escaped = false;
|
|
201
227
|
let started = false;
|
|
@@ -206,25 +232,25 @@ export function parseCodingCommand(value) {
|
|
|
206
232
|
started = true;
|
|
207
233
|
continue;
|
|
208
234
|
}
|
|
209
|
-
if (character ===
|
|
235
|
+
if (character === "\\" && quote !== "single") {
|
|
210
236
|
escaped = true;
|
|
211
237
|
started = true;
|
|
212
238
|
continue;
|
|
213
239
|
}
|
|
214
|
-
if (character === "'" && quote !==
|
|
215
|
-
quote = quote ===
|
|
240
|
+
if (character === "'" && quote !== "double") {
|
|
241
|
+
quote = quote === "single" ? undefined : "single";
|
|
216
242
|
started = true;
|
|
217
243
|
continue;
|
|
218
244
|
}
|
|
219
|
-
if (character === '"' && quote !==
|
|
220
|
-
quote = quote ===
|
|
245
|
+
if (character === '"' && quote !== "single") {
|
|
246
|
+
quote = quote === "double" ? undefined : "double";
|
|
221
247
|
started = true;
|
|
222
248
|
continue;
|
|
223
249
|
}
|
|
224
250
|
if (/\s/.test(character) && !quote) {
|
|
225
251
|
if (started) {
|
|
226
252
|
tokens.push(token);
|
|
227
|
-
token =
|
|
253
|
+
token = "";
|
|
228
254
|
started = false;
|
|
229
255
|
}
|
|
230
256
|
continue;
|
|
@@ -233,63 +259,66 @@ export function parseCodingCommand(value) {
|
|
|
233
259
|
started = true;
|
|
234
260
|
}
|
|
235
261
|
if (escaped || quote)
|
|
236
|
-
throw new CliError(
|
|
262
|
+
throw new CliError("The coding command contains an unfinished quote or escape.");
|
|
237
263
|
if (started)
|
|
238
264
|
tokens.push(token);
|
|
239
265
|
if (tokens.length === 0 || !tokens[0])
|
|
240
|
-
throw new CliError(
|
|
266
|
+
throw new CliError("The coding command cannot be empty.");
|
|
241
267
|
return tokens;
|
|
242
268
|
}
|
|
243
269
|
function buildCustomInvocation(value) {
|
|
244
270
|
const tokens = parseCodingCommand(value);
|
|
245
271
|
const command = tokens[0];
|
|
246
272
|
const args = tokens.slice(1);
|
|
247
|
-
const promptIndexes = args.flatMap((argument, index) =>
|
|
273
|
+
const promptIndexes = args.flatMap((argument, index) => argument === "{prompt}" ? [index] : []);
|
|
248
274
|
if (promptIndexes.length > 1)
|
|
249
|
-
throw new CliError(
|
|
275
|
+
throw new CliError("Use {prompt} at most once in the coding command.");
|
|
250
276
|
const index = promptIndexes[0] ?? args.length;
|
|
251
277
|
if (promptIndexes.length === 1)
|
|
252
278
|
args.splice(index, 1);
|
|
253
279
|
return {
|
|
254
280
|
command,
|
|
255
281
|
args,
|
|
256
|
-
output:
|
|
257
|
-
prompt: { type:
|
|
282
|
+
output: "text",
|
|
283
|
+
prompt: { type: "argument", index },
|
|
258
284
|
};
|
|
259
285
|
}
|
|
260
286
|
function contentText(value) {
|
|
261
|
-
if (typeof value ===
|
|
287
|
+
if (typeof value === "string" && value.trim())
|
|
262
288
|
return value;
|
|
263
289
|
if (!Array.isArray(value))
|
|
264
290
|
return undefined;
|
|
265
291
|
const parts = [];
|
|
266
292
|
for (const part of value) {
|
|
267
|
-
if (typeof part ===
|
|
293
|
+
if (typeof part === "string") {
|
|
268
294
|
parts.push(part);
|
|
269
295
|
}
|
|
270
|
-
else if (typeof part ===
|
|
296
|
+
else if (typeof part === "object" && part !== null) {
|
|
271
297
|
const record = part;
|
|
272
|
-
if ((record.type ===
|
|
298
|
+
if ((record.type === "text" || record.type === "output_text") &&
|
|
299
|
+
typeof record.text === "string") {
|
|
273
300
|
parts.push(record.text);
|
|
274
301
|
}
|
|
275
302
|
}
|
|
276
303
|
}
|
|
277
|
-
return parts.length > 0 ? parts.join(
|
|
304
|
+
return parts.length > 0 ? parts.join("") : undefined;
|
|
278
305
|
}
|
|
279
306
|
function finalPiAssistantMessage(event) {
|
|
280
307
|
// Pi's message_start/message_update/message_end events all carry the current,
|
|
281
308
|
// cumulative assistant snapshot. Publishing those snapshots produces a new
|
|
282
309
|
// CrewX message for every token. agent_end is Pi's single terminal event for
|
|
283
310
|
// a run, and its messages array contains the completed agent turns.
|
|
284
|
-
if (event.type !==
|
|
311
|
+
if (event.type !== "agent_end" ||
|
|
312
|
+
event.willRetry === true ||
|
|
313
|
+
!Array.isArray(event.messages)) {
|
|
285
314
|
return undefined;
|
|
286
315
|
}
|
|
287
316
|
for (let index = event.messages.length - 1; index >= 0; index -= 1) {
|
|
288
317
|
const candidate = event.messages[index];
|
|
289
|
-
if (typeof candidate !==
|
|
318
|
+
if (typeof candidate !== "object" || candidate === null)
|
|
290
319
|
continue;
|
|
291
320
|
const message = candidate;
|
|
292
|
-
if (message.role !==
|
|
321
|
+
if (message.role !== "assistant")
|
|
293
322
|
continue;
|
|
294
323
|
const text = contentText(message.content);
|
|
295
324
|
if (text)
|
|
@@ -305,45 +334,50 @@ export function parseAdapterLine(adapter, line) {
|
|
|
305
334
|
catch {
|
|
306
335
|
return { raw: line };
|
|
307
336
|
}
|
|
308
|
-
if (typeof raw !==
|
|
337
|
+
if (typeof raw !== "object" || raw === null)
|
|
309
338
|
return { raw };
|
|
310
339
|
const event = raw;
|
|
311
|
-
const eventType = typeof event.type ===
|
|
340
|
+
const eventType = typeof event.type === "string" ? event.type : undefined;
|
|
312
341
|
let message;
|
|
313
342
|
let role;
|
|
314
|
-
if (adapter ===
|
|
315
|
-
const item = typeof event.item ===
|
|
316
|
-
|
|
343
|
+
if (adapter === "codex") {
|
|
344
|
+
const item = typeof event.item === "object" && event.item !== null
|
|
345
|
+
? event.item
|
|
346
|
+
: undefined;
|
|
347
|
+
if (item?.type === "agent_message") {
|
|
317
348
|
message = contentText(item.text) ?? contentText(item.content);
|
|
318
|
-
role =
|
|
349
|
+
role = "assistant";
|
|
319
350
|
}
|
|
320
|
-
else if (eventType ===
|
|
351
|
+
else if (eventType === "agent_message") {
|
|
321
352
|
message = contentText(event.message) ?? contentText(event.text);
|
|
322
|
-
role =
|
|
353
|
+
role = "assistant";
|
|
323
354
|
}
|
|
324
355
|
}
|
|
325
|
-
else if (adapter ===
|
|
326
|
-
const messageObject = typeof event.message ===
|
|
356
|
+
else if (adapter === "claude") {
|
|
357
|
+
const messageObject = typeof event.message === "object" && event.message !== null
|
|
327
358
|
? event.message
|
|
328
359
|
: undefined;
|
|
329
|
-
if (eventType ===
|
|
360
|
+
if (eventType === "result" && event.is_error !== true) {
|
|
330
361
|
message = contentText(event.result);
|
|
331
|
-
role =
|
|
362
|
+
role = "assistant";
|
|
332
363
|
}
|
|
333
|
-
else if (eventType ===
|
|
364
|
+
else if (eventType === "assistant" ||
|
|
365
|
+
messageObject?.role === "assistant") {
|
|
334
366
|
message = contentText(messageObject?.content ?? event.content);
|
|
335
|
-
role =
|
|
367
|
+
role = "assistant";
|
|
336
368
|
}
|
|
337
|
-
else if (eventType ===
|
|
338
|
-
const delta = typeof event.delta ===
|
|
369
|
+
else if (eventType === "content_block_delta") {
|
|
370
|
+
const delta = typeof event.delta === "object" && event.delta !== null
|
|
371
|
+
? event.delta
|
|
372
|
+
: undefined;
|
|
339
373
|
message = contentText(delta?.text);
|
|
340
|
-
role =
|
|
374
|
+
role = "assistant";
|
|
341
375
|
}
|
|
342
376
|
}
|
|
343
|
-
else if (adapter ===
|
|
377
|
+
else if (adapter === "pi") {
|
|
344
378
|
message = finalPiAssistantMessage(event);
|
|
345
379
|
if (message) {
|
|
346
|
-
role =
|
|
380
|
+
role = "assistant";
|
|
347
381
|
}
|
|
348
382
|
}
|
|
349
383
|
return {
|
|
@@ -354,7 +388,7 @@ export function parseAdapterLine(adapter, line) {
|
|
|
354
388
|
};
|
|
355
389
|
}
|
|
356
390
|
function record(value) {
|
|
357
|
-
return typeof value ===
|
|
391
|
+
return typeof value === "object" && value !== null && !Array.isArray(value)
|
|
358
392
|
? value
|
|
359
393
|
: undefined;
|
|
360
394
|
}
|
|
@@ -364,7 +398,7 @@ function openClawError(code, message, cause) {
|
|
|
364
398
|
...(cause !== undefined ? { cause } : {}),
|
|
365
399
|
});
|
|
366
400
|
}
|
|
367
|
-
function publicOpenClawDiagnostic(value) {
|
|
401
|
+
export function publicOpenClawDiagnostic(value) {
|
|
368
402
|
return value
|
|
369
403
|
.replace(/(^|[\s"'`=:(])(?:\/(?!\/)[^\s"'`),;]+)+/g, "$1<local-path>")
|
|
370
404
|
.replace(/(^|[\s"'`=:(])[A-Za-z]:\\[^\s"'`),;]+/g, "$1<local-path>");
|
|
@@ -374,34 +408,34 @@ async function resolvedPath(value, label) {
|
|
|
374
408
|
return await realpath(value);
|
|
375
409
|
}
|
|
376
410
|
catch (error) {
|
|
377
|
-
throw openClawError(
|
|
411
|
+
throw openClawError("openclaw_workspace_unavailable", `${label} does not resolve to an accessible local directory.`, error);
|
|
378
412
|
}
|
|
379
413
|
}
|
|
380
414
|
async function openClawJsonProbe(args, options, timeoutMs = OPENCLAW_RUNTIME_PROBE_TIMEOUT_MS) {
|
|
381
415
|
const spawnProcess = options.spawnProcess ?? spawn;
|
|
382
416
|
let child;
|
|
383
417
|
try {
|
|
384
|
-
child = spawnProcess(
|
|
418
|
+
child = spawnProcess("openclaw", args, {
|
|
385
419
|
cwd: options.cwd,
|
|
386
420
|
env: runtimeEnvironment(options.environment),
|
|
387
421
|
shell: false,
|
|
388
|
-
detached: process.platform !==
|
|
389
|
-
stdio: [
|
|
422
|
+
detached: process.platform !== "win32",
|
|
423
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
390
424
|
});
|
|
391
425
|
}
|
|
392
426
|
catch (error) {
|
|
393
|
-
throw openClawError(
|
|
427
|
+
throw openClawError("openclaw_unavailable", "Unable to start openclaw. Is it installed and on PATH?", error);
|
|
394
428
|
}
|
|
395
|
-
let stdout =
|
|
396
|
-
let stderr =
|
|
429
|
+
let stdout = "";
|
|
430
|
+
let stderr = "";
|
|
397
431
|
let bytes = 0;
|
|
398
432
|
let exceeded = false;
|
|
399
433
|
let forceKill;
|
|
400
434
|
const stopProbe = () => {
|
|
401
435
|
if (child.exitCode !== null)
|
|
402
436
|
return;
|
|
403
|
-
terminateChild(child,
|
|
404
|
-
forceKill ??= setTimeout(() => terminateChild(child,
|
|
437
|
+
terminateChild(child, "SIGTERM");
|
|
438
|
+
forceKill ??= setTimeout(() => terminateChild(child, "SIGKILL"), 5_000);
|
|
405
439
|
forceKill.unref();
|
|
406
440
|
};
|
|
407
441
|
const consume = (stream, chunk) => {
|
|
@@ -413,14 +447,14 @@ async function openClawJsonProbe(args, options, timeoutMs = OPENCLAW_RUNTIME_PRO
|
|
|
413
447
|
stopProbe();
|
|
414
448
|
return;
|
|
415
449
|
}
|
|
416
|
-
if (stream ===
|
|
450
|
+
if (stream === "stdout")
|
|
417
451
|
stdout += chunk.toString();
|
|
418
452
|
else
|
|
419
453
|
stderr += chunk.toString();
|
|
420
454
|
};
|
|
421
|
-
child.stdout.on(
|
|
422
|
-
child.stderr.on(
|
|
423
|
-
child.stdin.on(
|
|
455
|
+
child.stdout.on("data", (chunk) => consume("stdout", chunk));
|
|
456
|
+
child.stderr.on("data", (chunk) => consume("stderr", chunk));
|
|
457
|
+
child.stdin.on("error", () => undefined);
|
|
424
458
|
child.stdin.end();
|
|
425
459
|
let timedOut = false;
|
|
426
460
|
const timeout = setTimeout(() => {
|
|
@@ -430,25 +464,25 @@ async function openClawJsonProbe(args, options, timeoutMs = OPENCLAW_RUNTIME_PRO
|
|
|
430
464
|
timeout.unref();
|
|
431
465
|
try {
|
|
432
466
|
const [code] = (await Promise.race([
|
|
433
|
-
once(child,
|
|
434
|
-
once(child,
|
|
467
|
+
once(child, "close"),
|
|
468
|
+
once(child, "error").then(([error]) => {
|
|
435
469
|
throw error;
|
|
436
470
|
}),
|
|
437
471
|
]));
|
|
438
472
|
if (timedOut) {
|
|
439
|
-
throw openClawError(
|
|
473
|
+
throw openClawError("openclaw_probe_timeout", `OpenClaw preflight timed out while running ${[args[0], args[1]].filter(Boolean).join(" ")}.`);
|
|
440
474
|
}
|
|
441
475
|
if (exceeded) {
|
|
442
|
-
throw openClawError(
|
|
476
|
+
throw openClawError("openclaw_probe_output_too_large", "OpenClaw preflight exceeded its 1048576 byte output limit.");
|
|
443
477
|
}
|
|
444
478
|
if (code !== 0) {
|
|
445
|
-
throw openClawError(
|
|
479
|
+
throw openClawError("openclaw_probe_failed", `OpenClaw preflight failed: ${publicOpenClawDiagnostic(stderr.trim()) || `exit ${String(code)}`}`);
|
|
446
480
|
}
|
|
447
481
|
try {
|
|
448
482
|
return JSON.parse(stdout);
|
|
449
483
|
}
|
|
450
484
|
catch (error) {
|
|
451
|
-
throw openClawError(
|
|
485
|
+
throw openClawError("openclaw_probe_invalid_json", "OpenClaw preflight returned invalid JSON.", error);
|
|
452
486
|
}
|
|
453
487
|
}
|
|
454
488
|
finally {
|
|
@@ -462,117 +496,123 @@ async function openClawJsonProbe(args, options, timeoutMs = OPENCLAW_RUNTIME_PRO
|
|
|
462
496
|
export async function verifyOpenClawRuntime(options) {
|
|
463
497
|
const agentId = options.profile.runtimeAgentId?.trim();
|
|
464
498
|
if (!agentId) {
|
|
465
|
-
throw openClawError(
|
|
499
|
+
throw openClawError("openclaw_agent_id_missing", "OpenClaw requires an explicit configured agent ID.");
|
|
466
500
|
}
|
|
467
|
-
const desiredWorkspace = await resolvedPath(options.cwd,
|
|
468
|
-
let agents = await openClawJsonProbe([
|
|
501
|
+
const desiredWorkspace = await resolvedPath(options.cwd, "CrewX assignment directory");
|
|
502
|
+
let agents = await openClawJsonProbe(["agents", "list", "--json"], options);
|
|
469
503
|
if (!Array.isArray(agents)) {
|
|
470
|
-
throw openClawError(
|
|
504
|
+
throw openClawError("openclaw_agent_list_invalid", "OpenClaw returned an invalid agent list.");
|
|
471
505
|
}
|
|
472
506
|
let selected = agents.map(record).find((agent) => agent?.id === agentId);
|
|
473
|
-
if (!selected && agentId.startsWith(
|
|
507
|
+
if (!selected && agentId.startsWith("crewx-")) {
|
|
474
508
|
await openClawJsonProbe([
|
|
475
|
-
|
|
476
|
-
|
|
509
|
+
"agents",
|
|
510
|
+
"add",
|
|
477
511
|
agentId,
|
|
478
|
-
|
|
512
|
+
"--workspace",
|
|
479
513
|
desiredWorkspace,
|
|
480
|
-
|
|
481
|
-
|
|
514
|
+
"--non-interactive",
|
|
515
|
+
"--json",
|
|
482
516
|
], options, OPENCLAW_SETUP_TIMEOUT_MS);
|
|
483
|
-
agents = await openClawJsonProbe([
|
|
517
|
+
agents = await openClawJsonProbe(["agents", "list", "--json"], options);
|
|
484
518
|
if (!Array.isArray(agents)) {
|
|
485
|
-
throw openClawError(
|
|
519
|
+
throw openClawError("openclaw_agent_list_invalid", "OpenClaw returned an invalid agent list after setup.");
|
|
486
520
|
}
|
|
487
521
|
selected = agents.map(record).find((agent) => agent?.id === agentId);
|
|
488
522
|
if (!selected) {
|
|
489
|
-
throw openClawError(
|
|
523
|
+
throw openClawError("openclaw_agent_setup_failed", `CrewX created OpenClaw agent "${agentId}", but OpenClaw did not return it.`);
|
|
490
524
|
}
|
|
491
525
|
}
|
|
492
526
|
if (!selected) {
|
|
493
|
-
throw openClawError(
|
|
527
|
+
throw openClawError("openclaw_agent_missing", `OpenClaw agent "${agentId}" is not configured. Run \`openclaw agents list --json\` to choose one.`);
|
|
494
528
|
}
|
|
495
|
-
if (typeof selected.workspace !==
|
|
496
|
-
throw openClawError(
|
|
529
|
+
if (typeof selected.workspace !== "string" || !selected.workspace.trim()) {
|
|
530
|
+
throw openClawError("openclaw_workspace_missing", `OpenClaw agent "${agentId}" does not report a workspace.`);
|
|
497
531
|
}
|
|
498
532
|
const configuredWorkspace = await resolvedPath(selected.workspace, `OpenClaw agent "${agentId}" workspace`);
|
|
499
533
|
if (configuredWorkspace !== desiredWorkspace) {
|
|
500
|
-
throw openClawError(
|
|
534
|
+
throw openClawError("openclaw_workspace_mismatch", `OpenClaw agent "${agentId}" is bound to a different local workspace than the CrewX-approved folder. OpenClaw has no per-run --cwd; create or select a dedicated OpenClaw agent for the approved folder, then retry.`);
|
|
501
535
|
}
|
|
502
|
-
const models = record(await openClawJsonProbe([
|
|
503
|
-
if (!models ||
|
|
504
|
-
|
|
536
|
+
const models = record(await openClawJsonProbe(["models", "status", "--agent", agentId, "--json"], options, OPENCLAW_SETUP_TIMEOUT_MS));
|
|
537
|
+
if (!models ||
|
|
538
|
+
(typeof models.agentId === "string" && models.agentId !== agentId)) {
|
|
539
|
+
throw openClawError("openclaw_model_status_invalid", `OpenClaw did not return model status for agent "${agentId}". Run \`openclaw models status --agent "${agentId}" --json\`.`);
|
|
505
540
|
}
|
|
506
|
-
const resolvedModel = typeof models.resolvedDefault ===
|
|
541
|
+
const resolvedModel = typeof models.resolvedDefault === "string" && models.resolvedDefault.trim()
|
|
507
542
|
? models.resolvedDefault.trim()
|
|
508
|
-
: typeof models.defaultModel ===
|
|
543
|
+
: typeof models.defaultModel === "string" && models.defaultModel.trim()
|
|
509
544
|
? models.defaultModel.trim()
|
|
510
545
|
: undefined;
|
|
511
546
|
if (!resolvedModel) {
|
|
512
|
-
throw openClawError(
|
|
547
|
+
throw openClawError("openclaw_model_missing", `OpenClaw agent "${agentId}" has no default model. Configure one, then run \`openclaw models status --agent "${agentId}" --json\`.`);
|
|
513
548
|
}
|
|
514
549
|
const allowedModels = Array.isArray(models.allowed)
|
|
515
|
-
? models.allowed.filter((value) => typeof value ===
|
|
550
|
+
? models.allowed.filter((value) => typeof value === "string")
|
|
516
551
|
: [];
|
|
517
552
|
if (allowedModels.length > 0 && !allowedModels.includes(resolvedModel)) {
|
|
518
|
-
throw openClawError(
|
|
553
|
+
throw openClawError("openclaw_model_unavailable", `OpenClaw agent "${agentId}" resolves to model "${resolvedModel}", but that model is not allowed. Run \`openclaw models status --agent "${agentId}" --json\` and select an allowed model.`);
|
|
519
554
|
}
|
|
520
555
|
const auth = record(models.auth);
|
|
521
556
|
const missingProviders = Array.isArray(auth?.missingProvidersInUse)
|
|
522
|
-
? auth.missingProvidersInUse.filter((value) => typeof value ===
|
|
557
|
+
? auth.missingProvidersInUse.filter((value) => typeof value === "string")
|
|
523
558
|
: [];
|
|
524
559
|
if (missingProviders.length > 0) {
|
|
525
|
-
throw openClawError(
|
|
560
|
+
throw openClawError("openclaw_auth_missing", `OpenClaw agent "${agentId}" is missing authentication for ${missingProviders.join(", ")}. Run \`openclaw models auth --agent "${agentId}" --help\`, configure the provider, then verify with \`openclaw models status --agent "${agentId}" --json\`.`);
|
|
526
561
|
}
|
|
527
562
|
const authRoutes = Array.isArray(auth?.runtimeAuthRoutes)
|
|
528
563
|
? auth.runtimeAuthRoutes.map(record).filter((route) => route !== undefined)
|
|
529
564
|
: [];
|
|
530
565
|
if (authRoutes.length > 0 &&
|
|
531
|
-
!authRoutes.some((route) => route.status ===
|
|
532
|
-
throw openClawError(
|
|
566
|
+
!authRoutes.some((route) => route.status === "usable")) {
|
|
567
|
+
throw openClawError("openclaw_auth_unusable", `OpenClaw agent "${agentId}" has no usable authentication route for "${resolvedModel}" (credentials may be expired or cooling down). Run \`openclaw models auth --agent "${agentId}" list --json\`, repair authentication, then retry.`);
|
|
533
568
|
}
|
|
534
|
-
const sessionLeaf = options.profile.sessionKey ??
|
|
569
|
+
const sessionLeaf = options.profile.sessionKey ?? "main";
|
|
535
570
|
const fullSessionKey = `agent:${agentId}:${sessionLeaf}`;
|
|
536
|
-
const explanation = record(await openClawJsonProbe([
|
|
571
|
+
const explanation = record(await openClawJsonProbe(["sandbox", "explain", "--session", fullSessionKey, "--json"], options));
|
|
537
572
|
const sandbox = record(explanation?.sandbox);
|
|
538
|
-
if (explanation?.agentId !== agentId ||
|
|
539
|
-
|
|
573
|
+
if (explanation?.agentId !== agentId ||
|
|
574
|
+
explanation?.sessionKey !== fullSessionKey ||
|
|
575
|
+
!sandbox) {
|
|
576
|
+
throw openClawError("openclaw_sandbox_invalid", "OpenClaw sandbox preflight did not describe the requested agent session.");
|
|
540
577
|
}
|
|
541
|
-
const effectiveHostRoot = typeof sandbox.effectiveHostWorkspaceRoot ===
|
|
542
|
-
? await resolvedPath(sandbox.effectiveHostWorkspaceRoot,
|
|
578
|
+
const effectiveHostRoot = typeof sandbox.effectiveHostWorkspaceRoot === "string"
|
|
579
|
+
? await resolvedPath(sandbox.effectiveHostWorkspaceRoot, "OpenClaw effective workspace")
|
|
543
580
|
: undefined;
|
|
544
581
|
if (effectiveHostRoot !== desiredWorkspace) {
|
|
545
|
-
throw openClawError(
|
|
582
|
+
throw openClawError("openclaw_sandbox_workspace_mismatch", "OpenClaw sandbox policy does not expose the exact CrewX assignment directory.");
|
|
546
583
|
}
|
|
547
584
|
if (sandbox.sessionIsSandboxed === true) {
|
|
548
|
-
if (sandbox.backend !==
|
|
549
|
-
sandbox.workspaceAccess !==
|
|
550
|
-
typeof sandbox.runtimeWorkdir !==
|
|
551
|
-
throw openClawError(
|
|
585
|
+
if (sandbox.backend !== "docker" ||
|
|
586
|
+
sandbox.workspaceAccess !== "rw" ||
|
|
587
|
+
typeof sandbox.runtimeWorkdir !== "string") {
|
|
588
|
+
throw openClawError("openclaw_sandbox_read_only", "OpenClaw must grant rw workspace access for CrewX coding assignments.");
|
|
552
589
|
}
|
|
553
|
-
const mounts = Array.isArray(sandbox.workspaceMounts)
|
|
590
|
+
const mounts = Array.isArray(sandbox.workspaceMounts)
|
|
591
|
+
? sandbox.workspaceMounts.map(record)
|
|
592
|
+
: [];
|
|
554
593
|
let writableWorkspaceMount = false;
|
|
555
594
|
for (const mount of mounts) {
|
|
556
|
-
if (typeof mount?.hostRoot ===
|
|
557
|
-
typeof mount.containerRoot ===
|
|
595
|
+
if (typeof mount?.hostRoot === "string" &&
|
|
596
|
+
typeof mount.containerRoot === "string" &&
|
|
558
597
|
mount.containerRoot === sandbox.runtimeWorkdir &&
|
|
559
598
|
mount.writable === true &&
|
|
560
|
-
(await resolvedPath(mount.hostRoot,
|
|
599
|
+
(await resolvedPath(mount.hostRoot, "OpenClaw sandbox mount")) ===
|
|
600
|
+
desiredWorkspace) {
|
|
561
601
|
writableWorkspaceMount = true;
|
|
562
602
|
break;
|
|
563
603
|
}
|
|
564
604
|
}
|
|
565
605
|
if (!writableWorkspaceMount) {
|
|
566
|
-
throw openClawError(
|
|
606
|
+
throw openClawError("openclaw_sandbox_mount_missing", "OpenClaw sandbox policy does not mount the CrewX assignment directory read-write at its runtime workdir.");
|
|
567
607
|
}
|
|
568
608
|
}
|
|
569
609
|
else {
|
|
570
|
-
if (typeof sandbox.runtimeWorkdir !==
|
|
571
|
-
throw openClawError(
|
|
610
|
+
if (typeof sandbox.runtimeWorkdir !== "string") {
|
|
611
|
+
throw openClawError("openclaw_runtime_workdir_missing", "OpenClaw did not report its direct runtime working directory.");
|
|
572
612
|
}
|
|
573
|
-
const runtimeWorkdir = await resolvedPath(sandbox.runtimeWorkdir,
|
|
613
|
+
const runtimeWorkdir = await resolvedPath(sandbox.runtimeWorkdir, "OpenClaw runtime working directory");
|
|
574
614
|
if (runtimeWorkdir !== desiredWorkspace) {
|
|
575
|
-
throw openClawError(
|
|
615
|
+
throw openClawError("openclaw_runtime_workdir_mismatch", "OpenClaw direct runtime workdir does not match the CrewX assignment directory.");
|
|
576
616
|
}
|
|
577
617
|
}
|
|
578
618
|
}
|
|
@@ -582,36 +622,36 @@ function openClawMessages(value) {
|
|
|
582
622
|
return [];
|
|
583
623
|
return payloads.flatMap((payload) => {
|
|
584
624
|
const text = record(payload)?.text;
|
|
585
|
-
return typeof text ===
|
|
625
|
+
return typeof text === "string" && text.trim() ? [text] : [];
|
|
586
626
|
});
|
|
587
627
|
}
|
|
588
628
|
function createLineConsumer(callback) {
|
|
589
|
-
let pending =
|
|
629
|
+
let pending = "";
|
|
590
630
|
return {
|
|
591
631
|
write(chunk) {
|
|
592
632
|
pending += chunk.toString();
|
|
593
633
|
while (true) {
|
|
594
|
-
const newline = pending.indexOf(
|
|
634
|
+
const newline = pending.indexOf("\n");
|
|
595
635
|
if (newline < 0)
|
|
596
636
|
break;
|
|
597
|
-
const line = pending.slice(0, newline).replace(/\r$/,
|
|
637
|
+
const line = pending.slice(0, newline).replace(/\r$/, "");
|
|
598
638
|
pending = pending.slice(newline + 1);
|
|
599
639
|
if (line.length > 0)
|
|
600
640
|
callback(line);
|
|
601
641
|
}
|
|
602
642
|
},
|
|
603
643
|
end() {
|
|
604
|
-
const line = pending.replace(/\r$/,
|
|
644
|
+
const line = pending.replace(/\r$/, "");
|
|
605
645
|
if (line.length > 0)
|
|
606
646
|
callback(line);
|
|
607
|
-
pending =
|
|
647
|
+
pending = "";
|
|
608
648
|
},
|
|
609
649
|
};
|
|
610
650
|
}
|
|
611
651
|
const ALLOWED_RUNTIME_CONTROL_VARIABLES = [
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
652
|
+
"CREWX_URL",
|
|
653
|
+
"CREWX_TOKEN",
|
|
654
|
+
"CREWX_CLI_PATH",
|
|
615
655
|
];
|
|
616
656
|
/**
|
|
617
657
|
* Coding runtimes inherit provider credentials and ordinary process settings,
|
|
@@ -622,7 +662,7 @@ const ALLOWED_RUNTIME_CONTROL_VARIABLES = [
|
|
|
622
662
|
export function runtimeEnvironment(environment = {}, controlEnvironment = {}, inherited = process.env) {
|
|
623
663
|
const result = {};
|
|
624
664
|
for (const [key, value] of Object.entries({ ...inherited, ...environment })) {
|
|
625
|
-
if (key.toUpperCase().startsWith(
|
|
665
|
+
if (key.toUpperCase().startsWith("CREWX_"))
|
|
626
666
|
continue;
|
|
627
667
|
if (value !== undefined)
|
|
628
668
|
result[key] = value;
|
|
@@ -635,23 +675,31 @@ export function runtimeEnvironment(environment = {}, controlEnvironment = {}, in
|
|
|
635
675
|
return result;
|
|
636
676
|
}
|
|
637
677
|
async function materializeInvocation(invocation, prompt) {
|
|
638
|
-
if (invocation.prompt.type ===
|
|
639
|
-
return {
|
|
678
|
+
if (invocation.prompt.type === "stdin") {
|
|
679
|
+
return {
|
|
680
|
+
args: invocation.args,
|
|
681
|
+
stdin: prompt,
|
|
682
|
+
cleanup: async () => undefined,
|
|
683
|
+
};
|
|
640
684
|
}
|
|
641
|
-
if (invocation.prompt.type ===
|
|
685
|
+
if (invocation.prompt.type === "argument") {
|
|
642
686
|
const args = [...invocation.args];
|
|
643
687
|
args.splice(invocation.prompt.index, 0, prompt);
|
|
644
|
-
return { args, stdin:
|
|
688
|
+
return { args, stdin: "", cleanup: async () => undefined };
|
|
645
689
|
}
|
|
646
|
-
const directory = await mkdtemp(join(tmpdir(),
|
|
690
|
+
const directory = await mkdtemp(join(tmpdir(), "crewx-prompt-"));
|
|
647
691
|
try {
|
|
648
692
|
await chmod(directory, 0o700);
|
|
649
|
-
const path = join(directory,
|
|
650
|
-
await writeFile(path, prompt, {
|
|
693
|
+
const path = join(directory, "prompt.txt");
|
|
694
|
+
await writeFile(path, prompt, {
|
|
695
|
+
encoding: "utf8",
|
|
696
|
+
flag: "wx",
|
|
697
|
+
mode: 0o600,
|
|
698
|
+
});
|
|
651
699
|
await chmod(path, 0o600);
|
|
652
700
|
return {
|
|
653
701
|
args: [...invocation.args, invocation.prompt.flag, path],
|
|
654
|
-
stdin:
|
|
702
|
+
stdin: "",
|
|
655
703
|
cleanup: async () => rm(directory, { recursive: true, force: true }),
|
|
656
704
|
};
|
|
657
705
|
}
|
|
@@ -660,7 +708,7 @@ async function materializeInvocation(invocation, prompt) {
|
|
|
660
708
|
throw error;
|
|
661
709
|
}
|
|
662
710
|
}
|
|
663
|
-
function emitMessage(options, messages, message, role =
|
|
711
|
+
function emitMessage(options, messages, message, role = "assistant") {
|
|
664
712
|
if (!message.trim())
|
|
665
713
|
return;
|
|
666
714
|
messages.push(message);
|
|
@@ -668,9 +716,9 @@ function emitMessage(options, messages, message, role = 'assistant') {
|
|
|
668
716
|
}
|
|
669
717
|
async function runSpawnAdapter(options) {
|
|
670
718
|
if (!options.prompt.trim())
|
|
671
|
-
throw new CliError(
|
|
719
|
+
throw new CliError("Agent prompt cannot be empty.");
|
|
672
720
|
const invocation = buildAdapterInvocation(options.adapter, options.profile, options.codingCommand);
|
|
673
|
-
if (options.adapter ===
|
|
721
|
+
if (options.adapter === "openclaw" && !options.codingCommand) {
|
|
674
722
|
await verifyOpenClawRuntime({
|
|
675
723
|
cwd: options.cwd,
|
|
676
724
|
profile: options.profile ?? {},
|
|
@@ -686,8 +734,8 @@ async function runSpawnAdapter(options) {
|
|
|
686
734
|
cwd: options.cwd,
|
|
687
735
|
env: runtimeEnvironment(options.environment, options.controlEnvironment),
|
|
688
736
|
shell: false,
|
|
689
|
-
detached: process.platform !==
|
|
690
|
-
stdio: [
|
|
737
|
+
detached: process.platform !== "win32",
|
|
738
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
691
739
|
});
|
|
692
740
|
}
|
|
693
741
|
catch (error) {
|
|
@@ -699,7 +747,9 @@ async function runSpawnAdapter(options) {
|
|
|
699
747
|
const stderr = [];
|
|
700
748
|
const messages = [];
|
|
701
749
|
const finalMessageOnly = !options.codingCommand &&
|
|
702
|
-
(options.adapter ===
|
|
750
|
+
(options.adapter === "codex" ||
|
|
751
|
+
options.adapter === "claude" ||
|
|
752
|
+
options.adapter === "pi");
|
|
703
753
|
let pendingFinalMessage;
|
|
704
754
|
const maxOutputBytes = options.maxOutputBytes ?? MAX_AGENT_OUTPUT_BYTES;
|
|
705
755
|
let outputBytes = 0;
|
|
@@ -710,7 +760,10 @@ async function runSpawnAdapter(options) {
|
|
|
710
760
|
options.onStdout?.(line, parsed);
|
|
711
761
|
if (parsed.message) {
|
|
712
762
|
if (finalMessageOnly) {
|
|
713
|
-
pendingFinalMessage = {
|
|
763
|
+
pendingFinalMessage = {
|
|
764
|
+
message: parsed.message,
|
|
765
|
+
role: parsed.role ?? "assistant",
|
|
766
|
+
};
|
|
714
767
|
}
|
|
715
768
|
else {
|
|
716
769
|
emitMessage(options, messages, parsed.message, parsed.role);
|
|
@@ -730,21 +783,21 @@ async function runSpawnAdapter(options) {
|
|
|
730
783
|
});
|
|
731
784
|
let timedOut = false;
|
|
732
785
|
let terminating = false;
|
|
733
|
-
const terminationGraceMs = options.adapter ===
|
|
786
|
+
const terminationGraceMs = options.adapter === "openclaw" ? 60_000 : 5_000;
|
|
734
787
|
const abort = () => {
|
|
735
788
|
if (lifecycle.isClosed() || child.exitCode !== null || terminating)
|
|
736
789
|
return;
|
|
737
790
|
terminating = true;
|
|
738
|
-
terminateChild(child,
|
|
791
|
+
terminateChild(child, "SIGTERM");
|
|
739
792
|
forceKill = setTimeout(() => {
|
|
740
793
|
if (!lifecycle.isClosed())
|
|
741
|
-
terminateChild(child,
|
|
794
|
+
terminateChild(child, "SIGKILL");
|
|
742
795
|
}, terminationGraceMs);
|
|
743
796
|
hardStop = setTimeout(() => {
|
|
744
797
|
if (lifecycle.isClosed())
|
|
745
798
|
return;
|
|
746
799
|
forcedClosed = true;
|
|
747
|
-
terminateChild(child,
|
|
800
|
+
terminateChild(child, "SIGKILL");
|
|
748
801
|
destroyChildPipes(child);
|
|
749
802
|
rejectTermination?.(new CliError(`${invocation.command} did not close after forced termination.`));
|
|
750
803
|
}, terminationGraceMs + 1_000);
|
|
@@ -760,27 +813,27 @@ async function runSpawnAdapter(options) {
|
|
|
760
813
|
}
|
|
761
814
|
consumer(chunk);
|
|
762
815
|
};
|
|
763
|
-
child.stdout.on(
|
|
764
|
-
child.stderr.on(
|
|
816
|
+
child.stdout.on("data", (chunk) => consumeOutput(stdoutLines.write, chunk));
|
|
817
|
+
child.stderr.on("data", (chunk) => consumeOutput(stderrLines.write, chunk));
|
|
765
818
|
if (options.signal?.aborted)
|
|
766
819
|
abort();
|
|
767
|
-
options.signal?.addEventListener(
|
|
820
|
+
options.signal?.addEventListener("abort", abort, { once: true });
|
|
768
821
|
const runTimeout = setTimeout(() => {
|
|
769
822
|
timedOut = true;
|
|
770
823
|
abort();
|
|
771
824
|
}, options.timeoutMs ?? DEFAULT_RUN_TIMEOUT_MS);
|
|
772
825
|
const spawnError = new Promise((_resolve, reject) => {
|
|
773
|
-
child.once(
|
|
826
|
+
child.once("error", (error) => {
|
|
774
827
|
reject(new CliError(`Unable to start ${invocation.command}. Is it installed and on PATH?`, 1, { cause: error }));
|
|
775
828
|
});
|
|
776
829
|
});
|
|
777
830
|
const stdinError = new Promise((_resolve, reject) => {
|
|
778
|
-
child.stdin.once(
|
|
831
|
+
child.stdin.once("error", (error) => {
|
|
779
832
|
reject(new CliError(`${invocation.command} closed its input before receiving the prompt.`, 1, { cause: error }));
|
|
780
833
|
});
|
|
781
834
|
});
|
|
782
835
|
try {
|
|
783
|
-
child.stdin.end(materialized.stdin,
|
|
836
|
+
child.stdin.end(materialized.stdin, "utf8");
|
|
784
837
|
const [exitCode, signal] = (await Promise.race([
|
|
785
838
|
lifecycle.close,
|
|
786
839
|
spawnError,
|
|
@@ -797,9 +850,9 @@ async function runSpawnAdapter(options) {
|
|
|
797
850
|
if (exitCode === 0 && !options.signal?.aborted && pendingFinalMessage) {
|
|
798
851
|
emitMessage(options, messages, pendingFinalMessage.message, pendingFinalMessage.role);
|
|
799
852
|
}
|
|
800
|
-
const completeOutput = stdout.join(
|
|
853
|
+
const completeOutput = stdout.join("\n").trim();
|
|
801
854
|
if (exitCode === 0 && messages.length === 0 && completeOutput) {
|
|
802
|
-
if (invocation.output ===
|
|
855
|
+
if (invocation.output === "json") {
|
|
803
856
|
let parsed;
|
|
804
857
|
try {
|
|
805
858
|
parsed = JSON.parse(completeOutput);
|
|
@@ -810,20 +863,26 @@ async function runSpawnAdapter(options) {
|
|
|
810
863
|
for (const message of openClawMessages(parsed))
|
|
811
864
|
emitMessage(options, messages, message);
|
|
812
865
|
}
|
|
813
|
-
else if (invocation.output ===
|
|
866
|
+
else if (invocation.output === "text") {
|
|
814
867
|
emitMessage(options, messages, completeOutput);
|
|
815
868
|
}
|
|
816
869
|
}
|
|
817
|
-
if (exitCode === 0 &&
|
|
818
|
-
|
|
870
|
+
if (exitCode === 0 &&
|
|
871
|
+
!options.signal?.aborted &&
|
|
872
|
+
options.adapter === "openclaw" &&
|
|
873
|
+
messages.length === 0) {
|
|
874
|
+
throw new CliError("OpenClaw completed without a visible assistant response.");
|
|
819
875
|
}
|
|
820
|
-
if (exitCode === 0 &&
|
|
876
|
+
if (exitCode === 0 &&
|
|
877
|
+
!options.signal?.aborted &&
|
|
878
|
+
finalMessageOnly &&
|
|
879
|
+
messages.length === 0) {
|
|
821
880
|
throw new CliError(`${invocation.command} completed without a visible assistant response.`);
|
|
822
881
|
}
|
|
823
882
|
return { exitCode, signal, messages, stdout, stderr };
|
|
824
883
|
}
|
|
825
884
|
finally {
|
|
826
|
-
options.signal?.removeEventListener(
|
|
885
|
+
options.signal?.removeEventListener("abort", abort);
|
|
827
886
|
clearTimeout(runTimeout);
|
|
828
887
|
if (forceKill)
|
|
829
888
|
clearTimeout(forceKill);
|
|
@@ -836,23 +895,24 @@ async function runSpawnAdapter(options) {
|
|
|
836
895
|
}
|
|
837
896
|
}
|
|
838
897
|
async function runHermesAcp(options) {
|
|
839
|
-
const restricted = options.profile?.chatOnly === true ||
|
|
898
|
+
const restricted = options.profile?.chatOnly === true ||
|
|
899
|
+
options.profile?.permissionPreset === "read_only";
|
|
840
900
|
if (restricted) {
|
|
841
|
-
throw new CliError(
|
|
901
|
+
throw new CliError("Hermes does not expose a trustworthy read-only ACP mode. Use a sandboxed custom command or choose standard/full access.");
|
|
842
902
|
}
|
|
843
903
|
const spawnProcess = options.spawnProcess ?? spawn;
|
|
844
904
|
let child;
|
|
845
905
|
try {
|
|
846
|
-
child = spawnProcess(
|
|
906
|
+
child = spawnProcess("hermes", ["acp"], {
|
|
847
907
|
cwd: options.cwd,
|
|
848
908
|
env: runtimeEnvironment(options.environment, options.controlEnvironment),
|
|
849
909
|
shell: false,
|
|
850
|
-
detached: process.platform !==
|
|
851
|
-
stdio: [
|
|
910
|
+
detached: process.platform !== "win32",
|
|
911
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
852
912
|
});
|
|
853
913
|
}
|
|
854
914
|
catch (error) {
|
|
855
|
-
throw new CliError(
|
|
915
|
+
throw new CliError("Unable to start hermes. Is it installed and on PATH?", 1, { cause: error });
|
|
856
916
|
}
|
|
857
917
|
const lifecycle = observeChild(child);
|
|
858
918
|
const stdout = [];
|
|
@@ -861,7 +921,7 @@ async function runHermesAcp(options) {
|
|
|
861
921
|
const pending = new Map();
|
|
862
922
|
let nextId = 1;
|
|
863
923
|
let sessionId;
|
|
864
|
-
let finalText =
|
|
924
|
+
let finalText = "";
|
|
865
925
|
let forceKill;
|
|
866
926
|
let hardStop;
|
|
867
927
|
let forcedClosed = false;
|
|
@@ -872,19 +932,19 @@ async function runHermesAcp(options) {
|
|
|
872
932
|
let abortRuntime = () => undefined;
|
|
873
933
|
let terminating = false;
|
|
874
934
|
const write = (value) => {
|
|
875
|
-
child.stdin.write(`${JSON.stringify(value)}\n`,
|
|
935
|
+
child.stdin.write(`${JSON.stringify(value)}\n`, "utf8");
|
|
876
936
|
};
|
|
877
937
|
const request = (method, params) => {
|
|
878
938
|
const id = nextId++;
|
|
879
939
|
return new Promise((resolve, reject) => {
|
|
880
940
|
pending.set(id, { resolve, reject });
|
|
881
|
-
write({ jsonrpc:
|
|
941
|
+
write({ jsonrpc: "2.0", id, method, params });
|
|
882
942
|
});
|
|
883
943
|
};
|
|
884
944
|
const resultOf = async (method, params) => {
|
|
885
945
|
const response = await request(method, params);
|
|
886
946
|
if (response.error)
|
|
887
|
-
throw new CliError(`Hermes ACP ${method} failed: ${response.error.message ??
|
|
947
|
+
throw new CliError(`Hermes ACP ${method} failed: ${response.error.message ?? "unknown error"}`);
|
|
888
948
|
return response.result;
|
|
889
949
|
};
|
|
890
950
|
const stdoutLines = createLineConsumer((line) => {
|
|
@@ -898,14 +958,17 @@ async function runHermesAcp(options) {
|
|
|
898
958
|
return;
|
|
899
959
|
}
|
|
900
960
|
const message = record(value);
|
|
901
|
-
const method = typeof message?.method ===
|
|
902
|
-
options.onStdout?.(line, {
|
|
903
|
-
|
|
961
|
+
const method = typeof message?.method === "string" ? message.method : undefined;
|
|
962
|
+
options.onStdout?.(line, {
|
|
963
|
+
raw: value,
|
|
964
|
+
...(method ? { eventType: method } : {}),
|
|
965
|
+
});
|
|
966
|
+
if (typeof message?.id === "number" && !method) {
|
|
904
967
|
const waiter = pending.get(message.id);
|
|
905
968
|
if (waiter) {
|
|
906
969
|
pending.delete(message.id);
|
|
907
970
|
const response = {};
|
|
908
|
-
if (Object.hasOwn(message,
|
|
971
|
+
if (Object.hasOwn(message, "result"))
|
|
909
972
|
response.result = message.result;
|
|
910
973
|
const rpcError = record(message.error);
|
|
911
974
|
if (rpcError)
|
|
@@ -914,27 +977,30 @@ async function runHermesAcp(options) {
|
|
|
914
977
|
}
|
|
915
978
|
return;
|
|
916
979
|
}
|
|
917
|
-
if (method ===
|
|
980
|
+
if (method === "session/update") {
|
|
918
981
|
const update = record(record(message?.params)?.update);
|
|
919
982
|
const content = record(update?.content);
|
|
920
|
-
if (update?.sessionUpdate ===
|
|
983
|
+
if (update?.sessionUpdate === "agent_message_chunk" &&
|
|
984
|
+
content?.type === "text" &&
|
|
985
|
+
typeof content.text === "string") {
|
|
921
986
|
finalText += content.text;
|
|
922
987
|
}
|
|
923
988
|
return;
|
|
924
989
|
}
|
|
925
|
-
if (method ===
|
|
990
|
+
if (method === "session/request_permission" &&
|
|
991
|
+
(typeof message?.id === "number" || typeof message?.id === "string")) {
|
|
926
992
|
const offered = record(message.params)?.options;
|
|
927
993
|
const allowed = Array.isArray(offered)
|
|
928
|
-
? [
|
|
994
|
+
? ["allow_once", "allow_session"].find((optionId) => offered.some((option) => record(option)?.optionId === optionId))
|
|
929
995
|
: undefined;
|
|
930
|
-
const fullAccess = options.profile?.permissionPreset ===
|
|
996
|
+
const fullAccess = options.profile?.permissionPreset === "full_access";
|
|
931
997
|
write({
|
|
932
|
-
jsonrpc:
|
|
998
|
+
jsonrpc: "2.0",
|
|
933
999
|
id: message.id,
|
|
934
1000
|
result: {
|
|
935
1001
|
outcome: fullAccess && allowed
|
|
936
|
-
? { outcome:
|
|
937
|
-
: { outcome:
|
|
1002
|
+
? { outcome: "selected", optionId: allowed }
|
|
1003
|
+
: { outcome: "cancelled" },
|
|
938
1004
|
},
|
|
939
1005
|
});
|
|
940
1006
|
}
|
|
@@ -954,80 +1020,94 @@ async function runHermesAcp(options) {
|
|
|
954
1020
|
}
|
|
955
1021
|
consumer(chunk);
|
|
956
1022
|
};
|
|
957
|
-
child.stdout.on(
|
|
958
|
-
child.stderr.on(
|
|
1023
|
+
child.stdout.on("data", (chunk) => consumeOutput(stdoutLines.write, chunk));
|
|
1024
|
+
child.stderr.on("data", (chunk) => consumeOutput(stderrLines.write, chunk));
|
|
959
1025
|
const failPending = (error) => {
|
|
960
1026
|
for (const waiter of pending.values())
|
|
961
1027
|
waiter.reject(error);
|
|
962
1028
|
pending.clear();
|
|
963
1029
|
};
|
|
964
|
-
child.once(
|
|
965
|
-
child.stdin.once(
|
|
966
|
-
|
|
1030
|
+
child.once("error", (error) => failPending(new CliError("Hermes ACP process failed.", 1, { cause: error })));
|
|
1031
|
+
child.stdin.once("error", (error) => failPending(new CliError("Hermes closed its ACP input unexpectedly.", 1, {
|
|
1032
|
+
cause: error,
|
|
1033
|
+
})));
|
|
1034
|
+
child.once("close", () => failPending(new CliError("Hermes ACP process closed before the request completed.")));
|
|
967
1035
|
const abort = () => {
|
|
968
1036
|
if (sessionId)
|
|
969
|
-
write({
|
|
1037
|
+
write({
|
|
1038
|
+
jsonrpc: "2.0",
|
|
1039
|
+
method: "session/cancel",
|
|
1040
|
+
params: { sessionId },
|
|
1041
|
+
});
|
|
970
1042
|
if (lifecycle.isClosed() || child.exitCode !== null || terminating)
|
|
971
1043
|
return;
|
|
972
1044
|
terminating = true;
|
|
973
|
-
terminateChild(child,
|
|
1045
|
+
terminateChild(child, "SIGTERM");
|
|
974
1046
|
forceKill = setTimeout(() => {
|
|
975
1047
|
if (!lifecycle.isClosed())
|
|
976
|
-
terminateChild(child,
|
|
1048
|
+
terminateChild(child, "SIGKILL");
|
|
977
1049
|
}, 5_000);
|
|
978
1050
|
forceKill.unref();
|
|
979
1051
|
hardStop = setTimeout(() => {
|
|
980
1052
|
if (lifecycle.isClosed())
|
|
981
1053
|
return;
|
|
982
1054
|
forcedClosed = true;
|
|
983
|
-
terminateChild(child,
|
|
1055
|
+
terminateChild(child, "SIGKILL");
|
|
984
1056
|
destroyChildPipes(child);
|
|
985
|
-
failPending(new CliError(
|
|
1057
|
+
failPending(new CliError("Hermes did not close after forced termination."));
|
|
986
1058
|
}, 6_000);
|
|
987
1059
|
};
|
|
988
1060
|
abortRuntime = abort;
|
|
989
1061
|
if (options.signal?.aborted)
|
|
990
1062
|
abort();
|
|
991
|
-
options.signal?.addEventListener(
|
|
1063
|
+
options.signal?.addEventListener("abort", abort, { once: true });
|
|
992
1064
|
const runTimeout = setTimeout(() => {
|
|
993
1065
|
timedOut = true;
|
|
994
1066
|
abort();
|
|
995
1067
|
}, options.timeoutMs ?? DEFAULT_RUN_TIMEOUT_MS);
|
|
996
1068
|
try {
|
|
997
|
-
const initialized = record(await resultOf(
|
|
1069
|
+
const initialized = record(await resultOf("initialize", {
|
|
998
1070
|
protocolVersion: 1,
|
|
999
|
-
clientCapabilities: {
|
|
1000
|
-
|
|
1071
|
+
clientCapabilities: {
|
|
1072
|
+
fs: { readTextFile: false, writeTextFile: false },
|
|
1073
|
+
terminal: false,
|
|
1074
|
+
},
|
|
1075
|
+
clientInfo: { name: "crewx", version: CLI_VERSION },
|
|
1001
1076
|
}));
|
|
1002
1077
|
if (initialized?.protocolVersion !== 1)
|
|
1003
|
-
throw new CliError(
|
|
1004
|
-
const created = record(await resultOf(
|
|
1005
|
-
if (typeof created?.sessionId !==
|
|
1006
|
-
throw new CliError(
|
|
1078
|
+
throw new CliError("Hermes returned an unsupported ACP protocol version.");
|
|
1079
|
+
const created = record(await resultOf("session/new", { cwd: options.cwd, mcpServers: [] }));
|
|
1080
|
+
if (typeof created?.sessionId !== "string" || !created.sessionId) {
|
|
1081
|
+
throw new CliError("Hermes did not return an ACP session id.");
|
|
1007
1082
|
}
|
|
1008
1083
|
sessionId = created.sessionId;
|
|
1009
|
-
await resultOf(
|
|
1084
|
+
await resultOf("session/set_mode", {
|
|
1010
1085
|
sessionId,
|
|
1011
|
-
modeId: options.profile?.permissionPreset ===
|
|
1086
|
+
modeId: options.profile?.permissionPreset === "full_access"
|
|
1087
|
+
? "dont_ask"
|
|
1088
|
+
: "accept_edits",
|
|
1012
1089
|
});
|
|
1013
1090
|
if (options.profile?.model) {
|
|
1014
|
-
await resultOf(
|
|
1091
|
+
await resultOf("session/set_model", {
|
|
1092
|
+
sessionId,
|
|
1093
|
+
modelId: options.profile.model,
|
|
1094
|
+
});
|
|
1015
1095
|
}
|
|
1016
|
-
await resultOf(
|
|
1096
|
+
await resultOf("session/prompt", {
|
|
1017
1097
|
sessionId,
|
|
1018
|
-
prompt: [{ type:
|
|
1098
|
+
prompt: [{ type: "text", text: options.prompt }],
|
|
1019
1099
|
});
|
|
1020
1100
|
if (timedOut)
|
|
1021
|
-
throw new CliError(
|
|
1101
|
+
throw new CliError("Hermes exceeded the CrewX run timeout.");
|
|
1022
1102
|
if (outputLimitExceeded) {
|
|
1023
1103
|
throw new CliError(`hermes exceeded the ${String(maxOutputBytes)} byte CrewX output limit.`);
|
|
1024
1104
|
}
|
|
1025
1105
|
if (!finalText.trim())
|
|
1026
|
-
throw new CliError(
|
|
1106
|
+
throw new CliError("Hermes completed without a visible assistant response.");
|
|
1027
1107
|
emitMessage(options, messages, finalText);
|
|
1028
1108
|
child.stdin.end();
|
|
1029
|
-
const terminateTimer = setTimeout(() => terminateChild(child,
|
|
1030
|
-
const killTimer = setTimeout(() => terminateChild(child,
|
|
1109
|
+
const terminateTimer = setTimeout(() => terminateChild(child, "SIGTERM"), 2_000);
|
|
1110
|
+
const killTimer = setTimeout(() => terminateChild(child, "SIGKILL"), 7_000);
|
|
1031
1111
|
let shutdownTimer;
|
|
1032
1112
|
terminateTimer.unref();
|
|
1033
1113
|
killTimer.unref();
|
|
@@ -1036,7 +1116,7 @@ async function runHermesAcp(options) {
|
|
|
1036
1116
|
new Promise((resolve) => {
|
|
1037
1117
|
shutdownTimer = setTimeout(() => {
|
|
1038
1118
|
forcedClosed = true;
|
|
1039
|
-
terminateChild(child,
|
|
1119
|
+
terminateChild(child, "SIGKILL");
|
|
1040
1120
|
destroyChildPipes(child);
|
|
1041
1121
|
resolve([child.exitCode, null]);
|
|
1042
1122
|
}, 8_000);
|
|
@@ -1057,7 +1137,7 @@ async function runHermesAcp(options) {
|
|
|
1057
1137
|
};
|
|
1058
1138
|
}
|
|
1059
1139
|
finally {
|
|
1060
|
-
options.signal?.removeEventListener(
|
|
1140
|
+
options.signal?.removeEventListener("abort", abort);
|
|
1061
1141
|
clearTimeout(runTimeout);
|
|
1062
1142
|
if (forceKill)
|
|
1063
1143
|
clearTimeout(forceKill);
|
|
@@ -1068,40 +1148,211 @@ async function runHermesAcp(options) {
|
|
|
1068
1148
|
}
|
|
1069
1149
|
}
|
|
1070
1150
|
}
|
|
1151
|
+
export function usesAcpRuntime(adapter, profile = {}) {
|
|
1152
|
+
if (!ACPX_HARNESSES.includes(adapter))
|
|
1153
|
+
return false;
|
|
1154
|
+
return (profile.transport === "acp" ||
|
|
1155
|
+
!["codex", "claude", "pi", "openclaw"].includes(adapter));
|
|
1156
|
+
}
|
|
1157
|
+
function acpWorkerEntrypoint() {
|
|
1158
|
+
return fileURLToPath(new URL("./acp-worker.js", import.meta.url));
|
|
1159
|
+
}
|
|
1160
|
+
async function runAcpxAdapter(options) {
|
|
1161
|
+
const spawnProcess = options.spawnProcess ?? spawn;
|
|
1162
|
+
let child;
|
|
1163
|
+
try {
|
|
1164
|
+
child = spawnProcess(process.execPath, [acpWorkerEntrypoint()], {
|
|
1165
|
+
cwd: options.cwd,
|
|
1166
|
+
env: runtimeEnvironment(options.environment, options.controlEnvironment),
|
|
1167
|
+
shell: false,
|
|
1168
|
+
detached: process.platform !== "win32",
|
|
1169
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
1170
|
+
});
|
|
1171
|
+
}
|
|
1172
|
+
catch (error) {
|
|
1173
|
+
throw new CliError(`Unable to start the ${options.adapter} ACP runtime.`, 1, { cause: error });
|
|
1174
|
+
}
|
|
1175
|
+
const lifecycle = observeChild(child);
|
|
1176
|
+
const stdout = [];
|
|
1177
|
+
const stderr = [];
|
|
1178
|
+
const messages = [];
|
|
1179
|
+
let finalText = "";
|
|
1180
|
+
let resultEnvelope;
|
|
1181
|
+
let workerError;
|
|
1182
|
+
let outputBytes = 0;
|
|
1183
|
+
let outputLimitExceeded = false;
|
|
1184
|
+
let timedOut = false;
|
|
1185
|
+
let terminating = false;
|
|
1186
|
+
let forceKill;
|
|
1187
|
+
const abort = () => {
|
|
1188
|
+
if (lifecycle.isClosed() || child.exitCode !== null || terminating)
|
|
1189
|
+
return;
|
|
1190
|
+
terminating = true;
|
|
1191
|
+
terminateChild(child, "SIGTERM");
|
|
1192
|
+
forceKill = setTimeout(() => {
|
|
1193
|
+
if (!lifecycle.isClosed())
|
|
1194
|
+
terminateChild(child, "SIGKILL");
|
|
1195
|
+
}, 5_000);
|
|
1196
|
+
forceKill.unref();
|
|
1197
|
+
};
|
|
1198
|
+
const consume = (callback, chunk) => {
|
|
1199
|
+
if (outputLimitExceeded)
|
|
1200
|
+
return;
|
|
1201
|
+
outputBytes += Buffer.byteLength(chunk);
|
|
1202
|
+
if (outputBytes > (options.maxOutputBytes ?? MAX_AGENT_OUTPUT_BYTES)) {
|
|
1203
|
+
outputLimitExceeded = true;
|
|
1204
|
+
abort();
|
|
1205
|
+
return;
|
|
1206
|
+
}
|
|
1207
|
+
callback(chunk);
|
|
1208
|
+
};
|
|
1209
|
+
const stdoutLines = createLineConsumer((line) => {
|
|
1210
|
+
stdout.push(line);
|
|
1211
|
+
let envelope;
|
|
1212
|
+
try {
|
|
1213
|
+
envelope = JSON.parse(line);
|
|
1214
|
+
}
|
|
1215
|
+
catch {
|
|
1216
|
+
workerError = { message: "The ACP worker returned invalid JSON." };
|
|
1217
|
+
return;
|
|
1218
|
+
}
|
|
1219
|
+
if (envelope.type === "event" && envelope.event) {
|
|
1220
|
+
const eventType = typeof envelope.event.type === "string"
|
|
1221
|
+
? `acp:${envelope.event.type}`
|
|
1222
|
+
: "acp:event";
|
|
1223
|
+
options.onStdout?.(line, { raw: envelope.event, eventType });
|
|
1224
|
+
if (envelope.event.type === "text_delta" &&
|
|
1225
|
+
envelope.event.stream !== "thought" &&
|
|
1226
|
+
typeof envelope.event.text === "string") {
|
|
1227
|
+
finalText += envelope.event.text;
|
|
1228
|
+
}
|
|
1229
|
+
return;
|
|
1230
|
+
}
|
|
1231
|
+
if (envelope.type === "result")
|
|
1232
|
+
resultEnvelope = envelope.result;
|
|
1233
|
+
if (envelope.type === "error")
|
|
1234
|
+
workerError = envelope.error;
|
|
1235
|
+
});
|
|
1236
|
+
const stderrLines = createLineConsumer((line) => {
|
|
1237
|
+
stderr.push(line);
|
|
1238
|
+
options.onStderr?.(line);
|
|
1239
|
+
});
|
|
1240
|
+
child.stdout.on("data", (chunk) => consume(stdoutLines.write, chunk));
|
|
1241
|
+
child.stderr.on("data", (chunk) => consume(stderrLines.write, chunk));
|
|
1242
|
+
if (options.signal?.aborted)
|
|
1243
|
+
abort();
|
|
1244
|
+
options.signal?.addEventListener("abort", abort, { once: true });
|
|
1245
|
+
const timeout = setTimeout(() => {
|
|
1246
|
+
timedOut = true;
|
|
1247
|
+
abort();
|
|
1248
|
+
}, options.timeoutMs ?? DEFAULT_RUN_TIMEOUT_MS);
|
|
1249
|
+
const request = {
|
|
1250
|
+
harness: options.adapter,
|
|
1251
|
+
prompt: options.prompt,
|
|
1252
|
+
cwd: options.cwd,
|
|
1253
|
+
sessionKey: options.profile?.sessionKey ??
|
|
1254
|
+
`crewx-${createHash("sha256")
|
|
1255
|
+
.update(JSON.stringify(["local", options.adapter, options.cwd]))
|
|
1256
|
+
.digest("hex")
|
|
1257
|
+
.slice(0, 48)}`,
|
|
1258
|
+
timeoutMs: options.timeoutMs ?? DEFAULT_RUN_TIMEOUT_MS,
|
|
1259
|
+
profile: {
|
|
1260
|
+
...(options.profile?.model ? { model: options.profile.model } : {}),
|
|
1261
|
+
...(options.profile?.permissionPreset
|
|
1262
|
+
? { permissionPreset: options.profile.permissionPreset }
|
|
1263
|
+
: {}),
|
|
1264
|
+
...(options.profile?.chatOnly !== undefined
|
|
1265
|
+
? { chatOnly: options.profile.chatOnly }
|
|
1266
|
+
: {}),
|
|
1267
|
+
},
|
|
1268
|
+
};
|
|
1269
|
+
try {
|
|
1270
|
+
child.stdin.end(JSON.stringify(request), "utf8");
|
|
1271
|
+
const [exitCode, signal] = (await Promise.race([
|
|
1272
|
+
lifecycle.close,
|
|
1273
|
+
once(child, "error").then(([error]) => {
|
|
1274
|
+
throw error;
|
|
1275
|
+
}),
|
|
1276
|
+
]));
|
|
1277
|
+
stdoutLines.end();
|
|
1278
|
+
stderrLines.end();
|
|
1279
|
+
if (timedOut)
|
|
1280
|
+
throw new CliError(`${options.adapter} ACP exceeded the CrewX run timeout.`);
|
|
1281
|
+
if (outputLimitExceeded) {
|
|
1282
|
+
throw new CliError(`${options.adapter} ACP exceeded the CrewX output limit.`);
|
|
1283
|
+
}
|
|
1284
|
+
if (options.signal?.aborted)
|
|
1285
|
+
return { exitCode, signal, messages, stdout, stderr };
|
|
1286
|
+
const failure = workerError ?? resultEnvelope?.error;
|
|
1287
|
+
if (failure?.message) {
|
|
1288
|
+
throw new CliError(failure.message, 1, {
|
|
1289
|
+
code: failure.code ?? "acp_runtime_failed",
|
|
1290
|
+
});
|
|
1291
|
+
}
|
|
1292
|
+
if (resultEnvelope?.status !== "completed" || exitCode !== 0) {
|
|
1293
|
+
throw new CliError(`${options.adapter} ACP runtime failed to complete.`, 1, {
|
|
1294
|
+
code: "acp_runtime_failed",
|
|
1295
|
+
});
|
|
1296
|
+
}
|
|
1297
|
+
if (!finalText.trim()) {
|
|
1298
|
+
throw new CliError(`${options.adapter} ACP completed without a visible assistant response.`);
|
|
1299
|
+
}
|
|
1300
|
+
emitMessage(options, messages, finalText);
|
|
1301
|
+
return { exitCode: 0, signal: null, messages, stdout, stderr };
|
|
1302
|
+
}
|
|
1303
|
+
finally {
|
|
1304
|
+
options.signal?.removeEventListener("abort", abort);
|
|
1305
|
+
clearTimeout(timeout);
|
|
1306
|
+
if (forceKill)
|
|
1307
|
+
clearTimeout(forceKill);
|
|
1308
|
+
if (!lifecycle.isClosed() && child.exitCode === null) {
|
|
1309
|
+
await terminateAndWait(child, lifecycle, 5_000);
|
|
1310
|
+
}
|
|
1311
|
+
}
|
|
1312
|
+
}
|
|
1071
1313
|
export async function runAdapter(options) {
|
|
1072
1314
|
if (!options.prompt.trim())
|
|
1073
|
-
throw new CliError(
|
|
1315
|
+
throw new CliError("Agent prompt cannot be empty.");
|
|
1074
1316
|
if (options.codingCommand &&
|
|
1075
|
-
(options.profile?.chatOnly === true ||
|
|
1076
|
-
|
|
1317
|
+
(options.profile?.chatOnly === true ||
|
|
1318
|
+
options.profile?.permissionPreset === "read_only")) {
|
|
1319
|
+
throw new CliError("A custom coding command cannot prove read-only or chat-only enforcement.");
|
|
1320
|
+
}
|
|
1321
|
+
if (!options.codingCommand &&
|
|
1322
|
+
usesAcpRuntime(options.adapter, options.profile)) {
|
|
1323
|
+
return runAcpxAdapter(options);
|
|
1077
1324
|
}
|
|
1078
|
-
if (options.adapter ===
|
|
1079
|
-
(options.profile?.chatOnly === true ||
|
|
1080
|
-
|
|
1325
|
+
if (options.adapter === "openclaw" &&
|
|
1326
|
+
(options.profile?.chatOnly === true ||
|
|
1327
|
+
options.profile?.permissionPreset === "read_only")) {
|
|
1328
|
+
throw new CliError("OpenClaw read-only and chat-only policy cannot be verified per run.");
|
|
1081
1329
|
}
|
|
1082
|
-
if (options.adapter ===
|
|
1330
|
+
if (options.adapter === "hermes" && !options.codingCommand)
|
|
1083
1331
|
return runHermesAcp(options);
|
|
1084
1332
|
return runSpawnAdapter(options);
|
|
1085
1333
|
}
|
|
1086
1334
|
export async function probeAdapter(adapter, spawnProcess = spawn, timeoutMs) {
|
|
1087
1335
|
const plan = adapterProbePlan(adapter);
|
|
1088
1336
|
const effectiveTimeoutMs = timeoutMs ?? plan.timeoutMs;
|
|
1089
|
-
const command = adapter;
|
|
1337
|
+
const command = ACPX_PROBE_COMMANDS[adapter] ?? adapter;
|
|
1090
1338
|
const probeArgs = plan.args;
|
|
1091
1339
|
let child;
|
|
1092
1340
|
try {
|
|
1093
1341
|
child = spawnProcess(command, probeArgs, {
|
|
1094
1342
|
env: runtimeEnvironment(),
|
|
1095
1343
|
shell: false,
|
|
1096
|
-
detached: process.platform !==
|
|
1097
|
-
stdio: [
|
|
1344
|
+
detached: process.platform !== "win32",
|
|
1345
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
1098
1346
|
});
|
|
1099
1347
|
}
|
|
1100
1348
|
catch (error) {
|
|
1101
|
-
return {
|
|
1349
|
+
return {
|
|
1350
|
+
available: false,
|
|
1351
|
+
error: error instanceof Error ? error.message : String(error),
|
|
1352
|
+
};
|
|
1102
1353
|
}
|
|
1103
1354
|
const lifecycle = observeChild(child);
|
|
1104
|
-
let output =
|
|
1355
|
+
let output = "";
|
|
1105
1356
|
let outputBytes = 0;
|
|
1106
1357
|
let outputLimitExceeded = false;
|
|
1107
1358
|
let forceKill;
|
|
@@ -1118,19 +1369,19 @@ export async function probeAdapter(adapter, spawnProcess = spawn, timeoutMs) {
|
|
|
1118
1369
|
if (lifecycle.isClosed() || child.exitCode !== null || terminating)
|
|
1119
1370
|
return;
|
|
1120
1371
|
terminating = true;
|
|
1121
|
-
terminateChild(child,
|
|
1372
|
+
terminateChild(child, "SIGTERM");
|
|
1122
1373
|
forceKill = setTimeout(() => {
|
|
1123
1374
|
if (!lifecycle.isClosed())
|
|
1124
|
-
terminateChild(child,
|
|
1375
|
+
terminateChild(child, "SIGKILL");
|
|
1125
1376
|
}, terminationGraceMs);
|
|
1126
1377
|
hardStop = setTimeout(() => {
|
|
1127
1378
|
if (lifecycle.isClosed())
|
|
1128
1379
|
return;
|
|
1129
1380
|
forcedClosed = true;
|
|
1130
|
-
terminateChild(child,
|
|
1381
|
+
terminateChild(child, "SIGKILL");
|
|
1131
1382
|
destroyChildPipes(child);
|
|
1132
1383
|
const message = outputLimitExceeded
|
|
1133
|
-
?
|
|
1384
|
+
? "probe output exceeded 1048576 bytes"
|
|
1134
1385
|
: `probe timed out after ${String(effectiveTimeoutMs)}ms`;
|
|
1135
1386
|
rejectTermination?.(new CliError(message));
|
|
1136
1387
|
}, terminationGraceMs + 100);
|
|
@@ -1146,13 +1397,13 @@ export async function probeAdapter(adapter, spawnProcess = spawn, timeoutMs) {
|
|
|
1146
1397
|
}
|
|
1147
1398
|
output += chunk.toString();
|
|
1148
1399
|
};
|
|
1149
|
-
child.stdout.on(
|
|
1400
|
+
child.stdout.on("data", (chunk) => {
|
|
1150
1401
|
consume(chunk);
|
|
1151
1402
|
});
|
|
1152
|
-
child.stderr.on(
|
|
1403
|
+
child.stderr.on("data", (chunk) => {
|
|
1153
1404
|
consume(chunk);
|
|
1154
1405
|
});
|
|
1155
|
-
child.stdin.on(
|
|
1406
|
+
child.stdin.on("error", () => undefined);
|
|
1156
1407
|
child.stdin.end();
|
|
1157
1408
|
const timeout = setTimeout(() => {
|
|
1158
1409
|
timedOut = true;
|
|
@@ -1162,23 +1413,32 @@ export async function probeAdapter(adapter, spawnProcess = spawn, timeoutMs) {
|
|
|
1162
1413
|
const [code] = (await Promise.race([
|
|
1163
1414
|
lifecycle.close,
|
|
1164
1415
|
terminationFailure,
|
|
1165
|
-
once(child,
|
|
1416
|
+
once(child, "error").then(([error]) => {
|
|
1166
1417
|
throw error;
|
|
1167
1418
|
}),
|
|
1168
1419
|
]));
|
|
1169
1420
|
if (timedOut)
|
|
1170
|
-
return {
|
|
1421
|
+
return {
|
|
1422
|
+
available: false,
|
|
1423
|
+
error: `probe timed out after ${String(effectiveTimeoutMs)}ms`,
|
|
1424
|
+
};
|
|
1171
1425
|
if (outputLimitExceeded)
|
|
1172
|
-
return { available: false, error:
|
|
1173
|
-
const version = adapter ===
|
|
1174
|
-
?
|
|
1426
|
+
return { available: false, error: "probe output exceeded 1048576 bytes" };
|
|
1427
|
+
const version = adapter === "hermes" || adapter === "openclaw"
|
|
1428
|
+
? "ready"
|
|
1175
1429
|
: output.trim().split(/\r?\n/, 1)[0];
|
|
1176
1430
|
return code === 0
|
|
1177
1431
|
? { available: true, ...(version ? { version } : {}) }
|
|
1178
|
-
: {
|
|
1432
|
+
: {
|
|
1433
|
+
available: false,
|
|
1434
|
+
error: version || `exited with code ${String(code)}`,
|
|
1435
|
+
};
|
|
1179
1436
|
}
|
|
1180
1437
|
catch (error) {
|
|
1181
|
-
return {
|
|
1438
|
+
return {
|
|
1439
|
+
available: false,
|
|
1440
|
+
error: error instanceof Error ? error.message : String(error),
|
|
1441
|
+
};
|
|
1182
1442
|
}
|
|
1183
1443
|
finally {
|
|
1184
1444
|
clearTimeout(timeout);
|