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/dist/adapters.js CHANGED
@@ -1,31 +1,55 @@
1
- import { spawn } from 'node:child_process';
2
- import { once } from 'node:events';
3
- import { chmod, mkdtemp, realpath, rm, writeFile } from 'node:fs/promises';
4
- import { tmpdir } from 'node:os';
5
- import { join } from 'node:path';
6
- import { AgentAdapterSchema } from 'crewx-agent-protocol';
7
- import { CLI_VERSION, DEFAULT_RUN_TIMEOUT_MS, MAX_AGENT_OUTPUT_BYTES } from './constants.js';
8
- import { CliError } from './errors.js';
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 === 'hermes'
14
- ? { args: ['acp', '--check'], timeoutMs: DEFAULT_RUNTIME_PROBE_TIMEOUT_MS }
15
- : adapter === 'openclaw'
16
- ? { args: ['agents', 'list', '--json'], timeoutMs: OPENCLAW_RUNTIME_PROBE_TIMEOUT_MS }
17
- : { args: ['--version'], timeoutMs: DEFAULT_RUNTIME_PROBE_TIMEOUT_MS };
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
- 'WebSearch',
21
- 'WebFetch',
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 !== 'win32' && child.pid) {
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('close', (exitCode, signal) => {
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, 'SIGTERM');
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, 'SIGKILL');
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}". Choose codex, claude, pi, hermes, or openclaw.`);
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 ? ['--model', profile.model] : [];
101
- const restricted = profile.chatOnly === true || profile.permissionPreset === 'read_only';
102
- const codexPermissionArgs = profile.permissionPreset === 'full_access'
103
- ? ['--dangerously-bypass-approvals-and-sandbox']
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
- '-c',
129
+ "-c",
106
130
  'approval_policy="never"',
107
- '--sandbox',
108
- restricted ? 'read-only' : 'workspace-write',
131
+ "--sandbox",
132
+ restricted ? "read-only" : "workspace-write",
109
133
  ];
