acpx 0.1.0 → 0.1.3

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/cli.js CHANGED
@@ -2,54 +2,399 @@
2
2
 
3
3
  // src/cli.ts
4
4
  import { Command, CommanderError, InvalidArgumentError } from "commander";
5
+ import { realpathSync } from "fs";
6
+ import path3 from "path";
7
+ import { pathToFileURL } from "url";
8
+ import { findSkillsRoot, maybeHandleSkillflag } from "skillflag";
9
+
10
+ // src/agent-registry.ts
11
+ var AGENT_REGISTRY = {
12
+ codex: "npx @zed-industries/codex-acp",
13
+ claude: "npx @zed-industries/claude-agent-acp",
14
+ gemini: "gemini",
15
+ opencode: "npx opencode-ai",
16
+ pi: "npx pi-acp"
17
+ };
18
+ var DEFAULT_AGENT_NAME = "codex";
19
+ function normalizeAgentName(value) {
20
+ return value.trim().toLowerCase();
21
+ }
22
+ function resolveAgentCommand(agentName) {
23
+ const normalized = normalizeAgentName(agentName);
24
+ return AGENT_REGISTRY[normalized] ?? agentName;
25
+ }
26
+ function listBuiltInAgents() {
27
+ return Object.keys(AGENT_REGISTRY);
28
+ }
5
29
 
6
30
  // src/output.ts
31
+ var MAX_THOUGHT_CHARS = 900;
32
+ var MAX_INLINE_CHARS = 220;
33
+ var MAX_OUTPUT_CHARS = 2e3;
34
+ var MAX_OUTPUT_LINES = 28;
35
+ var MAX_LOCATION_ITEMS = 5;
36
+ var OUTPUT_PRIORITY_KEYS = [
37
+ "stdout",
38
+ "stderr",
39
+ "output",
40
+ "content",
41
+ "text",
42
+ "message",
43
+ "result",
44
+ "response",
45
+ "value"
46
+ ];
7
47
  function nowIso() {
8
48
  return (/* @__PURE__ */ new Date()).toISOString();
9
49
  }
10
50
  function asStatus(status) {
11
51
  return status ?? "unknown";
12
52
  }
