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/LICENSE +18 -12
- package/README.md +133 -62
- package/dist/cli.d.ts +4 -0
- package/dist/cli.js +1814 -235
- package/package.json +33 -5
- package/skills/acpx/SKILL.md +254 -0
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.
|
|
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.
|
|
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.
|
|
37
|
-
[tool] ${update.title} (${asStatus(update.status)})
|
|
38
|
-
`);
|
|
386
|
+
this.renderToolUpdate(update);
|
|
39
387
|
return;
|
|
40
388
|
}
|
|
41
389
|
case "tool_call_update": {
|
|
42
|
-
|
|
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.
|
|
394
|
+
this.beginSection("plan");
|
|
395
|
+
this.writeLine(this.bold("[plan]"));
|
|
50
396
|
for (const entry of update.entries) {
|
|
51
|
-
this.
|
|
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.
|
|
62
|
-
|
|
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", "
|
|
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
|
-
|
|
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
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
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
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
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
|
-
|
|
1092
|
-
const
|
|
1093
|
-
|
|
1094
|
-
|
|
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
|
-
|
|
1102
|
-
|
|
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.
|
|
1151
|
-
record.
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
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
|
-
|
|
1195
|
-
|
|
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
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
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
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
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:
|
|
1366
|
-
verbose:
|
|
2767
|
+
timeoutMs: globalFlags.timeout,
|
|
2768
|
+
verbose: globalFlags.verbose
|
|
1367
2769
|
});
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
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
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
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
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
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
|
-
|
|
1421
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
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
|
+
};
|