110
134
  switch (adapter) {
111
- case 'codex':
135
+ case "codex":
112
136
  return {
113
- command: 'codex',
137
+ command: "codex",
114
138
  args: [
115
- '--search',
116
- 'exec',
117
- '--json',
118
- '--color',
119
- 'never',
120
- '--skip-git-repo-check',
121
- '--ephemeral',
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: 'jsonl',
127
- prompt: { type: 'stdin' },
150
+ output: "jsonl",
151
+ prompt: { type: "stdin" },
128
152
  };
129
- case 'claude':
153
+ case "claude":
130
154
  return {
131
- command: 'claude',
155
+ command: "claude",
132
156
  args: [
133
- '--print',
134
- '--output-format',
135
- 'stream-json',
136
- '--verbose',
137
- '--no-session-persistence',
157
+ "--print",
158
+ "--output-format",
159
+ "stream-json",
160
+ "--verbose",
161
+ "--no-session-persistence",
138
162
  ...modelArgs,
139
- ...(!restricted && profile.permissionPreset !== 'full_access'
140
- ? ['--allowedTools', ...CLAUDE_STANDARD_CREWX_TOOLS]
163
+ ...(!restricted && profile.permissionPreset !== "full_access"
164
+ ? ["--allowedTools", ...CLAUDE_STANDARD_CREWX_TOOLS]
141
165
  : []),
142
166
  ...(restricted
143
- ? ['--permission-mode', 'plan']
144
- : profile.permissionPreset === 'full_access'
145
- ? ['--dangerously-skip-permissions']
146
- : ['--permission-mode', 'acceptEdits']),
167
+ ? ["--permission-mode", "plan"]
168
+ : profile.permissionPreset === "full_access"
169
+ ? ["--dangerously-skip-permissions"]
170
+ : ["--permission-mode", "acceptEdits"]),
147
171
  ],
148
- output: 'jsonl',
149
- prompt: { type: 'stdin' },
172
+ output: "jsonl",
173
+ prompt: { type: "stdin" },
150
174
  };
151
- case 'pi':
175
+ case "pi":
152
176
  return {
153
- command: 'pi',
177
+ command: "pi",
154
178
  args: [
155
- '--mode',
156
- 'json',
157
- '--print',
158
- '--no-approve',
159
- '--no-session',
179
+ "--mode",
180
+ "json",
181
+ "--print",
182
+ "--no-approve",
183
+ "--no-session",
160
184
  ...modelArgs,
161
- ...(restricted ? ['--tools', 'read,grep,find,ls'] : []),
185
+ ...(restricted ? ["--tools", "read,grep,find,ls"] : []),
162
186
  ],
163
- output: 'jsonl',
164
- prompt: { type: 'stdin' },
187
+ output: "jsonl",
188
+ prompt: { type: "stdin" },
165
189
  };
166
- case 'hermes':
190
+ case "hermes":
167
191
  return {
168
- command: 'hermes',
169
- args: ['acp'],
170
- output: 'jsonl',
171
- prompt: { type: 'stdin' },
192
+ command: "hermes",
193
+ args: ["acp"],
194
+ output: "jsonl",
195
+ prompt: { type: "stdin" },
172
196
  };
173
- case 'openclaw':
197
+ case "openclaw":
174
198
  if (!profile.runtimeAgentId?.trim()) {
175
- throw new CliError('OpenClaw requires an explicit configured agent ID.');
199
+ throw new CliError("OpenClaw requires an explicit configured agent ID.");
176
200
  }
177
201
  return {
178
- command: 'openclaw',
202
+ command: "openclaw",
179
203
  args: [
180
- 'agent',
181
- '--local',
182
- '--agent',
204
+ "agent",
205
+ "--local",
206
+ "--agent",
183
207
  profile.runtimeAgentId.trim(),
184
- '--session-key',
185
- profile.sessionKey ?? 'main',
186
- '--json',
187
- '--timeout',
188
- '0',
208
+ "--session-key",
209
+ profile.sessionKey ?? "main",
210
+ "--json",
211
+ "--timeout",
212
+ "0",
189
213
  ...modelArgs,
190
214
  ],
191
- output: 'json',
192
- prompt: { type: 'file', flag: '--message-file' },
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 === '\\' && quote !== 'single') {
235
+ if (character === "\\" && quote !== "single") {
210
236
  escaped = true;
211
237
  started = true;
212
238
  continue;
213
239
  }
214
- if (character === "'" && quote !== 'double') {
215
- quote = quote === 'single' ? undefined : 'single';
240
+ if (character === "'" && quote !== "double") {
241
+ quote = quote === "single" ? undefined : "single";
216
242
  started = true;
217
243
  continue;
218
244
  }
219
- if (character === '"' && quote !== 'single') {
220
- quote = quote === 'double' ? undefined : 'double';
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('The coding command contains an unfinished quote or escape.');
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('The coding command cannot be empty.');
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) => (argument === '{prompt}' ? [index] : []));
273
+ const promptIndexes = args.flatMap((argument, index) => argument === "{prompt}" ? [index] : []);
248
274
  if (promptIndexes.length > 1)
249
- throw new CliError('Use {prompt} at most once in the coding command.');
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: 'text',
257
- prompt: { type: 'argument', index },
282
+ output: "text",
283
+ prompt: { type: "argument", index },
258
284
  };
259
285
  }
260
286
  function contentText(value) {
261
- if (typeof value === 'string' && value.trim())
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 === 'string') {
293
+ if (typeof part === "string") {
268
294
  parts.push(part);
269
295
  }
270
- else if (typeof part === 'object' && part !== null) {
296
+ else if (typeof part === "object" && part !== null) {
271
297
  const record = part;
272
- if ((record.type === 'text' || record.type === 'output_text') && typeof record.text === 'string') {
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('') : undefined;
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 !== 'agent_end' || event.willRetry === true || !Array.isArray(event.messages)) {
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 !== 'object' || candidate === null)
318
+ if (typeof candidate !== "object" || candidate === null)
290
319
  continue;
291
320
  const message = candidate;
292
- if (message.role !== 'assistant')
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 !== 'object' || raw === null)
337
+ if (typeof raw !== "object" || raw === null)
309
338
  return { raw };
310
339
  const event = raw;
311
- const eventType = typeof event.type === 'string' ? event.type : undefined;
340
+ const eventType = typeof event.type === "string" ? event.type : undefined;
312
341
  let message;
313
342
  let role;
314
- if (adapter === 'codex') {
315
- const item = typeof event.item === 'object' && event.item !== null ? event.item : undefined;
316
- if (item?.type === 'agent_message') {
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 = 'assistant';
349
+ role = "assistant";
319
350
  }
320
- else if (eventType === 'agent_message') {
351
+ else if (eventType === "agent_message") {
321
352
  message = contentText(event.message) ?? contentText(event.text);
322
- role = 'assistant';
353
+ role = "assistant";
323
354
  }
324
355
  }
325
- else if (adapter === 'claude') {
326
- const messageObject = typeof event.message === 'object' && event.message !== null
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 === 'result' && event.is_error !== true) {
360
+ if (eventType === "result" && event.is_error !== true) {
330
361
  message = contentText(event.result);
331
- role = 'assistant';
362
+ role = "assistant";
332
363
  }
333
- else if (eventType === 'assistant' || messageObject?.role === 'assistant') {
364
+ else if (eventType === "assistant" ||
365
+ messageObject?.role === "assistant") {
334
366
  message = contentText(messageObject?.content ?? event.content);
335
- role = 'assistant';
367
+ role = "assistant";
336
368
  }
337
- else if (eventType === 'content_block_delta') {
338
- const delta = typeof event.delta === 'object' && event.delta !== null ? event.delta : undefined;
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 = 'assistant';
374
+ role = "assistant";
341
375
  }
342
376
  }
343
- else if (adapter === 'pi') {
377
+ else if (adapter === "pi") {
344
378
  message = finalPiAssistantMessage(event);
345
379
  if (message) {
346
- role = 'assistant';
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 === 'object' && value !== null && !Array.isArray(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('openclaw_workspace_unavailable', `${label} does not resolve to an accessible local directory.`, error);
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('openclaw', args, {
418
+ child = spawnProcess("openclaw", args, {
385
419
  cwd: options.cwd,
386
420
  env: runtimeEnvironment(options.environment),
387
421
  shell: false,
388
- detached: process.platform !== 'win32',
389
- stdio: ['pipe', 'pipe', 'pipe'],
422
+ detached: process.platform !== "win32",
423
+ stdio: ["pipe", "pipe", "pipe"],
390
424
  });
391
425
  }
392
426
  catch (error) {
393
- throw openClawError('openclaw_unavailable', 'Unable to start openclaw. Is it installed and on PATH?', error);
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, 'SIGTERM');
404
- forceKill ??= setTimeout(() => terminateChild(child, 'SIGKILL'), 5_000);
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 === 'stdout')
450
+ if (stream === "stdout")
417
451
  stdout += chunk.toString();
418
452
  else
419
453
  stderr += chunk.toString();
420
454
  };
421
- child.stdout.on('data', (chunk) => consume('stdout', chunk));
422
- child.stderr.on('data', (chunk) => consume('stderr', chunk));
423
- child.stdin.on('error', () => undefined);
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, 'close'),
434
- once(child, 'error').then(([error]) => {
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('openclaw_probe_timeout', `OpenClaw preflight timed out while running ${[args[0], args[1]].filter(Boolean).join(' ')}.`);
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('openclaw_probe_output_too_large', 'OpenClaw preflight exceeded its 1048576 byte output limit.');
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('openclaw_probe_failed', `OpenClaw preflight failed: ${publicOpenClawDiagnostic(stderr.trim()) || `exit ${String(code)}`}`);
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('openclaw_probe_invalid_json', 'OpenClaw preflight returned invalid JSON.', error);
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('openclaw_agent_id_missing', 'OpenClaw requires an explicit configured agent ID.');
499
+ throw openClawError("openclaw_agent_id_missing", "OpenClaw requires an explicit configured agent ID.");
466
500
  }
467
- const desiredWorkspace = await resolvedPath(options.cwd, 'CrewX assignment directory');
468
- let agents = await openClawJsonProbe(['agents', 'list', '--json'], options);
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('openclaw_agent_list_invalid', 'OpenClaw returned an invalid agent list.');
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('crewx-')) {
507
+ if (!selected && agentId.startsWith("crewx-")) {
474
508
  await openClawJsonProbe([
475
- 'agents',
476
- 'add',
509
+ "agents",
510
+ "add",
477
511
  agentId,
478
- '--workspace',
512
+ "--workspace",
479
513
  desiredWorkspace,
480
- '--non-interactive',
481
- '--json',
514
+ "--non-interactive",
515
+ "--json",
482
516
  ], options, OPENCLAW_SETUP_TIMEOUT_MS);
483
- agents = await openClawJsonProbe(['agents', 'list', '--json'], options);
517
+ agents = await openClawJsonProbe(["agents", "list", "--json"], options);
484
518
  if (!Array.isArray(agents)) {
485
- throw openClawError('openclaw_agent_list_invalid', 'OpenClaw returned an invalid agent list after setup.');
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('openclaw_agent_setup_failed', `CrewX created OpenClaw agent "${agentId}", but OpenClaw did not return it.`);
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('openclaw_agent_missing', `OpenClaw agent "${agentId}" is not configured. Run \`openclaw agents list --json\` to choose one.`);
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 !== 'string' || !selected.workspace.trim()) {
496
- throw openClawError('openclaw_workspace_missing', `OpenClaw agent "${agentId}" does not report a workspace.`);
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('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.`);
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(['models', 'status', '--agent', agentId, '--json'], options, OPENCLAW_SETUP_TIMEOUT_MS));
503
- if (!models || (typeof models.agentId === 'string' && models.agentId !== agentId)) {
504
- throw openClawError('openclaw_model_status_invalid', `OpenClaw did not return model status for agent "${agentId}". Run \`openclaw models status --agent "${agentId}" --json\`.`);
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 === 'string' && models.resolvedDefault.trim()
541
+ const resolvedModel = typeof models.resolvedDefault === "string" && models.resolvedDefault.trim()
507
542
  ? models.resolvedDefault.trim()
508
- : typeof models.defaultModel === 'string' && models.defaultModel.trim()
543
+ : typeof models.defaultModel === "string" && models.defaultModel.trim()
509
544
  ? models.defaultModel.trim()
510
545
  : undefined;
511
546
  if (!resolvedModel) {
512
- throw openClawError('openclaw_model_missing', `OpenClaw agent "${agentId}" has no default model. Configure one, then run \`openclaw models status --agent "${agentId}" --json\`.`);
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 === 'string')
550
+ ? models.allowed.filter((value) => typeof value === "string")
516
551
  : [];
517
552
  if (allowedModels.length > 0 && !allowedModels.includes(resolvedModel)) {
518
- 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.`);
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 === 'string')
557
+ ? auth.missingProvidersInUse.filter((value) => typeof value === "string")
523
558
  : [];
524
559
  if (missingProviders.length > 0) {
525
- 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\`.`);
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 === 'usable')) {
532
- 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.`);
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 ?? 'main';
569
+ const sessionLeaf = options.profile.sessionKey ?? "main";
535
570
  const fullSessionKey = `agent:${agentId}:${sessionLeaf}`;
536
- const explanation = record(await openClawJsonProbe(['sandbox', 'explain', '--session', fullSessionKey, '--json'], options));
571
+ const explanation = record(await openClawJsonProbe(["sandbox", "explain", "--session", fullSessionKey, "--json"], options));
537
572
  const sandbox = record(explanation?.sandbox);
538
- if (explanation?.agentId !== agentId || explanation?.sessionKey !== fullSessionKey || !sandbox) {
539
- throw openClawError('openclaw_sandbox_invalid', 'OpenClaw sandbox preflight did not describe the requested agent session.');
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 === 'string'
542
- ? await resolvedPath(sandbox.effectiveHostWorkspaceRoot, 'OpenClaw effective workspace')
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('openclaw_sandbox_workspace_mismatch', 'OpenClaw sandbox policy does not expose the exact CrewX assignment directory.');
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 !== 'docker' ||
549
- sandbox.workspaceAccess !== 'rw' ||
550
- typeof sandbox.runtimeWorkdir !== 'string') {
551
- throw openClawError('openclaw_sandbox_read_only', 'OpenClaw must grant rw workspace access for CrewX coding assignments.');
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) ? sandbox.workspaceMounts.map(record) : [];
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 === 'string' &&
557
- typeof mount.containerRoot === 'string' &&
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, 'OpenClaw sandbox mount')) === desiredWorkspace) {
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('openclaw_sandbox_mount_missing', 'OpenClaw sandbox policy does not mount the CrewX assignment directory read-write at its runtime workdir.');
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 !== 'string') {
571
- throw openClawError('openclaw_runtime_workdir_missing', 'OpenClaw did not report its direct runtime working directory.');
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, 'OpenClaw runtime working directory');
613
+ const runtimeWorkdir = await resolvedPath(sandbox.runtimeWorkdir, "OpenClaw runtime working directory");
574
614
  if (runtimeWorkdir !== desiredWorkspace) {
575
- throw openClawError('openclaw_runtime_workdir_mismatch', 'OpenClaw direct runtime workdir does not match the CrewX assignment directory.');
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 === 'string' && text.trim() ? [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('\n');
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
- 'CREWX_URL',
613
- 'CREWX_TOKEN',
614
- 'CREWX_CLI_PATH',
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('CREWX_'))
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 === 'stdin') {
639
- return { args: invocation.args, stdin: prompt, cleanup: async () => undefined };
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 === 'argument') {
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: '', cleanup: async () => undefined };
688
+ return { args, stdin: "", cleanup: async () => undefined };
645
689
  }
646
- const directory = await mkdtemp(join(tmpdir(), 'crewx-prompt-'));
690
+ const directory = await mkdtemp(join(tmpdir(), "crewx-prompt-"));
647
691
  try {
648
692
  await chmod(directory, 0o700);
649
- const path = join(directory, 'prompt.txt');
650
- await writeFile(path, prompt, { encoding: 'utf8', flag: 'wx', mode: 0o600 });
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 = 'assistant') {
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('Agent prompt cannot be empty.');
719
+ throw new CliError("Agent prompt cannot be empty.");
672
720
  const invocation = buildAdapterInvocation(options.adapter, options.profile, options.codingCommand);
673
- if (options.adapter === 'openclaw' && !options.codingCommand) {
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 !== 'win32',
690
- stdio: ['pipe', 'pipe', 'pipe'],
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 === 'codex' || options.adapter === 'claude' || options.adapter === 'pi');
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 = { message: parsed.message, role: parsed.role ?? 'assistant' };
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 === 'openclaw' ? 60_000 : 5_000;
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, 'SIGTERM');
791
+ terminateChild(child, "SIGTERM");
739
792
  forceKill = setTimeout(() => {
740
793
  if (!lifecycle.isClosed())
741
- terminateChild(child, 'SIGKILL');
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, 'SIGKILL');
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('data', (chunk) => consumeOutput(stdoutLines.write, chunk));
764
- child.stderr.on('data', (chunk) => consumeOutput(stderrLines.write, chunk));
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('abort', abort, { once: true });
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('error', (error) => {
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('error', (error) => {
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, 'utf8');
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('\n').trim();
853
+ const completeOutput = stdout.join("\n").trim();
801
854
  if (exitCode === 0 && messages.length === 0 && completeOutput) {
802
- if (invocation.output === 'json') {
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 === 'text') {
866
+ else if (invocation.output === "text") {
814
867
  emitMessage(options, messages, completeOutput);
815
868
  }
816
869
  }
817
- if (exitCode === 0 && !options.signal?.aborted && options.adapter === 'openclaw' && messages.length === 0) {
818
- throw new CliError('OpenClaw completed without a visible assistant response.');
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 && !options.signal?.aborted && finalMessageOnly && messages.length === 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('abort', abort);
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 || options.profile?.permissionPreset === 'read_only';
898
+ const restricted = options.profile?.chatOnly === true ||
899
+ options.profile?.permissionPreset === "read_only";
840
900
  if (restricted) {
841
- throw new CliError('Hermes does not expose a trustworthy read-only ACP mode. Use a sandboxed custom command or choose standard/full access.');
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('hermes', ['acp'], {
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 !== 'win32',
851
- stdio: ['pipe', 'pipe', 'pipe'],
910
+ detached: process.platform !== "win32",
911
+ stdio: ["pipe", "pipe", "pipe"],
852
912
  });
853
913
  }
854
914
  catch (error) {
855
- throw new CliError('Unable to start hermes. Is it installed and on PATH?', 1, { cause: error });
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`, 'utf8');
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: '2.0', id, method, params });
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 ?? 'unknown error'}`);
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 === 'string' ? message.method : undefined;
902
- options.onStdout?.(line, { raw: value, ...(method ? { eventType: method } : {}) });
903
- if (typeof message?.id === 'number' && !method) {
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, 'result'))
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 === 'session/update') {
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 === 'agent_message_chunk' && content?.type === 'text' && typeof content.text === 'string') {
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 === 'session/request_permission' && (typeof message?.id === 'number' || typeof message?.id === 'string')) {
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
- ? ['allow_once', 'allow_session'].find((optionId) => offered.some((option) => record(option)?.optionId === optionId))
994
+ ? ["allow_once", "allow_session"].find((optionId) => offered.some((option) => record(option)?.optionId === optionId))
929
995
  : undefined;
930
- const fullAccess = options.profile?.permissionPreset === 'full_access';
996
+ const fullAccess = options.profile?.permissionPreset === "full_access";
931
997
  write({
932
- jsonrpc: '2.0',
998
+ jsonrpc: "2.0",
933
999
  id: message.id,
934
1000
  result: {
935
1001
  outcome: fullAccess && allowed
936
- ? { outcome: 'selected', optionId: allowed }
937
- : { outcome: 'cancelled' },
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('data', (chunk) => consumeOutput(stdoutLines.write, chunk));
958
- child.stderr.on('data', (chunk) => consumeOutput(stderrLines.write, chunk));
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('error', (error) => failPending(new CliError('Hermes ACP process failed.', 1, { cause: error })));
965
- child.stdin.once('error', (error) => failPending(new CliError('Hermes closed its ACP input unexpectedly.', 1, { cause: error })));
966
- child.once('close', () => failPending(new CliError('Hermes ACP process closed before the request completed.')));
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({ jsonrpc: '2.0', method: 'session/cancel', params: { sessionId } });
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, 'SIGTERM');
1045
+ terminateChild(child, "SIGTERM");
974
1046
  forceKill = setTimeout(() => {
975
1047
  if (!lifecycle.isClosed())
976
- terminateChild(child, 'SIGKILL');
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, 'SIGKILL');
1055
+ terminateChild(child, "SIGKILL");
984
1056
  destroyChildPipes(child);
985
- failPending(new CliError('Hermes did not close after forced termination.'));
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('abort', abort, { once: true });
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('initialize', {
1069
+ const initialized = record(await resultOf("initialize", {
998
1070
  protocolVersion: 1,
999
- clientCapabilities: { fs: { readTextFile: false, writeTextFile: false }, terminal: false },
1000
- clientInfo: { name: 'crewx', version: CLI_VERSION },
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('Hermes returned an unsupported ACP protocol version.');
1004
- const created = record(await resultOf('session/new', { cwd: options.cwd, mcpServers: [] }));
1005
- if (typeof created?.sessionId !== 'string' || !created.sessionId) {
1006
- throw new CliError('Hermes did not return an ACP session id.');
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('session/set_mode', {
1084
+ await resultOf("session/set_mode", {
1010
1085
  sessionId,
1011
- modeId: options.profile?.permissionPreset === 'full_access' ? 'dont_ask' : 'accept_edits',
1086
+ modeId: options.profile?.permissionPreset === "full_access"
1087
+ ? "dont_ask"
1088
+ : "accept_edits",
1012
1089
  });
1013
1090
  if (options.profile?.model) {
1014
- await resultOf('session/set_model', { sessionId, modelId: options.profile.model });
1091
+ await resultOf("session/set_model", {
1092
+ sessionId,
1093
+ modelId: options.profile.model,
1094
+ });
1015
1095
  }
1016
- await resultOf('session/prompt', {
1096
+ await resultOf("session/prompt", {
1017
1097
  sessionId,
1018
- prompt: [{ type: 'text', text: options.prompt }],
1098
+ prompt: [{ type: "text", text: options.prompt }],
1019
1099
  });
1020
1100
  if (timedOut)
1021
- throw new CliError('Hermes exceeded the CrewX run timeout.');
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('Hermes completed without a visible assistant response.');
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, 'SIGTERM'), 2_000);
1030
- const killTimer = setTimeout(() => terminateChild(child, 'SIGKILL'), 7_000);
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, 'SIGKILL');
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('abort', abort);
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('Agent prompt cannot be empty.');
1315
+ throw new CliError("Agent prompt cannot be empty.");
1074
1316
  if (options.codingCommand &&
1075
- (options.profile?.chatOnly === true || options.profile?.permissionPreset === 'read_only')) {
1076
- throw new CliError('A custom coding command cannot prove read-only or chat-only enforcement.');
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 === 'openclaw' &&
1079
- (options.profile?.chatOnly === true || options.profile?.permissionPreset === 'read_only')) {
1080
- throw new CliError('OpenClaw read-only and chat-only policy cannot be verified per run.');
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 === 'hermes' && !options.codingCommand)
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 !== 'win32',
1097
- stdio: ['pipe', 'pipe', 'pipe'],
1344
+ detached: process.platform !== "win32",
1345
+ stdio: ["pipe", "pipe", "pipe"],
1098
1346
  });
1099
1347
  }
1100
1348
  catch (error) {
1101
- return { available: false, error: error instanceof Error ? error.message : String(error) };
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, 'SIGTERM');
1372
+ terminateChild(child, "SIGTERM");
1122
1373
  forceKill = setTimeout(() => {
1123
1374
  if (!lifecycle.isClosed())
1124
- terminateChild(child, 'SIGKILL');
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, 'SIGKILL');
1381
+ terminateChild(child, "SIGKILL");
1131
1382
  destroyChildPipes(child);
1132
1383
  const message = outputLimitExceeded
1133
- ? 'probe output exceeded 1048576 bytes'
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('data', (chunk) => {
1400
+ child.stdout.on("data", (chunk) => {
1150
1401
  consume(chunk);
1151
1402
  });
1152
- child.stderr.on('data', (chunk) => {
1403
+ child.stderr.on("data", (chunk) => {
1153
1404
  consume(chunk);
1154
1405
  });
1155
- child.stdin.on('error', () => undefined);
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, 'error').then(([error]) => {
1416
+ once(child, "error").then(([error]) => {
1166
1417
  throw error;
1167
1418
  }),
1168
1419
  ]));
1169
1420
  if (timedOut)
1170
- return { available: false, error: `probe timed out after ${String(effectiveTimeoutMs)}ms` };
1421
+ return {
1422
+ available: false,
1423
+ error: `probe timed out after ${String(effectiveTimeoutMs)}ms`,
1424
+ };
1171
1425
  if (outputLimitExceeded)
1172
- return { available: false, error: 'probe output exceeded 1048576 bytes' };
1173
- const version = adapter === 'hermes' || adapter === 'openclaw'
1174
- ? 'ready'
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
- : { available: false, error: version || `exited with code ${String(code)}` };
1432
+ : {
1433
+ available: false,
1434
+ error: version || `exited with code ${String(code)}`,
1435
+ };
1179
1436
  }
1180
1437
  catch (error) {
1181
- return { available: false, error: error instanceof Error ? error.message : String(error) };
1438
+ return {
1439
+ available: false,
1440
+ error: error instanceof Error ? error.message : String(error),
1441
+ };
1182
1442
  }
1183
1443
  finally {
1184
1444
  clearTimeout(timeout);