53
+ function isFinalStatus(status) {
54
+ return status === "completed" || status === "failed";
55
+ }
56
+ function toStatusLabel(status) {
57
+ switch (status) {
58
+ case "in_progress":
59
+ return "running";
60
+ case "pending":
61
+ return "pending";
62
+ case "completed":
63
+ return "completed";
64
+ case "failed":
65
+ return "failed";
66
+ default:
67
+ return "running";
68
+ }
69
+ }
70
+ function asRecord(value) {
71
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
72
+ return void 0;
73
+ }
74
+ return value;
75
+ }
76
+ function collapseWhitespace(value) {
77
+ return value.replace(/\s+/g, " ").trim();
78
+ }
79
+ function truncate(value, maxChars) {
80
+ if (value.length <= maxChars) {
81
+ return value;
82
+ }
83
+ if (maxChars <= 3) {
84
+ return value.slice(0, maxChars);
85
+ }
86
+ return `${value.slice(0, maxChars - 3)}...`;
87
+ }
88
+ function toInline(value, maxChars = MAX_INLINE_CHARS) {
89
+ return truncate(collapseWhitespace(value), maxChars);
90
+ }
91
+ function indentBlock(value, prefix) {
92
+ return value.split("\n").map((line) => `${prefix}${line}`).join("\n");
93
+ }
94
+ function dedupeStrings(values) {
95
+ const seen = /* @__PURE__ */ new Set();
96
+ const result = [];
97
+ for (const value of values) {
98
+ if (seen.has(value)) {
99
+ continue;
100
+ }
101
+ seen.add(value);
102
+ result.push(value);
103
+ }
104
+ return result;
105
+ }
106
+ function safeJson(value, spacing) {
107
+ const seen = /* @__PURE__ */ new WeakSet();
108
+ try {
109
+ return JSON.stringify(
110
+ value,
111
+ (_key, entry) => {
112
+ if (typeof entry === "bigint") {
113
+ return `${entry}n`;
114
+ }
115
+ if (typeof entry === "function") {
116
+ return `[Function ${entry.name || "anonymous"}]`;
117
+ }
118
+ if (typeof entry === "symbol") {
119
+ return entry.toString();
120
+ }
121
+ if (entry && typeof entry === "object") {
122
+ if (seen.has(entry)) {
123
+ return "[Circular]";
124
+ }
125
+ seen.add(entry);
126
+ }
127
+ return entry;
128
+ },
129
+ spacing
130
+ );
131
+ } catch {
132
+ return void 0;
133
+ }
134
+ }
135
+ function readFirstString(source, keys) {
136
+ for (const key of keys) {
137
+ const value = source[key];
138
+ if (typeof value === "string" && value.trim()) {
139
+ return value.trim();
140
+ }
141
+ }
142
+ return void 0;
143
+ }
144
+ function readFirstStringArray(source, keys) {
145
+ for (const key of keys) {
146
+ const value = source[key];
147
+ if (!Array.isArray(value)) {
148
+ continue;
149
+ }
150
+ const entries = value.map((entry) => typeof entry === "string" ? entry.trim() : "").filter((entry) => entry.length > 0);
151
+ if (entries.length > 0) {
152
+ return entries;
153
+ }
154
+ }
155
+ return void 0;
156
+ }
157
+ function summarizeToolInput(rawInput) {
158
+ if (rawInput == null) {
159
+ return void 0;
160
+ }
161
+ if (typeof rawInput === "string" || typeof rawInput === "number" || typeof rawInput === "boolean") {
162
+ return toInline(String(rawInput));
163
+ }
164
+ const record = asRecord(rawInput);
165
+ if (record) {
166
+ const command = readFirstString(record, ["command", "cmd", "program"]);
167
+ const args = readFirstStringArray(record, ["args", "arguments"]);
168
+ if (command) {
169
+ const invocation = [command, ...args ?? []].join(" ");
170
+ return toInline(invocation);
171
+ }
172
+ const location = readFirstString(record, [
173
+ "path",
174
+ "file",
175
+ "filePath",
176
+ "filepath",
177
+ "target",
178
+ "uri",
179
+ "url"
180
+ ]);
181
+ if (location) {
182
+ return toInline(location);
183
+ }
184
+ const query = readFirstString(record, ["query", "pattern", "text", "search"]);
185
+ if (query) {
186
+ return toInline(query);
187
+ }
188
+ }
189
+ const json = safeJson(rawInput, 0);
190
+ return json ? toInline(json) : void 0;
191
+ }
192
+ function formatLocations(locations) {
193
+ if (!locations || locations.length === 0) {
194
+ return void 0;
195
+ }
196
+ const unique = /* @__PURE__ */ new Set();
197
+ for (const location of locations) {
198
+ const path4 = location.path?.trim();
199
+ if (!path4) {
200
+ continue;
201
+ }
202
+ const line = typeof location.line === "number" && Number.isFinite(location.line) ? `:${Math.max(1, Math.trunc(location.line))}` : "";
203
+ unique.add(`${path4}${line}`);
204
+ }
205
+ const items = [...unique];
206
+ if (items.length === 0) {
207
+ return void 0;
208
+ }
209
+ const visible = items.slice(0, MAX_LOCATION_ITEMS);
210
+ const hidden = items.length - visible.length;
211
+ if (hidden <= 0) {
212
+ return visible.join(", ");
213
+ }
214
+ return `${visible.join(", ")}, +${hidden} more`;
215
+ }
216
+ function summarizeDiff(path4, oldText, newText) {
217
+ const oldLines = oldText ? oldText.split("\n").length : 0;
218
+ const newLines = newText.split("\n").length;
219
+ const delta = newLines - oldLines;
220
+ if (delta === 0) {
221
+ return `diff ${path4} (line count unchanged)`;
222
+ }
223
+ const signedDelta = `${delta > 0 ? "+" : ""}${delta}`;
224
+ return `diff ${path4} (${signedDelta} lines)`;
225
+ }
226
+ function textFromContentBlock(content) {
227
+ switch (content.type) {
228
+ case "text":
229
+ return content.text;
230
+ case "resource_link":
231
+ return content.title ?? content.name ?? content.uri;
232
+ case "resource": {
233
+ if ("text" in content.resource && typeof content.resource.text === "string") {
234
+ return content.resource.text;
235
+ }
236
+ const uri = content.resource.uri;
237
+ const mimeType = content.resource.mimeType;
238
+ return `[resource] ${uri}${mimeType ? ` (${mimeType})` : ""}`;
239
+ }
240
+ case "image":
241
+ return `[image] ${content.mimeType}`;
242
+ case "audio":
243
+ return `[audio] ${content.mimeType}`;
244
+ default:
245
+ return void 0;
246
+ }
247
+ }
248
+ function summarizeToolContent(content) {
249
+ if (!content || content.length === 0) {
250
+ return void 0;
251
+ }
252
+ const fragments = [];
253
+ for (const entry of content) {
254
+ if (entry.type === "content") {
255
+ const text = textFromContentBlock(entry.content);
256
+ if (text && text.trim()) {
257
+ fragments.push(text.trimEnd());
258
+ }
259
+ continue;
260
+ }
261
+ if (entry.type === "diff") {
262
+ fragments.push(summarizeDiff(entry.path, entry.oldText, entry.newText));
263
+ continue;
264
+ }
265
+ if (entry.type === "terminal") {
266
+ fragments.push(`[terminal] ${entry.terminalId}`);
267
+ }
268
+ }
269
+ const unique = dedupeStrings(
270
+ fragments.map((fragment) => fragment.trim()).filter((fragment) => fragment.length > 0)
271
+ );
272
+ if (unique.length === 0) {
273
+ return void 0;
274
+ }
275
+ return unique.join("\n\n");
276
+ }
277
+ function extractOutputText(value, depth = 0, seen = /* @__PURE__ */ new Set()) {
278
+ if (value == null) {
279
+ return void 0;
280
+ }
281
+ if (typeof value === "string") {
282
+ const trimmed = value.trimEnd();
283
+ return trimmed.length > 0 ? trimmed : void 0;
284
+ }
285
+ if (typeof value === "number" || typeof value === "boolean") {
286
+ return String(value);
287
+ }
288
+ if (depth >= 4) {
289
+ return void 0;
290
+ }
291
+ if (Array.isArray(value)) {
292
+ const parts = value.map((entry) => extractOutputText(entry, depth + 1, seen)).filter((entry) => Boolean(entry));
293
+ if (parts.length === 0) {
294
+ return void 0;
295
+ }
296
+ return dedupeStrings(parts).join("\n");
297
+ }
298
+ const record = asRecord(value);
299
+ if (!record) {
300
+ return void 0;
301
+ }
302
+ if (seen.has(record)) {
303
+ return void 0;
304
+ }
305
+ seen.add(record);
306
+ const preferred = [];
307
+ for (const key of OUTPUT_PRIORITY_KEYS) {
308
+ if (!(key in record)) {
309
+ continue;
310
+ }
311
+ const extracted = extractOutputText(record[key], depth + 1, seen);
312
+ if (extracted) {
313
+ preferred.push(extracted);
314
+ }
315
+ }
316
+ const uniquePreferred = dedupeStrings(preferred);
317
+ if (uniquePreferred.length > 0) {
318
+ return uniquePreferred.join("\n");
319
+ }
320
+ const json = safeJson(record, 2);
321
+ if (!json || json === "{}") {
322
+ return void 0;
323
+ }
324
+ return json;
325
+ }
326
+ function summarizeToolOutput(rawOutput, content) {
327
+ const outputFromRaw = extractOutputText(rawOutput);
328
+ const outputFromContent = summarizeToolContent(content);
329
+ const fragments = dedupeStrings(
330
+ [outputFromRaw, outputFromContent].map((fragment) => fragment?.trim()).filter((fragment) => Boolean(fragment))
331
+ );
332
+ if (fragments.length === 0) {
333
+ return void 0;
334
+ }
335
+ return fragments.join("\n\n");
336
+ }
337
+ function limitOutputBlock(value) {
338
+ const normalized = value.replace(/\r\n/g, "\n").trim();
339
+ if (!normalized) {
340
+ return "";
341
+ }
342
+ const lines = normalized.split("\n");
343
+ const visible = lines.slice(0, MAX_OUTPUT_LINES);
344
+ let result = visible.join("\n");
345
+ if (lines.length > visible.length) {
346
+ const hidden = lines.length - visible.length;
347
+ result += `
348
+ ... (${hidden} more lines)`;
349
+ }
350
+ if (result.length > MAX_OUTPUT_CHARS) {
351
+ result = `${result.slice(0, MAX_OUTPUT_CHARS - 3)}...`;
352
+ }
353
+ return result;
354
+ }
13
355
  var TextOutputFormatter = class {
14
356
  stdout;
357
+ useColor;
358
+ toolStates = /* @__PURE__ */ new Map();
359
+ thoughtBuffer = "";
360
+ wroteAny = false;
361
+ atLineStart = true;
362
+ section = null;
15
363
  constructor(stdout) {
16
364
  this.stdout = stdout;
365
+ this.useColor = Boolean(stdout.isTTY);
17
366
  }
18
367
  onSessionUpdate(notification) {
19
368
  const update = notification.update;
369
+ if (update.sessionUpdate !== "agent_thought_chunk") {
370
+ this.flushThoughtBuffer();
371
+ }
20
372
  switch (update.sessionUpdate) {
21
373
  case "agent_message_chunk": {
22
374
  if (update.content.type === "text") {
23
- this.stdout.write(update.content.text);
375
+ this.writeAssistantChunk(update.content.text);
24
376
  }
25
377
  return;
26
378
  }
27
379
  case "agent_thought_chunk": {
28
380
  if (update.content.type === "text") {
29
- this.stdout.write(`
30
- [thought] ${update.content.text}
31
- `);
381
+ this.thoughtBuffer += update.content.text;
32
382
  }
33
383
  return;
34
384
  }
35
385
  case "tool_call": {
36
- this.stdout.write(`
37
- [tool] ${update.title} (${asStatus(update.status)})
38
- `);
386
+ this.renderToolUpdate(update);
39
387
  return;
40
388
  }
41
389
  case "tool_call_update": {
42
- const title = update.title ?? update.toolCallId;
43
- this.stdout.write(`
44
- [tool] ${title} (${asStatus(update.status)})
45
- `);
390
+ this.renderToolUpdate(update);
46
391
  return;
47
392
  }
48
393
  case "plan": {
49
- this.stdout.write("\n[plan]\n");
394
+ this.beginSection("plan");
395
+ this.writeLine(this.bold("[plan]"));
50
396
  for (const entry of update.entries) {
51
- this.stdout.write(`- (${entry.status}) ${entry.content}
52
- `);
397
+ this.writeLine(` - [${entry.status}] ${entry.content}`);
53
398
  }
54
399
  return;
55
400
  }
@@ -58,11 +403,181 @@ var TextOutputFormatter = class {
58
403
  }
59
404
  }
60
405
  onDone(stopReason) {
61
- this.stdout.write(`
62
- [done] ${stopReason}
63
- `);
406
+ this.flushThoughtBuffer();
407
+ this.beginSection("done");
408
+ this.writeLine(this.dim(`[done] ${stopReason}`));
64
409
  }
65
410
  flush() {
411
+ this.flushThoughtBuffer();
412
+ if (!this.atLineStart) {
413
+ this.write("\n");
414
+ }
415
+ }
416
+ write(chunk) {
417
+ if (!chunk) {
418
+ return;
419
+ }
420
+ this.stdout.write(chunk);
421
+ this.wroteAny = true;
422
+ this.atLineStart = chunk.endsWith("\n");
423
+ }
424
+ writeLine(line) {
425
+ this.write(`${line}
426
+ `);
427
+ }
428
+ beginSection(next) {
429
+ if (!this.atLineStart) {
430
+ this.write("\n");
431
+ }
432
+ if (this.wroteAny) {
433
+ this.write("\n");
434
+ }
435
+ this.section = next;
436
+ }
437
+ writeAssistantChunk(text) {
438
+ if (!text) {
439
+ return;
440
+ }
441
+ this.section = "assistant";
442
+ this.write(text);
443
+ }
444
+ flushThoughtBuffer() {
445
+ const thought = truncate(collapseWhitespace(this.thoughtBuffer), MAX_THOUGHT_CHARS);
446
+ this.thoughtBuffer = "";
447
+ if (!thought) {
448
+ return;
449
+ }
450
+ this.beginSection("thought");
451
+ this.writeLine(this.dim(`[thinking] ${thought}`));
452
+ }
453
+ renderToolUpdate(update) {
454
+ const state = this.getOrCreateToolState(update.toolCallId);
455
+ this.mergeToolState(state, update);
456
+ const status = asStatus(state.status);
457
+ if (isFinalStatus(status)) {
458
+ const signature = this.toolSignature(state);
459
+ if (signature !== state.finalSignature) {
460
+ state.finalSignature = signature;
461
+ this.renderFinalToolState(state, status);
462
+ }
463
+ return;
464
+ }
465
+ if (state.startedPrinted) {
466
+ return;
467
+ }
468
+ state.startedPrinted = true;
469
+ this.renderStartingToolState(state, status);
470
+ }
471
+ getOrCreateToolState(toolCallId) {
472
+ const existing = this.toolStates.get(toolCallId);
473
+ if (existing) {
474
+ return existing;
475
+ }
476
+ const created = {
477
+ id: toolCallId,
478
+ startedPrinted: false
479
+ };
480
+ this.toolStates.set(toolCallId, created);
481
+ return created;
482
+ }
483
+ mergeToolState(state, update) {
484
+ if (typeof update.title === "string" && update.title.trim().length > 0) {
485
+ state.title = update.title;
486
+ }
487
+ if (update.status !== void 0) {
488
+ state.status = update.status;
489
+ }
490
+ if (update.kind !== void 0) {
491
+ state.kind = update.kind;
492
+ }
493
+ if (update.locations !== void 0) {
494
+ state.locations = update.locations;
495
+ }
496
+ if (update.rawInput !== void 0) {
497
+ state.rawInput = update.rawInput;
498
+ }
499
+ if (update.rawOutput !== void 0) {
500
+ state.rawOutput = update.rawOutput;
501
+ }
502
+ if (update.content !== void 0) {
503
+ state.content = update.content;
504
+ }
505
+ }
506
+ toolSignature(state) {
507
+ const signaturePayload = {
508
+ title: state.title,
509
+ status: state.status,
510
+ kind: state.kind,
511
+ input: summarizeToolInput(state.rawInput),
512
+ files: formatLocations(state.locations),
513
+ output: summarizeToolOutput(state.rawOutput, state.content)
514
+ };
515
+ return safeJson(signaturePayload, 0) ?? JSON.stringify(signaturePayload);
516
+ }
517
+ renderStartingToolState(state, status) {
518
+ this.beginSection("tool");
519
+ const title = state.title ?? state.id;
520
+ const label = status === "pending" ? "pending" : "running";
521
+ const statusText = this.colorStatus(label, status);
522
+ this.writeLine(`${this.bold("[tool]")} ${title} (${statusText})`);
523
+ const input = summarizeToolInput(state.rawInput);
524
+ if (input) {
525
+ this.writeLine(` input: ${input}`);
526
+ }
527
+ const files = formatLocations(state.locations);
528
+ if (files) {
529
+ this.writeLine(` files: ${files}`);
530
+ }
531
+ }
532
+ renderFinalToolState(state, status) {
533
+ this.beginSection("tool");
534
+ const title = state.title ?? state.id;
535
+ const statusText = this.colorStatus(toStatusLabel(status), status);
536
+ this.writeLine(`${this.bold("[tool]")} ${title} (${statusText})`);
537
+ if (state.kind) {
538
+ this.writeLine(` kind: ${state.kind}`);
539
+ }
540
+ const input = summarizeToolInput(state.rawInput);
541
+ if (input) {
542
+ this.writeLine(` input: ${input}`);
543
+ }
544
+ const files = formatLocations(state.locations);
545
+ if (files) {
546
+ this.writeLine(` files: ${files}`);
547
+ }
548
+ const output = summarizeToolOutput(state.rawOutput, state.content);
549
+ if (output) {
550
+ this.writeLine(" output:");
551
+ this.writeLine(indentBlock(limitOutputBlock(output), " "));
552
+ }
553
+ }
554
+ formatAnsi(text, code) {
555
+ if (!this.useColor) {
556
+ return text;
557
+ }
558
+ return `\x1B[${code}m${text}\x1B[0m`;
559
+ }
560
+ bold(text) {
561
+ return this.formatAnsi(text, "1");
562
+ }
563
+ dim(text) {
564
+ return this.formatAnsi(text, "2");
565
+ }
566
+ colorStatus(text, status) {
567
+ if (!this.useColor) {
568
+ return text;
569
+ }
570
+ switch (status) {
571
+ case "completed":
572
+ return this.formatAnsi(text, "32");
573
+ case "failed":
574
+ return this.formatAnsi(text, "31");
575
+ case "pending":
576
+ case "in_progress":
577
+ case "unknown":
578
+ default:
579
+ return this.formatAnsi(text, "33");
580
+ }
66
581
  }
67
582
  };
68
583
  var JsonOutputFormatter = class {
@@ -190,7 +705,9 @@ function createOutputFormatter(format, options = {}) {
190
705
  }
191
706
 
192
707
  // src/session.ts
708
+ import { createHash, randomUUID as randomUUID2 } from "crypto";
193
709
  import fs2 from "fs/promises";
710
+ import net from "net";
194
711
  import os from "os";
195
712
  import path2 from "path";
196
713
 
@@ -200,9 +717,7 @@ import {
200
717
  PROTOCOL_VERSION,
201
718
  ndJsonStream
202
719
  } from "@agentclientprotocol/sdk";
203
- import {
204
- spawn
205
- } from "child_process";
720
+ import { spawn } from "child_process";
206
721
  import { randomUUID } from "crypto";
207
722
  import fs from "fs/promises";
208
723
  import path from "path";
@@ -463,9 +978,15 @@ var AcpClient = class {
463
978
  this.log(`spawning agent: ${command} ${args.join(" ")}`);
464
979
  const child = spawn(command, args, {
465
980
  cwd: this.options.cwd,
466
- stdio: ["pipe", "pipe", "inherit"]
981
+ stdio: ["pipe", "pipe", "pipe"]
467
982
  });
468
983
  await waitForSpawn(child);
984
+ child.stderr.on("data", (chunk) => {
985
+ if (!this.options.verbose) {
986
+ return;
987
+ }
988
+ process.stderr.write(chunk);
989
+ });
469
990
  const input = Writable.toWeb(child.stdin);
470
991
  const output = Readable.toWeb(child.stdout);
471
992
  const stream = ndJsonStream(input, output);
@@ -777,8 +1298,12 @@ var AcpClient = class {
777
1298
 
778
1299
  // src/session.ts
779
1300
  var SESSION_BASE_DIR = path2.join(os.homedir(), ".acpx", "sessions");
1301
+ var QUEUE_BASE_DIR = path2.join(os.homedir(), ".acpx", "queues");
780
1302
  var PROCESS_EXIT_GRACE_MS = 1500;
781
1303
  var PROCESS_POLL_MS = 50;
1304
+ var QUEUE_CONNECT_ATTEMPTS = 40;
1305
+ var QUEUE_CONNECT_RETRY_MS = 50;
1306
+ var DEFAULT_QUEUE_OWNER_TTL_MS = 3e5;
782
1307
  var TimeoutError = class extends Error {
783
1308
  constructor(timeoutMs) {
784
1309
  super(`Timed out after ${timeoutMs}ms`);
@@ -851,8 +1376,11 @@ function parseSessionRecord(raw) {
851
1376
  return null;
852
1377
  }
853
1378
  const record = raw;
1379
+ const name = record.name == null ? void 0 : typeof record.name === "string" && record.name.trim().length > 0 ? record.name.trim() : null;
854
1380
  const pid = record.pid == null ? void 0 : Number.isInteger(record.pid) && record.pid > 0 ? record.pid : null;
855
- if (typeof record.id !== "string" || typeof record.sessionId !== "string" || typeof record.agentCommand !== "string" || typeof record.cwd !== "string" || typeof record.createdAt !== "string" || typeof record.lastUsedAt !== "string" || pid === null) {
1381
+ const closed = record.closed == null ? false : typeof record.closed === "boolean" ? record.closed : null;
1382
+ const closedAt = record.closedAt == null ? void 0 : typeof record.closedAt === "string" ? record.closedAt : null;
1383
+ if (typeof record.id !== "string" || typeof record.sessionId !== "string" || typeof record.agentCommand !== "string" || typeof record.cwd !== "string" || name === null || typeof record.createdAt !== "string" || typeof record.lastUsedAt !== "string" || pid === null || closed === null || closedAt === null) {
856
1384
  return null;
857
1385
  }
858
1386
  return {
@@ -861,8 +1389,11 @@ function parseSessionRecord(raw) {
861
1389
  sessionId: record.sessionId,
862
1390
  agentCommand: record.agentCommand,
863
1391
  cwd: record.cwd,
1392
+ name,
864
1393
  createdAt: record.createdAt,
865
1394
  lastUsedAt: record.lastUsedAt,
1395
+ closed,
1396
+ closedAt,
866
1397
  pid
867
1398
  };
868
1399
  }
@@ -917,9 +1448,25 @@ function toPromptResult(stopReason, sessionId, client) {
917
1448
  function absolutePath(value) {
918
1449
  return path2.resolve(value);
919
1450
  }
1451
+ function normalizeName(value) {
1452
+ if (value == null) {
1453
+ return void 0;
1454
+ }
1455
+ const trimmed = value.trim();
1456
+ return trimmed.length > 0 ? trimmed : void 0;
1457
+ }
920
1458
  function isoNow() {
921
1459
  return (/* @__PURE__ */ new Date()).toISOString();
922
1460
  }
1461
+ function normalizeQueueOwnerTtlMs(ttlMs) {
1462
+ if (ttlMs == null) {
1463
+ return DEFAULT_QUEUE_OWNER_TTL_MS;
1464
+ }
1465
+ if (!Number.isFinite(ttlMs) || ttlMs < 0) {
1466
+ return DEFAULT_QUEUE_OWNER_TTL_MS;
1467
+ }
1468
+ return Math.round(ttlMs);
1469
+ }
923
1470
  function formatError(error) {
924
1471
  if (error instanceof Error) {
925
1472
  return error.message;
@@ -1017,89 +1564,682 @@ async function isLikelyMatchingProcess(pid, agentCommand) {
1017
1564
  return true;
1018
1565
  }
1019
1566
  }
1020
- async function runOnce(options) {
1021
- const output = options.outputFormatter;
1022
- const client = new AcpClient({
1023
- agentCommand: options.agentCommand,
1024
- cwd: absolutePath(options.cwd),
1025
- permissionMode: options.permissionMode,
1026
- verbose: options.verbose,
1027
- onSessionUpdate: (notification) => output.onSessionUpdate(notification)
1028
- });
1029
- try {
1030
- return await withInterrupt(
1031
- async () => {
1032
- await withTimeout(client.start(), options.timeoutMs);
1033
- const sessionId = await withTimeout(
1034
- client.createSession(absolutePath(options.cwd)),
1035
- options.timeoutMs
1036
- );
1037
- const response = await withTimeout(
1038
- client.prompt(sessionId, options.message),
1039
- options.timeoutMs
1040
- );
1041
- output.onDone(response.stopReason);
1042
- output.flush();
1043
- return toPromptResult(response.stopReason, sessionId, client);
1044
- },
1045
- async () => {
1046
- await client.close();
1047
- }
1048
- );
1049
- } finally {
1050
- await client.close();
1567
+ function asRecord2(value) {
1568
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
1569
+ return void 0;
1051
1570
  }
1571
+ return value;
1052
1572
  }
1053
- async function createSession(options) {
1054
- const client = new AcpClient({
1055
- agentCommand: options.agentCommand,
1056
- cwd: absolutePath(options.cwd),
1057
- permissionMode: options.permissionMode,
1058
- verbose: options.verbose
1059
- });
1060
- try {
1061
- return await withInterrupt(
1062
- async () => {
1063
- await withTimeout(client.start(), options.timeoutMs);
1064
- const sessionId = await withTimeout(
1065
- client.createSession(absolutePath(options.cwd)),
1066
- options.timeoutMs
1067
- );
1068
- const now = isoNow();
1069
- const record = {
1070
- id: sessionId,
1071
- sessionId,
1072
- agentCommand: options.agentCommand,
1073
- cwd: absolutePath(options.cwd),
1074
- createdAt: now,
1075
- lastUsedAt: now,
1076
- pid: client.getAgentPid(),
1077
- protocolVersion: client.initializeResult?.protocolVersion,
1078
- agentCapabilities: client.initializeResult?.agentCapabilities
1079
- };
1080
- await writeSessionRecord(record);
1081
- return record;
1082
- },
1083
- async () => {
1084
- await client.close();
1085
- }
1086
- );
1087
- } finally {
1088
- await client.close();
1573
+ function isPermissionMode(value) {
1574
+ return value === "approve-all" || value === "approve-reads" || value === "deny-all";
1575
+ }
1576
+ function parseQueueOwnerRecord(raw) {
1577
+ const record = asRecord2(raw);
1578
+ if (!record) {
1579
+ return null;
1089
1580
  }
1581
+ if (!Number.isInteger(record.pid) || record.pid <= 0 || typeof record.sessionId !== "string" || typeof record.socketPath !== "string") {
1582
+ return null;
1583
+ }
1584
+ return {
1585
+ pid: record.pid,
1586
+ sessionId: record.sessionId,
1587
+ socketPath: record.socketPath
1588
+ };
1090
1589
  }
1091
- async function sendSession(options) {
1092
- const output = options.outputFormatter;
1093
- const record = await resolveSessionRecord(options.sessionId);
1094
- const storedProcessAlive = isProcessAlive(record.pid);
1095
- if (storedProcessAlive && options.verbose) {
1096
- process.stderr.write(
1097
- `[acpx] saved session pid ${record.pid} is running; reconnecting with loadSession
1098
- `
1099
- );
1590
+ function parseQueueSubmitRequest(raw) {
1591
+ const request = asRecord2(raw);
1592
+ if (!request) {
1593
+ return null;
1100
1594
  }
1101
- const client = new AcpClient({
1102
- agentCommand: record.agentCommand,
1595
+ if (request.type !== "submit_prompt" || typeof request.requestId !== "string" || typeof request.message !== "string" || !isPermissionMode(request.permissionMode) || typeof request.waitForCompletion !== "boolean") {
1596
+ return null;
1597
+ }
1598
+ const timeoutRaw = request.timeoutMs;
1599
+ const timeoutMs = typeof timeoutRaw === "number" && Number.isFinite(timeoutRaw) && timeoutRaw > 0 ? Math.round(timeoutRaw) : void 0;
1600
+ return {
1601
+ type: "submit_prompt",
1602
+ requestId: request.requestId,
1603
+ message: request.message,
1604
+ permissionMode: request.permissionMode,
1605
+ timeoutMs,
1606
+ waitForCompletion: request.waitForCompletion
1607
+ };
1608
+ }
1609
+ function parseSessionSendResult(raw) {
1610
+ const result = asRecord2(raw);
1611
+ if (!result) {
1612
+ return null;
1613
+ }
1614
+ if (typeof result.stopReason !== "string" || typeof result.sessionId !== "string" || typeof result.resumed !== "boolean") {
1615
+ return null;
1616
+ }
1617
+ const permissionStats = asRecord2(result.permissionStats);
1618
+ const record = asRecord2(result.record);
1619
+ if (!permissionStats || !record) {
1620
+ return null;
1621
+ }
1622
+ const statsValid = typeof permissionStats.requested === "number" && typeof permissionStats.approved === "number" && typeof permissionStats.denied === "number" && typeof permissionStats.cancelled === "number";
1623
+ if (!statsValid) {
1624
+ return null;
1625
+ }
1626
+ const recordValid = typeof record.id === "string" && typeof record.sessionId === "string" && typeof record.agentCommand === "string" && typeof record.cwd === "string" && typeof record.createdAt === "string" && typeof record.lastUsedAt === "string";
1627
+ if (!recordValid) {
1628
+ return null;
1629
+ }
1630
+ return result;
1631
+ }
1632
+ function parseQueueOwnerMessage(raw) {
1633
+ const message = asRecord2(raw);
1634
+ if (!message || typeof message.type !== "string") {
1635
+ return null;
1636
+ }
1637
+ if (typeof message.requestId !== "string") {
1638
+ return null;
1639
+ }
1640
+ if (message.type === "accepted") {
1641
+ return {
1642
+ type: "accepted",
1643
+ requestId: message.requestId
1644
+ };
1645
+ }
1646
+ if (message.type === "session_update") {
1647
+ const notification = message.notification;
1648
+ if (!notification || typeof notification !== "object") {
1649
+ return null;
1650
+ }
1651
+ return {
1652
+ type: "session_update",
1653
+ requestId: message.requestId,
1654
+ notification
1655
+ };
1656
+ }
1657
+ if (message.type === "done") {
1658
+ if (typeof message.stopReason !== "string") {
1659
+ return null;
1660
+ }
1661
+ return {
1662
+ type: "done",
1663
+ requestId: message.requestId,
1664
+ stopReason: message.stopReason
1665
+ };
1666
+ }
1667
+ if (message.type === "result") {
1668
+ const parsedResult = parseSessionSendResult(message.result);
1669
+ if (!parsedResult) {
1670
+ return null;
1671
+ }
1672
+ return {
1673
+ type: "result",
1674
+ requestId: message.requestId,
1675
+ result: parsedResult
1676
+ };
1677
+ }
1678
+ if (message.type === "error") {
1679
+ if (typeof message.message !== "string") {
1680
+ return null;
1681
+ }
1682
+ return {
1683
+ type: "error",
1684
+ requestId: message.requestId,
1685
+ message: message.message
1686
+ };
1687
+ }
1688
+ return null;
1689
+ }
1690
+ function queueKeyForSession(sessionId) {
1691
+ return createHash("sha256").update(sessionId).digest("hex").slice(0, 24);
1692
+ }
1693
+ function queueLockFilePath(sessionId) {
1694
+ return path2.join(QUEUE_BASE_DIR, `${queueKeyForSession(sessionId)}.lock`);
1695
+ }
1696
+ function queueSocketPath(sessionId) {
1697
+ const key = queueKeyForSession(sessionId);
1698
+ if (process.platform === "win32") {
1699
+ return `\\\\.\\pipe\\acpx-${key}`;
1700
+ }
1701
+ return path2.join(QUEUE_BASE_DIR, `${key}.sock`);
1702
+ }
1703
+ async function ensureQueueDir() {
1704
+ await fs2.mkdir(QUEUE_BASE_DIR, { recursive: true });
1705
+ }
1706
+ async function removeSocketFile(socketPath) {
1707
+ if (process.platform === "win32") {
1708
+ return;
1709
+ }
1710
+ try {
1711
+ await fs2.unlink(socketPath);
1712
+ } catch (error) {
1713
+ if (error.code !== "ENOENT") {
1714
+ throw error;
1715
+ }
1716
+ }
1717
+ }
1718
+ async function readQueueOwnerRecord(sessionId) {
1719
+ const lockPath = queueLockFilePath(sessionId);
1720
+ try {
1721
+ const payload = await fs2.readFile(lockPath, "utf8");
1722
+ const parsed = parseQueueOwnerRecord(JSON.parse(payload));
1723
+ return parsed ?? void 0;
1724
+ } catch {
1725
+ return void 0;
1726
+ }
1727
+ }
1728
+ async function cleanupStaleQueueOwner(sessionId, owner) {
1729
+ const lockPath = queueLockFilePath(sessionId);
1730
+ const socketPath = owner?.socketPath ?? queueSocketPath(sessionId);
1731
+ await removeSocketFile(socketPath).catch(() => {
1732
+ });
1733
+ await fs2.unlink(lockPath).catch((error) => {
1734
+ if (error.code !== "ENOENT") {
1735
+ throw error;
1736
+ }
1737
+ });
1738
+ }
1739
+ async function tryAcquireQueueOwnerLease(sessionId) {
1740
+ await ensureQueueDir();
1741
+ const lockPath = queueLockFilePath(sessionId);
1742
+ const socketPath = queueSocketPath(sessionId);
1743
+ const payload = JSON.stringify(
1744
+ {
1745
+ pid: process.pid,
1746
+ sessionId,
1747
+ socketPath,
1748
+ createdAt: isoNow()
1749
+ },
1750
+ null,
1751
+ 2
1752
+ );
1753
+ try {
1754
+ await fs2.writeFile(lockPath, `${payload}
1755
+ `, {
1756
+ encoding: "utf8",
1757
+ flag: "wx"
1758
+ });
1759
+ await removeSocketFile(socketPath).catch(() => {
1760
+ });
1761
+ return { lockPath, socketPath };
1762
+ } catch (error) {
1763
+ if (error.code !== "EEXIST") {
1764
+ throw error;
1765
+ }
1766
+ const owner = await readQueueOwnerRecord(sessionId);
1767
+ if (!owner || !isProcessAlive(owner.pid)) {
1768
+ await cleanupStaleQueueOwner(sessionId, owner);
1769
+ }
1770
+ return void 0;
1771
+ }
1772
+ }
1773
+ async function releaseQueueOwnerLease(lease) {
1774
+ await removeSocketFile(lease.socketPath).catch(() => {
1775
+ });
1776
+ await fs2.unlink(lease.lockPath).catch((error) => {
1777
+ if (error.code !== "ENOENT") {
1778
+ throw error;
1779
+ }
1780
+ });
1781
+ }
1782
+ function shouldRetryQueueConnect(error) {
1783
+ const code = error.code;
1784
+ return code === "ENOENT" || code === "ECONNREFUSED";
1785
+ }
1786
+ async function waitMs(ms) {
1787
+ await new Promise((resolve) => {
1788
+ setTimeout(resolve, ms);
1789
+ });
1790
+ }
1791
+ async function connectToSocket(socketPath) {
1792
+ return await new Promise((resolve, reject) => {
1793
+ const socket = net.createConnection(socketPath);
1794
+ const onConnect = () => {
1795
+ socket.off("error", onError);
1796
+ resolve(socket);
1797
+ };
1798
+ const onError = (error) => {
1799
+ socket.off("connect", onConnect);
1800
+ reject(error);
1801
+ };
1802
+ socket.once("connect", onConnect);
1803
+ socket.once("error", onError);
1804
+ });
1805
+ }
1806
+ async function connectToQueueOwner(owner) {
1807
+ let lastError;
1808
+ for (let attempt = 0; attempt < QUEUE_CONNECT_ATTEMPTS; attempt += 1) {
1809
+ try {
1810
+ return await connectToSocket(owner.socketPath);
1811
+ } catch (error) {
1812
+ lastError = error;
1813
+ if (!shouldRetryQueueConnect(error)) {
1814
+ throw error;
1815
+ }
1816
+ if (!isProcessAlive(owner.pid)) {
1817
+ return void 0;
1818
+ }
1819
+ await waitMs(QUEUE_CONNECT_RETRY_MS);
1820
+ }
1821
+ }
1822
+ if (lastError && !shouldRetryQueueConnect(lastError)) {
1823
+ throw lastError;
1824
+ }
1825
+ return void 0;
1826
+ }
1827
+ function writeQueueMessage(socket, message) {
1828
+ if (socket.destroyed || !socket.writable) {
1829
+ return;
1830
+ }
1831
+ socket.write(`${JSON.stringify(message)}
1832
+ `);
1833
+ }
1834
+ var QueueTaskOutputFormatter = class {
1835
+ requestId;
1836
+ send;
1837
+ constructor(task) {
1838
+ this.requestId = task.requestId;
1839
+ this.send = task.send;
1840
+ }
1841
+ onSessionUpdate(notification) {
1842
+ this.send({
1843
+ type: "session_update",
1844
+ requestId: this.requestId,
1845
+ notification
1846
+ });
1847
+ }
1848
+ onDone(stopReason) {
1849
+ this.send({
1850
+ type: "done",
1851
+ requestId: this.requestId,
1852
+ stopReason
1853
+ });
1854
+ }
1855
+ flush() {
1856
+ }
1857
+ };
1858
+ var DISCARD_OUTPUT_FORMATTER = {
1859
+ onSessionUpdate() {
1860
+ },
1861
+ onDone() {
1862
+ },
1863
+ flush() {
1864
+ }
1865
+ };
1866
+ var SessionQueueOwner = class _SessionQueueOwner {
1867
+ server;
1868
+ pending = [];
1869
+ waiters = [];
1870
+ closed = false;
1871
+ constructor(server) {
1872
+ this.server = server;
1873
+ }
1874
+ static async start(lease) {
1875
+ const ownerRef = { current: void 0 };
1876
+ const server = net.createServer((socket) => {
1877
+ ownerRef.current?.handleConnection(socket);
1878
+ });
1879
+ ownerRef.current = new _SessionQueueOwner(server);
1880
+ await new Promise((resolve, reject) => {
1881
+ const onListening = () => {
1882
+ server.off("error", onError);
1883
+ resolve();
1884
+ };
1885
+ const onError = (error) => {
1886
+ server.off("listening", onListening);
1887
+ reject(error);
1888
+ };
1889
+ server.once("listening", onListening);
1890
+ server.once("error", onError);
1891
+ server.listen(lease.socketPath);
1892
+ });
1893
+ return ownerRef.current;
1894
+ }
1895
+ async close() {
1896
+ if (this.closed) {
1897
+ return;
1898
+ }
1899
+ this.closed = true;
1900
+ for (const waiter of this.waiters.splice(0)) {
1901
+ waiter(void 0);
1902
+ }
1903
+ for (const task of this.pending.splice(0)) {
1904
+ if (task.waitForCompletion) {
1905
+ task.send({
1906
+ type: "error",
1907
+ requestId: task.requestId,
1908
+ message: "Queue owner shutting down before prompt execution"
1909
+ });
1910
+ }
1911
+ task.close();
1912
+ }
1913
+ await new Promise((resolve) => {
1914
+ this.server.close(() => resolve());
1915
+ });
1916
+ }
1917
+ async nextTask(timeoutMs) {
1918
+ if (this.pending.length > 0) {
1919
+ return this.pending.shift();
1920
+ }
1921
+ if (this.closed) {
1922
+ return void 0;
1923
+ }
1924
+ return await new Promise((resolve) => {
1925
+ const shouldTimeout = timeoutMs != null;
1926
+ const timer = shouldTimeout && setTimeout(
1927
+ () => {
1928
+ const index = this.waiters.indexOf(waiter);
1929
+ if (index >= 0) {
1930
+ this.waiters.splice(index, 1);
1931
+ }
1932
+ resolve(void 0);
1933
+ },
1934
+ Math.max(0, timeoutMs)
1935
+ );
1936
+ const waiter = (task) => {
1937
+ if (timer) {
1938
+ clearTimeout(timer);
1939
+ }
1940
+ resolve(task);
1941
+ };
1942
+ this.waiters.push(waiter);
1943
+ });
1944
+ }
1945
+ enqueue(task) {
1946
+ if (this.closed) {
1947
+ if (task.waitForCompletion) {
1948
+ task.send({
1949
+ type: "error",
1950
+ requestId: task.requestId,
1951
+ message: "Queue owner is shutting down"
1952
+ });
1953
+ }
1954
+ task.close();
1955
+ return;
1956
+ }
1957
+ const waiter = this.waiters.shift();
1958
+ if (waiter) {
1959
+ waiter(task);
1960
+ return;
1961
+ }
1962
+ this.pending.push(task);
1963
+ }
1964
+ handleConnection(socket) {
1965
+ socket.setEncoding("utf8");
1966
+ if (this.closed) {
1967
+ writeQueueMessage(socket, {
1968
+ type: "error",
1969
+ requestId: "unknown",
1970
+ message: "Queue owner is closed"
1971
+ });
1972
+ socket.end();
1973
+ return;
1974
+ }
1975
+ let buffer = "";
1976
+ let handled = false;
1977
+ const fail = (requestId, message) => {
1978
+ writeQueueMessage(socket, {
1979
+ type: "error",
1980
+ requestId,
1981
+ message
1982
+ });
1983
+ socket.end();
1984
+ };
1985
+ const processLine = (line) => {
1986
+ if (handled) {
1987
+ return;
1988
+ }
1989
+ handled = true;
1990
+ let parsed;
1991
+ try {
1992
+ parsed = JSON.parse(line);
1993
+ } catch {
1994
+ fail("unknown", "Invalid queue request payload");
1995
+ return;
1996
+ }
1997
+ const request = parseQueueSubmitRequest(parsed);
1998
+ if (!request) {
1999
+ fail("unknown", "Invalid queue request");
2000
+ return;
2001
+ }
2002
+ const task = {
2003
+ requestId: request.requestId,
2004
+ message: request.message,
2005
+ permissionMode: request.permissionMode,
2006
+ timeoutMs: request.timeoutMs,
2007
+ waitForCompletion: request.waitForCompletion,
2008
+ send: (message) => {
2009
+ writeQueueMessage(socket, message);
2010
+ },
2011
+ close: () => {
2012
+ if (!socket.destroyed) {
2013
+ socket.end();
2014
+ }
2015
+ }
2016
+ };
2017
+ writeQueueMessage(socket, {
2018
+ type: "accepted",
2019
+ requestId: request.requestId
2020
+ });
2021
+ if (!request.waitForCompletion) {
2022
+ task.close();
2023
+ }
2024
+ this.enqueue(task);
2025
+ };
2026
+ socket.on("data", (chunk) => {
2027
+ buffer += chunk;
2028
+ let index = buffer.indexOf("\n");
2029
+ while (index >= 0) {
2030
+ const line = buffer.slice(0, index).trim();
2031
+ buffer = buffer.slice(index + 1);
2032
+ if (line.length > 0) {
2033
+ processLine(line);
2034
+ }
2035
+ index = buffer.indexOf("\n");
2036
+ }
2037
+ });
2038
+ socket.on("error", () => {
2039
+ });
2040
+ }
2041
+ };
2042
+ async function submitToQueueOwner(owner, options) {
2043
+ const socket = await connectToQueueOwner(owner);
2044
+ if (!socket) {
2045
+ return void 0;
2046
+ }
2047
+ socket.setEncoding("utf8");
2048
+ const requestId = randomUUID2();
2049
+ const request = {
2050
+ type: "submit_prompt",
2051
+ requestId,
2052
+ message: options.message,
2053
+ permissionMode: options.permissionMode,
2054
+ timeoutMs: options.timeoutMs,
2055
+ waitForCompletion: options.waitForCompletion
2056
+ };
2057
+ return await new Promise((resolve, reject) => {
2058
+ let settled = false;
2059
+ let acknowledged = false;
2060
+ let buffer = "";
2061
+ let sawDone = false;
2062
+ const finishResolve = (result) => {
2063
+ if (settled) {
2064
+ return;
2065
+ }
2066
+ settled = true;
2067
+ socket.removeAllListeners();
2068
+ if (!socket.destroyed) {
2069
+ socket.end();
2070
+ }
2071
+ resolve(result);
2072
+ };
2073
+ const finishReject = (error) => {
2074
+ if (settled) {
2075
+ return;
2076
+ }
2077
+ settled = true;
2078
+ socket.removeAllListeners();
2079
+ if (!socket.destroyed) {
2080
+ socket.destroy();
2081
+ }
2082
+ reject(error);
2083
+ };
2084
+ const processLine = (line) => {
2085
+ let parsed;
2086
+ try {
2087
+ parsed = JSON.parse(line);
2088
+ } catch {
2089
+ finishReject(new Error("Queue owner sent invalid JSON payload"));
2090
+ return;
2091
+ }
2092
+ const message = parseQueueOwnerMessage(parsed);
2093
+ if (!message || message.requestId !== requestId) {
2094
+ finishReject(new Error("Queue owner sent malformed message"));
2095
+ return;
2096
+ }
2097
+ if (message.type === "accepted") {
2098
+ acknowledged = true;
2099
+ if (!options.waitForCompletion) {
2100
+ const queued = {
2101
+ queued: true,
2102
+ sessionId: options.sessionId,
2103
+ requestId
2104
+ };
2105
+ finishResolve(queued);
2106
+ }
2107
+ return;
2108
+ }
2109
+ if (!acknowledged) {
2110
+ finishReject(new Error("Queue owner did not acknowledge request"));
2111
+ return;
2112
+ }
2113
+ if (message.type === "session_update") {
2114
+ options.outputFormatter.onSessionUpdate(message.notification);
2115
+ return;
2116
+ }
2117
+ if (message.type === "done") {
2118
+ options.outputFormatter.onDone(message.stopReason);
2119
+ sawDone = true;
2120
+ return;
2121
+ }
2122
+ if (message.type === "result") {
2123
+ if (!sawDone) {
2124
+ options.outputFormatter.onDone(message.result.stopReason);
2125
+ }
2126
+ options.outputFormatter.flush();
2127
+ finishResolve(message.result);
2128
+ return;
2129
+ }
2130
+ finishReject(new Error(message.message));
2131
+ };
2132
+ socket.on("data", (chunk) => {
2133
+ buffer += chunk;
2134
+ let index = buffer.indexOf("\n");
2135
+ while (index >= 0) {
2136
+ const line = buffer.slice(0, index).trim();
2137
+ buffer = buffer.slice(index + 1);
2138
+ if (line.length > 0) {
2139
+ processLine(line);
2140
+ }
2141
+ index = buffer.indexOf("\n");
2142
+ }
2143
+ });
2144
+ socket.once("error", (error) => {
2145
+ finishReject(error);
2146
+ });
2147
+ socket.once("close", () => {
2148
+ if (settled) {
2149
+ return;
2150
+ }
2151
+ if (!acknowledged) {
2152
+ finishReject(
2153
+ new Error("Queue owner disconnected before acknowledging request")
2154
+ );
2155
+ return;
2156
+ }
2157
+ if (!options.waitForCompletion) {
2158
+ const queued = {
2159
+ queued: true,
2160
+ sessionId: options.sessionId,
2161
+ requestId
2162
+ };
2163
+ finishResolve(queued);
2164
+ return;
2165
+ }
2166
+ finishReject(new Error("Queue owner disconnected before prompt completion"));
2167
+ });
2168
+ socket.write(`${JSON.stringify(request)}
2169
+ `);
2170
+ });
2171
+ }
2172
+ async function trySubmitToRunningOwner(options) {
2173
+ const owner = await readQueueOwnerRecord(options.sessionId);
2174
+ if (!owner) {
2175
+ return void 0;
2176
+ }
2177
+ if (!isProcessAlive(owner.pid)) {
2178
+ await cleanupStaleQueueOwner(options.sessionId, owner);
2179
+ return void 0;
2180
+ }
2181
+ const submitted = await submitToQueueOwner(owner, options);
2182
+ if (submitted) {
2183
+ if (options.verbose) {
2184
+ process.stderr.write(
2185
+ `[acpx] queued prompt on active owner pid ${owner.pid} for session ${options.sessionId}
2186
+ `
2187
+ );
2188
+ }
2189
+ return submitted;
2190
+ }
2191
+ if (!isProcessAlive(owner.pid)) {
2192
+ await cleanupStaleQueueOwner(options.sessionId, owner);
2193
+ return void 0;
2194
+ }
2195
+ throw new Error("Session queue owner is running but not accepting queue requests");
2196
+ }
2197
+ async function runQueuedTask(sessionRecordId, task, verbose) {
2198
+ const outputFormatter = task.waitForCompletion ? new QueueTaskOutputFormatter(task) : DISCARD_OUTPUT_FORMATTER;
2199
+ try {
2200
+ const result = await runSessionPrompt({
2201
+ sessionRecordId,
2202
+ message: task.message,
2203
+ permissionMode: task.permissionMode,
2204
+ outputFormatter,
2205
+ timeoutMs: task.timeoutMs,
2206
+ verbose
2207
+ });
2208
+ if (task.waitForCompletion) {
2209
+ task.send({
2210
+ type: "result",
2211
+ requestId: task.requestId,
2212
+ result
2213
+ });
2214
+ }
2215
+ } catch (error) {
2216
+ const message = formatError(error);
2217
+ if (task.waitForCompletion) {
2218
+ task.send({
2219
+ type: "error",
2220
+ requestId: task.requestId,
2221
+ message
2222
+ });
2223
+ }
2224
+ if (error instanceof InterruptedError) {
2225
+ throw error;
2226
+ }
2227
+ } finally {
2228
+ task.close();
2229
+ }
2230
+ }
2231
+ async function runSessionPrompt(options) {
2232
+ const output = options.outputFormatter;
2233
+ const record = await resolveSessionRecord(options.sessionRecordId);
2234
+ const storedProcessAlive = isProcessAlive(record.pid);
2235
+ if (storedProcessAlive && options.verbose) {
2236
+ process.stderr.write(
2237
+ `[acpx] saved session pid ${record.pid} is running; reconnecting with loadSession
2238
+ `
2239
+ );
2240
+ }
2241
+ const client = new AcpClient({
2242
+ agentCommand: record.agentCommand,
1103
2243
  cwd: absolutePath(record.cwd),
1104
2244
  permissionMode: options.permissionMode,
1105
2245
  verbose: options.verbose,
@@ -1144,18 +2284,94 @@ async function sendSession(options) {
1144
2284
  client.prompt(activeSessionId, options.message),
1145
2285
  options.timeoutMs
1146
2286
  );
1147
- output.onDone(response.stopReason);
1148
- output.flush();
1149
- record.lastUsedAt = isoNow();
1150
- record.protocolVersion = client.initializeResult?.protocolVersion;
1151
- record.agentCapabilities = client.initializeResult?.agentCapabilities;
1152
- await writeSessionRecord(record);
1153
- return {
1154
- ...toPromptResult(response.stopReason, record.id, client),
1155
- record,
1156
- resumed,
1157
- loadError
2287
+ output.onDone(response.stopReason);
2288
+ output.flush();
2289
+ record.lastUsedAt = isoNow();
2290
+ record.closed = false;
2291
+ record.closedAt = void 0;
2292
+ record.protocolVersion = client.initializeResult?.protocolVersion;
2293
+ record.agentCapabilities = client.initializeResult?.agentCapabilities;
2294
+ await writeSessionRecord(record);
2295
+ return {
2296
+ ...toPromptResult(response.stopReason, record.id, client),
2297
+ record,
2298
+ resumed,
2299
+ loadError
2300
+ };
2301
+ },
2302
+ async () => {
2303
+ await client.close();
2304
+ }
2305
+ );
2306
+ } finally {
2307
+ await client.close();
2308
+ }
2309
+ }
2310
+ async function runOnce(options) {
2311
+ const output = options.outputFormatter;
2312
+ const client = new AcpClient({
2313
+ agentCommand: options.agentCommand,
2314
+ cwd: absolutePath(options.cwd),
2315
+ permissionMode: options.permissionMode,
2316
+ verbose: options.verbose,
2317
+ onSessionUpdate: (notification) => output.onSessionUpdate(notification)
2318
+ });
2319
+ try {
2320
+ return await withInterrupt(
2321
+ async () => {
2322
+ await withTimeout(client.start(), options.timeoutMs);
2323
+ const sessionId = await withTimeout(
2324
+ client.createSession(absolutePath(options.cwd)),
2325
+ options.timeoutMs
2326
+ );
2327
+ const response = await withTimeout(
2328
+ client.prompt(sessionId, options.message),
2329
+ options.timeoutMs
2330
+ );
2331
+ output.onDone(response.stopReason);
2332
+ output.flush();
2333
+ return toPromptResult(response.stopReason, sessionId, client);
2334
+ },
2335
+ async () => {
2336
+ await client.close();
2337
+ }
2338
+ );
2339
+ } finally {
2340
+ await client.close();
2341
+ }
2342
+ }
2343
+ async function createSession(options) {
2344
+ const client = new AcpClient({
2345
+ agentCommand: options.agentCommand,
2346
+ cwd: absolutePath(options.cwd),
2347
+ permissionMode: options.permissionMode,
2348
+ verbose: options.verbose
2349
+ });
2350
+ try {
2351
+ return await withInterrupt(
2352
+ async () => {
2353
+ await withTimeout(client.start(), options.timeoutMs);
2354
+ const sessionId = await withTimeout(
2355
+ client.createSession(absolutePath(options.cwd)),
2356
+ options.timeoutMs
2357
+ );
2358
+ const now = isoNow();
2359
+ const record = {
2360
+ id: sessionId,
2361
+ sessionId,
2362
+ agentCommand: options.agentCommand,
2363
+ cwd: absolutePath(options.cwd),
2364
+ name: normalizeName(options.name),
2365
+ createdAt: now,
2366
+ lastUsedAt: now,
2367
+ closed: false,
2368
+ closedAt: void 0,
2369
+ pid: client.getAgentPid(),
2370
+ protocolVersion: client.initializeResult?.protocolVersion,
2371
+ agentCapabilities: client.initializeResult?.agentCapabilities
1158
2372
  };
2373
+ await writeSessionRecord(record);
2374
+ return record;
1159
2375
  },
1160
2376
  async () => {
1161
2377
  await client.close();
@@ -1165,6 +2381,73 @@ async function sendSession(options) {
1165
2381
  await client.close();
1166
2382
  }
1167
2383
  }
2384
+ async function sendSession(options) {
2385
+ const waitForCompletion = options.waitForCompletion !== false;
2386
+ const queueOwnerTtlMs = normalizeQueueOwnerTtlMs(options.ttlMs);
2387
+ const queuedToOwner = await trySubmitToRunningOwner({
2388
+ sessionId: options.sessionId,
2389
+ message: options.message,
2390
+ permissionMode: options.permissionMode,
2391
+ outputFormatter: options.outputFormatter,
2392
+ timeoutMs: options.timeoutMs,
2393
+ waitForCompletion,
2394
+ verbose: options.verbose
2395
+ });
2396
+ if (queuedToOwner) {
2397
+ return queuedToOwner;
2398
+ }
2399
+ for (; ; ) {
2400
+ const lease = await tryAcquireQueueOwnerLease(options.sessionId);
2401
+ if (!lease) {
2402
+ const retryQueued = await trySubmitToRunningOwner({
2403
+ sessionId: options.sessionId,
2404
+ message: options.message,
2405
+ permissionMode: options.permissionMode,
2406
+ outputFormatter: options.outputFormatter,
2407
+ timeoutMs: options.timeoutMs,
2408
+ waitForCompletion,
2409
+ verbose: options.verbose
2410
+ });
2411
+ if (retryQueued) {
2412
+ return retryQueued;
2413
+ }
2414
+ await waitMs(QUEUE_CONNECT_RETRY_MS);
2415
+ continue;
2416
+ }
2417
+ let owner;
2418
+ try {
2419
+ owner = await SessionQueueOwner.start(lease);
2420
+ const localResult = await runSessionPrompt({
2421
+ sessionRecordId: options.sessionId,
2422
+ message: options.message,
2423
+ permissionMode: options.permissionMode,
2424
+ outputFormatter: options.outputFormatter,
2425
+ timeoutMs: options.timeoutMs,
2426
+ verbose: options.verbose
2427
+ });
2428
+ const idleWaitMs = queueOwnerTtlMs === 0 ? void 0 : Math.max(0, queueOwnerTtlMs);
2429
+ while (true) {
2430
+ const task = await owner.nextTask(idleWaitMs);
2431
+ if (!task) {
2432
+ if (queueOwnerTtlMs > 0 && options.verbose) {
2433
+ process.stderr.write(
2434
+ `[acpx] queue owner TTL expired after ${Math.round(queueOwnerTtlMs / 1e3)}s for session ${options.sessionId}; shutting down
2435
+ `
2436
+ );
2437
+ }
2438
+ break;
2439
+ }
2440
+ await runQueuedTask(options.sessionId, task, options.verbose);
2441
+ }
2442
+ return localResult;
2443
+ } finally {
2444
+ if (owner) {
2445
+ await owner.close();
2446
+ }
2447
+ await releaseQueueOwnerLease(lease);
2448
+ }
2449
+ }
2450
+ }
1168
2451
  async function listSessions() {
1169
2452
  await ensureSessionDir();
1170
2453
  const entries = await fs2.readdir(SESSION_BASE_DIR, { withFileTypes: true });
@@ -1186,13 +2469,47 @@ async function listSessions() {
1186
2469
  records.sort((a, b) => b.lastUsedAt.localeCompare(a.lastUsedAt));
1187
2470
  return records;
1188
2471
  }
2472
+ async function listSessionsForAgent(agentCommand) {
2473
+ const sessions = await listSessions();
2474
+ return sessions.filter((session) => session.agentCommand === agentCommand);
2475
+ }
2476
+ async function findSession(options) {
2477
+ const normalizedCwd = absolutePath(options.cwd);
2478
+ const normalizedName = normalizeName(options.name);
2479
+ const sessions = await listSessionsForAgent(options.agentCommand);
2480
+ return sessions.find((session) => {
2481
+ if (session.cwd !== normalizedCwd) {
2482
+ return false;
2483
+ }
2484
+ if (!options.includeClosed && session.closed) {
2485
+ return false;
2486
+ }
2487
+ if (normalizedName == null) {
2488
+ return session.name == null;
2489
+ }
2490
+ return session.name === normalizedName;
2491
+ });
2492
+ }
2493
+ async function terminateQueueOwnerForSession(sessionId) {
2494
+ const owner = await readQueueOwnerRecord(sessionId);
2495
+ if (!owner) {
2496
+ return;
2497
+ }
2498
+ if (isProcessAlive(owner.pid)) {
2499
+ await terminateProcess(owner.pid);
2500
+ }
2501
+ await cleanupStaleQueueOwner(sessionId, owner);
2502
+ }
1189
2503
  async function closeSession(sessionId) {
1190
2504
  const record = await resolveSessionRecord(sessionId);
2505
+ await terminateQueueOwnerForSession(record.id);
1191
2506
  if (record.pid != null && isProcessAlive(record.pid) && await isLikelyMatchingProcess(record.pid, record.agentCommand)) {
1192
2507
  await terminateProcess(record.pid);
1193
2508
  }
1194
- const file = sessionFilePath(record.id);
1195
- await fs2.unlink(file);
2509
+ record.pid = void 0;
2510
+ record.closed = true;
2511
+ record.closedAt = isoNow();
2512
+ await writeSessionRecord(record);
1196
2513
  return record;
1197
2514
  }
1198
2515
 
@@ -1208,6 +2525,7 @@ var EXIT_CODES = {
1208
2525
  var OUTPUT_FORMATS = ["text", "json", "quiet"];
1209
2526
 
1210
2527
  // src/cli.ts
2528
+ var TOP_LEVEL_VERBS = /* @__PURE__ */ new Set(["prompt", "exec", "sessions", "help"]);
1211
2529
  function parseOutputFormat(value) {
1212
2530
  if (!OUTPUT_FORMATS.includes(value)) {
1213
2531
  throw new InvalidArgumentError(
@@ -1223,6 +2541,20 @@ function parseTimeoutSeconds(value) {
1223
2541
  }
1224
2542
  return Math.round(parsed * 1e3);
1225
2543
  }
2544
+ function parseTtlSeconds(value) {
2545
+ const parsed = Number(value);
2546
+ if (!Number.isFinite(parsed) || parsed < 0) {
2547
+ throw new InvalidArgumentError("TTL must be a non-negative number of seconds");
2548
+ }
2549
+ return Math.round(parsed * 1e3);
2550
+ }
2551
+ function parseSessionName(value) {
2552
+ const trimmed = value.trim();
2553
+ if (trimmed.length === 0) {
2554
+ throw new InvalidArgumentError("Session name must not be empty");
2555
+ }
2556
+ return trimmed;
2557
+ }
1226
2558
  function resolvePermissionMode(flags) {
1227
2559
  const selected2 = [flags.approveAll, flags.approveReads, flags.denyAll].filter(
1228
2560
  Boolean
@@ -1240,20 +2572,6 @@ function resolvePermissionMode(flags) {
1240
2572
  }
1241
2573
  return "approve-reads";
1242
2574
  }
1243
- function addPermissionFlags(command) {
1244
- return command.option("--approve-all", "Auto-approve all permission requests").option(
1245
- "--approve-reads",
1246
- "Auto-approve read/search requests and prompt for writes"
1247
- ).option("--deny-all", "Deny all permission requests");
1248
- }
1249
- function addFormatFlag(command) {
1250
- return command.option(
1251
- "--format <fmt>",
1252
- "Output format: text, json, quiet",
1253
- parseOutputFormat,
1254
- "text"
1255
- );
1256
- }
1257
2575
  async function readPrompt(promptParts) {
1258
2576
  const joined = promptParts.join(" ").trim();
1259
2577
  if (joined.length > 0) {
@@ -1281,19 +2599,63 @@ function applyPermissionExitCode(result) {
1281
2599
  process.exitCode = EXIT_CODES.PERMISSION_DENIED;
1282
2600
  }
1283
2601
  }
1284
- function printSessionRecordByFormat(record, format) {
1285
- if (format === "json") {
1286
- process.stdout.write(
1287
- `${JSON.stringify({
1288
- type: "session",
1289
- ...record
1290
- })}
1291
- `
2602
+ function addGlobalFlags(command) {
2603
+ return command.option("--agent <command>", "Raw ACP agent command (escape hatch)").option("--cwd <dir>", "Working directory", process.cwd()).option("--approve-all", "Auto-approve all permission requests").option(
2604
+ "--approve-reads",
2605
+ "Auto-approve read/search requests and prompt for writes"
2606
+ ).option("--deny-all", "Deny all permission requests").option(
2607
+ "--format <fmt>",
2608
+ "Output format: text, json, quiet",
2609
+ parseOutputFormat,
2610
+ "text"
2611
+ ).option(
2612
+ "--timeout <seconds>",
2613
+ "Maximum time to wait for agent response",
2614
+ parseTimeoutSeconds
2615
+ ).option(
2616
+ "--ttl <seconds>",
2617
+ "Queue owner idle TTL before shutdown (0 = keep alive forever) (default: 300)",
2618
+ parseTtlSeconds
2619
+ ).option("--verbose", "Enable verbose debug logs");
2620
+ }
2621
+ function addSessionOption(command) {
2622
+ return command.option(
2623
+ "-s, --session <name>",
2624
+ "Use named session instead of cwd default",
2625
+ parseSessionName
2626
+ ).option(
2627
+ "--no-wait",
2628
+ "Queue prompt and return immediately when another prompt is already running"
2629
+ );
2630
+ }
2631
+ function resolveGlobalFlags(command) {
2632
+ const opts = command.optsWithGlobals();
2633
+ return {
2634
+ agent: opts.agent,
2635
+ cwd: opts.cwd ?? process.cwd(),
2636
+ timeout: opts.timeout,
2637
+ ttl: opts.ttl ?? DEFAULT_QUEUE_OWNER_TTL_MS,
2638
+ verbose: opts.verbose,
2639
+ format: opts.format ?? "text",
2640
+ approveAll: opts.approveAll,
2641
+ approveReads: opts.approveReads,
2642
+ denyAll: opts.denyAll
2643
+ };
2644
+ }
2645
+ function resolveAgentInvocation(explicitAgentName, globalFlags) {
2646
+ const override = globalFlags.agent?.trim();
2647
+ if (override && explicitAgentName) {
2648
+ throw new InvalidArgumentError(
2649
+ "Do not combine positional agent with --agent override"
1292
2650
  );
1293
- return;
1294
2651
  }
1295
- process.stdout.write(`${record.id}
1296
- `);
2652
+ const agentName = explicitAgentName ?? DEFAULT_AGENT_NAME;
2653
+ const agentCommand = override && override.length > 0 ? override : resolveAgentCommand(agentName);
2654
+ return {
2655
+ agentName,
2656
+ agentCommand,
2657
+ cwd: path3.resolve(globalFlags.cwd)
2658
+ };
1297
2659
  }
1298
2660
  function printSessionsByFormat(sessions, format) {
1299
2661
  if (format === "json") {
@@ -1303,7 +2665,8 @@ function printSessionsByFormat(sessions, format) {
1303
2665
  }
1304
2666
  if (format === "quiet") {
1305
2667
  for (const session of sessions) {
1306
- process.stdout.write(`${session.id}
2668
+ const closedMarker = session.closed ? " [closed]" : "";
2669
+ process.stdout.write(`${session.id}${closedMarker}
1307
2670
  `);
1308
2671
  }
1309
2672
  return;
@@ -1313,121 +2676,319 @@ function printSessionsByFormat(sessions, format) {
1313
2676
  return;
1314
2677
  }
1315
2678
  for (const session of sessions) {
2679
+ const closedMarker = session.closed ? " [closed]" : "";
1316
2680
  process.stdout.write(
1317
- `${session.id} ${session.cwd} ${session.lastUsedAt}
2681
+ `${session.id}${closedMarker} ${session.name ?? "-"} ${session.cwd} ${session.lastUsedAt}
1318
2682
  `
1319
2683
  );
1320
2684
  }
1321
2685
  }
1322
- async function main() {
1323
- const program = new Command();
1324
- program.name("acpx").description("Headless CLI client for the Agent Client Protocol").showHelpAfterError();
1325
- program.command("run").description("Run a one-shot prompt").argument("[prompt...]", "Prompt text").requiredOption("--agent <command>", "ACP adapter command").option("--cwd <dir>", "Working directory", process.cwd()).option(
1326
- "--timeout <seconds>",
1327
- "Maximum time to wait for agent response",
1328
- parseTimeoutSeconds
1329
- ).option("--verbose", "Enable verbose debug logs").allowUnknownOption(false);
1330
- const runCommand = program.commands.find((cmd) => cmd.name() === "run");
1331
- if (!runCommand) {
1332
- throw new Error("Failed to build run command");
1333
- }
1334
- addPermissionFlags(runCommand);
1335
- addFormatFlag(runCommand);
1336
- runCommand.action(async (promptParts, flags) => {
1337
- const prompt = await readPrompt(promptParts);
1338
- const permissionMode = resolvePermissionMode(flags);
1339
- const formatter = createOutputFormatter(flags.format);
1340
- const result = await runOnce({
1341
- agentCommand: flags.agent,
1342
- cwd: flags.cwd,
1343
- message: prompt,
1344
- permissionMode,
1345
- outputFormatter: formatter,
1346
- timeoutMs: flags.timeout,
1347
- verbose: flags.verbose
1348
- });
1349
- applyPermissionExitCode(result);
2686
+ function printClosedSessionByFormat(record, format) {
2687
+ if (format === "json") {
2688
+ process.stdout.write(
2689
+ `${JSON.stringify({
2690
+ type: "session_closed",
2691
+ id: record.id,
2692
+ sessionId: record.sessionId,
2693
+ name: record.name
2694
+ })}
2695
+ `
2696
+ );
2697
+ return;
2698
+ }
2699
+ if (format === "quiet") {
2700
+ return;
2701
+ }
2702
+ process.stdout.write(`${record.id}
2703
+ `);
2704
+ }
2705
+ function printNewSessionByFormat(record, replaced, format) {
2706
+ if (format === "json") {
2707
+ process.stdout.write(
2708
+ `${JSON.stringify({
2709
+ type: "session_created",
2710
+ id: record.id,
2711
+ sessionId: record.sessionId,
2712
+ name: record.name,
2713
+ replacedSessionId: replaced?.id
2714
+ })}
2715
+ `
2716
+ );
2717
+ return;
2718
+ }
2719
+ if (format === "quiet") {
2720
+ process.stdout.write(`${record.id}
2721
+ `);
2722
+ return;
2723
+ }
2724
+ if (replaced) {
2725
+ process.stdout.write(`${record.id} (replaced ${replaced.id})
2726
+ `);
2727
+ return;
2728
+ }
2729
+ process.stdout.write(`${record.id}
2730
+ `);
2731
+ }
2732
+ function printQueuedPromptByFormat(result, format) {
2733
+ if (format === "json") {
2734
+ process.stdout.write(
2735
+ `${JSON.stringify({
2736
+ type: "queued",
2737
+ sessionId: result.sessionId,
2738
+ requestId: result.requestId
2739
+ })}
2740
+ `
2741
+ );
2742
+ return;
2743
+ }
2744
+ if (format === "quiet") {
2745
+ return;
2746
+ }
2747
+ process.stdout.write(`[queued] ${result.requestId}
2748
+ `);
2749
+ }
2750
+ async function handlePrompt(explicitAgentName, promptParts, flags, command) {
2751
+ const globalFlags = resolveGlobalFlags(command);
2752
+ const permissionMode = resolvePermissionMode(globalFlags);
2753
+ const prompt = await readPrompt(promptParts);
2754
+ const outputFormatter = createOutputFormatter(globalFlags.format);
2755
+ const agent = resolveAgentInvocation(explicitAgentName, globalFlags);
2756
+ let record = await findSession({
2757
+ agentCommand: agent.agentCommand,
2758
+ cwd: agent.cwd,
2759
+ name: flags.session
1350
2760
  });
1351
- const session = program.command("session").description("Session management");
1352
- const sessionCreate = session.command("create").description("Create a persistent session").requiredOption("--agent <command>", "ACP adapter command").option("--cwd <dir>", "Working directory", process.cwd()).option(
1353
- "--timeout <seconds>",
1354
- "Maximum time to wait for agent response",
1355
- parseTimeoutSeconds
1356
- ).option("--verbose", "Enable verbose debug logs");
1357
- addPermissionFlags(sessionCreate);
1358
- addFormatFlag(sessionCreate);
1359
- sessionCreate.action(async (flags) => {
1360
- const permissionMode = resolvePermissionMode(flags);
1361
- const record = await createSession({
1362
- agentCommand: flags.agent,
1363
- cwd: flags.cwd,
2761
+ if (!record) {
2762
+ record = await createSession({
2763
+ agentCommand: agent.agentCommand,
2764
+ cwd: agent.cwd,
2765
+ name: flags.session,
1364
2766
  permissionMode,
1365
- timeoutMs: flags.timeout,
1366
- verbose: flags.verbose
2767
+ timeoutMs: globalFlags.timeout,
2768
+ verbose: globalFlags.verbose
1367
2769
  });
1368
- printSessionRecordByFormat(record, flags.format);
1369
- });
1370
- const sessionSend = session.command("send").description("Send a prompt to an existing session").argument("<sessionId>", "Session ID").argument("[prompt...]", "Prompt text").option(
1371
- "--timeout <seconds>",
1372
- "Maximum time to wait for agent response",
1373
- parseTimeoutSeconds
1374
- ).option("--verbose", "Enable verbose debug logs");
1375
- addPermissionFlags(sessionSend);
1376
- addFormatFlag(sessionSend);
1377
- sessionSend.action(
1378
- async (sessionId, promptParts, flags) => {
1379
- const prompt = await readPrompt(promptParts);
1380
- const permissionMode = resolvePermissionMode(flags);
1381
- const formatter = createOutputFormatter(flags.format);
1382
- const result = await sendSession({
1383
- sessionId,
1384
- message: prompt,
1385
- permissionMode,
1386
- outputFormatter: formatter,
1387
- timeoutMs: flags.timeout,
1388
- verbose: flags.verbose
1389
- });
1390
- applyPermissionExitCode(result);
1391
- if (flags.verbose && result.loadError) {
1392
- process.stderr.write(
1393
- `[acpx] loadSession failed, started fresh session: ${result.loadError}
1394
- `
1395
- );
1396
- }
2770
+ if (globalFlags.verbose) {
2771
+ const scope = flags.session ? `named session "${flags.session}"` : "cwd session";
2772
+ process.stderr.write(`[acpx] created ${scope}: ${record.id}
2773
+ `);
1397
2774
  }
1398
- );
1399
- const sessionList = session.command("list").description("List saved sessions");
1400
- addFormatFlag(sessionList);
1401
- sessionList.action(async (flags) => {
1402
- const sessions = await listSessions();
1403
- printSessionsByFormat(sessions, flags.format);
2775
+ }
2776
+ const result = await sendSession({
2777
+ sessionId: record.id,
2778
+ message: prompt,
2779
+ permissionMode,
2780
+ outputFormatter,
2781
+ timeoutMs: globalFlags.timeout,
2782
+ ttlMs: globalFlags.ttl,
2783
+ verbose: globalFlags.verbose,
2784
+ waitForCompletion: flags.wait !== false
1404
2785
  });
1405
- const sessionClose = session.command("close").description("Close and remove a saved session").argument("<sessionId>", "Session ID");
1406
- addFormatFlag(sessionClose);
1407
- sessionClose.action(async (sessionId, flags) => {
1408
- const record = await closeSession(sessionId);
1409
- if (flags.format === "json") {
1410
- process.stdout.write(
1411
- `${JSON.stringify({
1412
- type: "session_closed",
1413
- id: record.id,
1414
- sessionId: record.sessionId
1415
- })}
2786
+ if ("queued" in result) {
2787
+ printQueuedPromptByFormat(result, globalFlags.format);
2788
+ return;
2789
+ }
2790
+ applyPermissionExitCode(result);
2791
+ if (globalFlags.verbose && result.loadError) {
2792
+ process.stderr.write(
2793
+ `[acpx] loadSession failed, started fresh session: ${result.loadError}
1416
2794
  `
2795
+ );
2796
+ }
2797
+ }
2798
+ async function handleExec(explicitAgentName, promptParts, command) {
2799
+ const globalFlags = resolveGlobalFlags(command);
2800
+ const permissionMode = resolvePermissionMode(globalFlags);
2801
+ const prompt = await readPrompt(promptParts);
2802
+ const outputFormatter = createOutputFormatter(globalFlags.format);
2803
+ const agent = resolveAgentInvocation(explicitAgentName, globalFlags);
2804
+ const result = await runOnce({
2805
+ agentCommand: agent.agentCommand,
2806
+ cwd: agent.cwd,
2807
+ message: prompt,
2808
+ permissionMode,
2809
+ outputFormatter,
2810
+ timeoutMs: globalFlags.timeout,
2811
+ verbose: globalFlags.verbose
2812
+ });
2813
+ applyPermissionExitCode(result);
2814
+ }
2815
+ async function handleSessionsList(explicitAgentName, command) {
2816
+ const globalFlags = resolveGlobalFlags(command);
2817
+ const agent = resolveAgentInvocation(explicitAgentName, globalFlags);
2818
+ const sessions = await listSessionsForAgent(agent.agentCommand);
2819
+ printSessionsByFormat(sessions, globalFlags.format);
2820
+ }
2821
+ async function handleSessionsClose(explicitAgentName, sessionName, command) {
2822
+ const globalFlags = resolveGlobalFlags(command);
2823
+ const agent = resolveAgentInvocation(explicitAgentName, globalFlags);
2824
+ const record = await findSession({
2825
+ agentCommand: agent.agentCommand,
2826
+ cwd: agent.cwd,
2827
+ name: sessionName
2828
+ });
2829
+ if (!record) {
2830
+ if (sessionName) {
2831
+ throw new Error(
2832
+ `No named session "${sessionName}" for cwd ${agent.cwd} and agent ${agent.agentName}`
1417
2833
  );
1418
- return;
1419
2834
  }
1420
- if (flags.format === "quiet") {
1421
- return;
2835
+ throw new Error(`No cwd session for ${agent.cwd} and agent ${agent.agentName}`);
2836
+ }
2837
+ const closed = await closeSession(record.id);
2838
+ printClosedSessionByFormat(closed, globalFlags.format);
2839
+ }
2840
+ async function handleSessionsNew(explicitAgentName, flags, command) {
2841
+ const globalFlags = resolveGlobalFlags(command);
2842
+ const permissionMode = resolvePermissionMode(globalFlags);
2843
+ const agent = resolveAgentInvocation(explicitAgentName, globalFlags);
2844
+ const replaced = await findSession({
2845
+ agentCommand: agent.agentCommand,
2846
+ cwd: agent.cwd,
2847
+ name: flags.name
2848
+ });
2849
+ if (replaced) {
2850
+ await closeSession(replaced.id);
2851
+ if (globalFlags.verbose) {
2852
+ process.stderr.write(`[acpx] soft-closed prior session: ${replaced.id}
2853
+ `);
1422
2854
  }
1423
- process.stdout.write(`${record.id}
2855
+ }
2856
+ const created = await createSession({
2857
+ agentCommand: agent.agentCommand,
2858
+ cwd: agent.cwd,
2859
+ name: flags.name,
2860
+ permissionMode,
2861
+ timeoutMs: globalFlags.timeout,
2862
+ verbose: globalFlags.verbose
2863
+ });
2864
+ if (globalFlags.verbose) {
2865
+ const scope = flags.name ? `named session "${flags.name}"` : "cwd session";
2866
+ process.stderr.write(`[acpx] created ${scope}: ${created.id}
1424
2867
  `);
2868
+ }
2869
+ printNewSessionByFormat(created, replaced, globalFlags.format);
2870
+ }
2871
+ function registerSessionsCommand(parent, explicitAgentName) {
2872
+ const sessionsCommand = parent.command("sessions").description("List, create, or close sessions for this agent");
2873
+ sessionsCommand.action(async function() {
2874
+ await handleSessionsList(explicitAgentName, this);
2875
+ });
2876
+ sessionsCommand.command("list").description("List sessions").action(async function() {
2877
+ await handleSessionsList(explicitAgentName, this);
2878
+ });
2879
+ sessionsCommand.command("new").description("Create a fresh session for current cwd").option("--name <name>", "Session name", parseSessionName).action(async function(flags) {
2880
+ await handleSessionsNew(explicitAgentName, flags, this);
2881
+ });
2882
+ sessionsCommand.command("close").description("Close session for current cwd").argument("[name]", "Session name", parseSessionName).action(async function(name) {
2883
+ await handleSessionsClose(explicitAgentName, name, this);
2884
+ });
2885
+ }
2886
+ function registerAgentCommand(program, agentName) {
2887
+ const agentCommand = program.command(agentName).description(`Use ${agentName} agent`).argument("[prompt...]", "Prompt text").showHelpAfterError();
2888
+ addSessionOption(agentCommand);
2889
+ agentCommand.action(async function(promptParts, flags) {
2890
+ await handlePrompt(agentName, promptParts, flags, this);
2891
+ });
2892
+ const promptCommand = agentCommand.command("prompt").description("Prompt using persistent session").argument("[prompt...]", "Prompt text").showHelpAfterError();
2893
+ addSessionOption(promptCommand);
2894
+ promptCommand.action(async function(promptParts, flags) {
2895
+ await handlePrompt(agentName, promptParts, flags, this);
2896
+ });
2897
+ agentCommand.command("exec").description("One-shot prompt without saved session").argument("[prompt...]", "Prompt text").showHelpAfterError().action(async function(promptParts) {
2898
+ await handleExec(agentName, promptParts, this);
2899
+ });
2900
+ registerSessionsCommand(agentCommand, agentName);
2901
+ }
2902
+ function registerDefaultCommands(program) {
2903
+ const promptCommand = program.command("prompt").description(`Prompt using ${DEFAULT_AGENT_NAME} by default`).argument("[prompt...]", "Prompt text").showHelpAfterError();
2904
+ addSessionOption(promptCommand);
2905
+ promptCommand.action(async function(promptParts, flags) {
2906
+ await handlePrompt(void 0, promptParts, flags, this);
1425
2907
  });
2908
+ program.command("exec").description(`One-shot prompt using ${DEFAULT_AGENT_NAME} by default`).argument("[prompt...]", "Prompt text").showHelpAfterError().action(async function(promptParts) {
2909
+ await handleExec(void 0, promptParts, this);
2910
+ });
2911
+ registerSessionsCommand(program, void 0);
2912
+ }
2913
+ function detectAgentToken(argv) {
2914
+ let hasAgentOverride = false;
2915
+ for (let index = 0; index < argv.length; index += 1) {
2916
+ const token = argv[index];
2917
+ if (token === "--") {
2918
+ break;
2919
+ }
2920
+ if (!token.startsWith("-") || token === "-") {
2921
+ return { token, hasAgentOverride };
2922
+ }
2923
+ if (token === "--agent") {
2924
+ hasAgentOverride = true;
2925
+ index += 1;
2926
+ continue;
2927
+ }
2928
+ if (token.startsWith("--agent=")) {
2929
+ hasAgentOverride = true;
2930
+ continue;
2931
+ }
2932
+ if (token === "--cwd" || token === "--format" || token === "--timeout" || token === "--ttl") {
2933
+ index += 1;
2934
+ continue;
2935
+ }
2936
+ if (token.startsWith("--cwd=") || token.startsWith("--format=") || token.startsWith("--timeout=") || token.startsWith("--ttl=")) {
2937
+ continue;
2938
+ }
2939
+ if (token === "--approve-all" || token === "--approve-reads" || token === "--deny-all" || token === "--verbose") {
2940
+ continue;
2941
+ }
2942
+ return { hasAgentOverride };
2943
+ }
2944
+ return { hasAgentOverride };
2945
+ }
2946
+ async function main(argv = process.argv) {
2947
+ await maybeHandleSkillflag(argv, {
2948
+ skillsRoot: findSkillsRoot(import.meta.url),
2949
+ includeBundledSkill: false
2950
+ });
2951
+ const program = new Command();
2952
+ program.name("acpx").description("Headless CLI client for the Agent Client Protocol").showHelpAfterError();
2953
+ addGlobalFlags(program);
2954
+ const builtInAgents = listBuiltInAgents();
2955
+ for (const agentName of builtInAgents) {
2956
+ registerAgentCommand(program, agentName);
2957
+ }
2958
+ registerDefaultCommands(program);
2959
+ const scan = detectAgentToken(argv.slice(2));
2960
+ if (!scan.hasAgentOverride && scan.token && !TOP_LEVEL_VERBS.has(scan.token) && !builtInAgents.includes(scan.token)) {
2961
+ registerAgentCommand(program, scan.token);
2962
+ }
2963
+ program.argument("[prompt...]", "Prompt text").action(async function(promptParts) {
2964
+ if (promptParts.length === 0 && process.stdin.isTTY) {
2965
+ this.outputHelp();
2966
+ return;
2967
+ }
2968
+ await handlePrompt(void 0, promptParts, {}, this);
2969
+ });
2970
+ program.addHelpText(
2971
+ "after",
2972
+ `
2973
+ Examples:
2974
+ acpx codex "fix the tests"
2975
+ acpx codex prompt "fix the tests"
2976
+ acpx codex --no-wait "queue follow-up task"
2977
+ acpx codex exec "what does this repo do"
2978
+ acpx codex -s backend "fix the API"
2979
+ acpx codex sessions
2980
+ acpx codex sessions new --name backend
2981
+ acpx codex sessions close backend
2982
+ acpx --ttl 30 codex "investigate flaky tests"
2983
+ acpx claude "refactor auth"
2984
+ acpx gemini "add logging"
2985
+ acpx --agent ./my-custom-server "do something"`
2986
+ );
1426
2987
  program.exitOverride((error) => {
1427
2988
  throw error;
1428
2989
  });
1429
2990
  try {
1430
- await program.parseAsync(process.argv);
2991
+ await program.parseAsync(argv);
1431
2992
  } catch (error) {
1432
2993
  if (error instanceof CommanderError) {
1433
2994
  if (error.code === "commander.helpDisplayed" || error.code === "commander.version") {
@@ -1449,4 +3010,22 @@ async function main() {
1449
3010
  process.exit(EXIT_CODES.ERROR);
1450
3011
  }
1451
3012
  }
1452
- void main();
3013
+ function isCliEntrypoint(argv) {
3014
+ const entry = argv[1];
3015
+ if (!entry) {
3016
+ return false;
3017
+ }
3018
+ try {
3019
+ const resolved = pathToFileURL(realpathSync(entry)).href;
3020
+ return import.meta.url === resolved;
3021
+ } catch {
3022
+ return false;
3023
+ }
3024
+ }
3025
+ if (isCliEntrypoint(process.argv)) {
3026
+ void main(process.argv);
3027
+ }
3028
+ export {
3029
+ main,
3030
+ parseTtlSeconds
3031
+ };