acpx 0.1.2 → 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/README.md +9 -0
- package/dist/cli.d.ts +4 -0
- package/dist/cli.js +152 -16
- package/package.json +19 -19
- package/skills/acpx/SKILL.md +254 -0
package/README.md
CHANGED
|
@@ -9,6 +9,8 @@ One command surface for Codex, Claude, Gemini, OpenCode, Pi, or custom ACP serve
|
|
|
9
9
|
- **Persistent sessions**: multi-turn conversations that survive across invocations, scoped per repo
|
|
10
10
|
- **Named sessions**: run parallel workstreams in the same repo (`-s backend`, `-s frontend`)
|
|
11
11
|
- **Prompt queueing**: submit prompts while one is already running, they execute in order
|
|
12
|
+
- **Soft-close lifecycle**: close sessions without deleting history from disk
|
|
13
|
+
- **Queue owner TTL**: keep queue owners alive briefly for follow-up prompts (`--ttl`)
|
|
12
14
|
- **Fire-and-forget**: `--no-wait` queues a prompt and returns immediately
|
|
13
15
|
- **Structured output**: typed ACP messages (thinking, tool calls, diffs) instead of ANSI scraping
|
|
14
16
|
- **Any ACP agent**: built-in registry + `--agent` escape hatch for custom servers
|
|
@@ -76,6 +78,8 @@ acpx codex -s docs 'rewrite API docs' # parallel work in another named
|
|
|
76
78
|
|
|
77
79
|
acpx codex sessions # list sessions for codex command
|
|
78
80
|
acpx codex sessions list # explicit list
|
|
81
|
+
acpx codex sessions new # create fresh cwd-scoped default session
|
|
82
|
+
acpx codex sessions new --name api # create fresh named session
|
|
79
83
|
acpx codex sessions close # close cwd-scoped default session
|
|
80
84
|
acpx codex sessions close api # close cwd-scoped named session
|
|
81
85
|
|
|
@@ -111,6 +115,7 @@ acpx --format json codex exec 'review changed files'
|
|
|
111
115
|
acpx --format quiet codex 'final recommendation only'
|
|
112
116
|
|
|
113
117
|
acpx --timeout 90 codex 'investigate intermittent test timeout'
|
|
118
|
+
acpx --ttl 30 codex 'keep queue owner alive for quick follow-ups'
|
|
114
119
|
acpx --verbose codex 'debug why adapter startup is failing'
|
|
115
120
|
```
|
|
116
121
|
|
|
@@ -150,7 +155,11 @@ acpx --agent ./my-custom-acp-server 'do something'
|
|
|
150
155
|
|
|
151
156
|
- Prompt commands use saved sessions scoped to `(agent command, cwd, optional name)`.
|
|
152
157
|
- `-s <name>` creates/selects a parallel named session in the same repo.
|
|
158
|
+
- `sessions new [--name <name>]` creates a fresh session for that scope and soft-closes the prior one.
|
|
159
|
+
- `sessions close [name]` soft-closes the session: queue owner/processes are terminated, record is kept with `closed: true`.
|
|
160
|
+
- Auto-resume for cwd scope skips sessions marked closed.
|
|
153
161
|
- Prompt submissions are queue-aware per session. If a prompt is already running, new prompts are queued and drained by the running `acpx` process.
|
|
162
|
+
- Queue owners use an idle TTL (default 300s). `--ttl <seconds>` overrides it; `--ttl 0` keeps owners alive indefinitely.
|
|
154
163
|
- `--no-wait` submits to that queue and returns immediately.
|
|
155
164
|
- `exec` is always one-shot and does not reuse saved sessions.
|
|
156
165
|
- Session metadata is stored under `~/.acpx/sessions/`.
|
package/dist/cli.d.ts
CHANGED
package/dist/cli.js
CHANGED
|
@@ -2,7 +2,10 @@
|
|
|
2
2
|
|
|
3
3
|
// src/cli.ts
|
|
4
4
|
import { Command, CommanderError, InvalidArgumentError } from "commander";
|
|
5
|
+
import { realpathSync } from "fs";
|
|
5
6
|
import path3 from "path";
|
|
7
|
+
import { pathToFileURL } from "url";
|
|
8
|
+
import { findSkillsRoot, maybeHandleSkillflag } from "skillflag";
|
|
6
9
|
|
|
7
10
|
// src/agent-registry.ts
|
|
8
11
|
var AGENT_REGISTRY = {
|
|
@@ -1300,7 +1303,7 @@ var PROCESS_EXIT_GRACE_MS = 1500;
|
|
|
1300
1303
|
var PROCESS_POLL_MS = 50;
|
|
1301
1304
|
var QUEUE_CONNECT_ATTEMPTS = 40;
|
|
1302
1305
|
var QUEUE_CONNECT_RETRY_MS = 50;
|
|
1303
|
-
var
|
|
1306
|
+
var DEFAULT_QUEUE_OWNER_TTL_MS = 3e5;
|
|
1304
1307
|
var TimeoutError = class extends Error {
|
|
1305
1308
|
constructor(timeoutMs) {
|
|
1306
1309
|
super(`Timed out after ${timeoutMs}ms`);
|
|
@@ -1375,7 +1378,9 @@ function parseSessionRecord(raw) {
|
|
|
1375
1378
|
const record = raw;
|
|
1376
1379
|
const name = record.name == null ? void 0 : typeof record.name === "string" && record.name.trim().length > 0 ? record.name.trim() : null;
|
|
1377
1380
|
const pid = record.pid == null ? void 0 : Number.isInteger(record.pid) && record.pid > 0 ? record.pid : null;
|
|
1378
|
-
|
|
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) {
|
|
1379
1384
|
return null;
|
|
1380
1385
|
}
|
|
1381
1386
|
return {
|
|
@@ -1387,6 +1392,8 @@ function parseSessionRecord(raw) {
|
|
|
1387
1392
|
name,
|
|
1388
1393
|
createdAt: record.createdAt,
|
|
1389
1394
|
lastUsedAt: record.lastUsedAt,
|
|
1395
|
+
closed,
|
|
1396
|
+
closedAt,
|
|
1390
1397
|
pid
|
|
1391
1398
|
};
|
|
1392
1399
|
}
|
|
@@ -1451,6 +1458,15 @@ function normalizeName(value) {
|
|
|
1451
1458
|
function isoNow() {
|
|
1452
1459
|
return (/* @__PURE__ */ new Date()).toISOString();
|
|
1453
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
|
+
}
|
|
1454
1470
|
function formatError(error) {
|
|
1455
1471
|
if (error instanceof Error) {
|
|
1456
1472
|
return error.message;
|
|
@@ -1906,7 +1922,8 @@ var SessionQueueOwner = class _SessionQueueOwner {
|
|
|
1906
1922
|
return void 0;
|
|
1907
1923
|
}
|
|
1908
1924
|
return await new Promise((resolve) => {
|
|
1909
|
-
const
|
|
1925
|
+
const shouldTimeout = timeoutMs != null;
|
|
1926
|
+
const timer = shouldTimeout && setTimeout(
|
|
1910
1927
|
() => {
|
|
1911
1928
|
const index = this.waiters.indexOf(waiter);
|
|
1912
1929
|
if (index >= 0) {
|
|
@@ -1917,7 +1934,9 @@ var SessionQueueOwner = class _SessionQueueOwner {
|
|
|
1917
1934
|
Math.max(0, timeoutMs)
|
|
1918
1935
|
);
|
|
1919
1936
|
const waiter = (task) => {
|
|
1920
|
-
|
|
1937
|
+
if (timer) {
|
|
1938
|
+
clearTimeout(timer);
|
|
1939
|
+
}
|
|
1921
1940
|
resolve(task);
|
|
1922
1941
|
};
|
|
1923
1942
|
this.waiters.push(waiter);
|
|
@@ -2268,6 +2287,8 @@ async function runSessionPrompt(options) {
|
|
|
2268
2287
|
output.onDone(response.stopReason);
|
|
2269
2288
|
output.flush();
|
|
2270
2289
|
record.lastUsedAt = isoNow();
|
|
2290
|
+
record.closed = false;
|
|
2291
|
+
record.closedAt = void 0;
|
|
2271
2292
|
record.protocolVersion = client.initializeResult?.protocolVersion;
|
|
2272
2293
|
record.agentCapabilities = client.initializeResult?.agentCapabilities;
|
|
2273
2294
|
await writeSessionRecord(record);
|
|
@@ -2343,6 +2364,8 @@ async function createSession(options) {
|
|
|
2343
2364
|
name: normalizeName(options.name),
|
|
2344
2365
|
createdAt: now,
|
|
2345
2366
|
lastUsedAt: now,
|
|
2367
|
+
closed: false,
|
|
2368
|
+
closedAt: void 0,
|
|
2346
2369
|
pid: client.getAgentPid(),
|
|
2347
2370
|
protocolVersion: client.initializeResult?.protocolVersion,
|
|
2348
2371
|
agentCapabilities: client.initializeResult?.agentCapabilities
|
|
@@ -2360,6 +2383,7 @@ async function createSession(options) {
|
|
|
2360
2383
|
}
|
|
2361
2384
|
async function sendSession(options) {
|
|
2362
2385
|
const waitForCompletion = options.waitForCompletion !== false;
|
|
2386
|
+
const queueOwnerTtlMs = normalizeQueueOwnerTtlMs(options.ttlMs);
|
|
2363
2387
|
const queuedToOwner = await trySubmitToRunningOwner({
|
|
2364
2388
|
sessionId: options.sessionId,
|
|
2365
2389
|
message: options.message,
|
|
@@ -2401,9 +2425,16 @@ async function sendSession(options) {
|
|
|
2401
2425
|
timeoutMs: options.timeoutMs,
|
|
2402
2426
|
verbose: options.verbose
|
|
2403
2427
|
});
|
|
2428
|
+
const idleWaitMs = queueOwnerTtlMs === 0 ? void 0 : Math.max(0, queueOwnerTtlMs);
|
|
2404
2429
|
while (true) {
|
|
2405
|
-
const task = await owner.nextTask(
|
|
2430
|
+
const task = await owner.nextTask(idleWaitMs);
|
|
2406
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
|
+
}
|
|
2407
2438
|
break;
|
|
2408
2439
|
}
|
|
2409
2440
|
await runQueuedTask(options.sessionId, task, options.verbose);
|
|
@@ -2450,6 +2481,9 @@ async function findSession(options) {
|
|
|
2450
2481
|
if (session.cwd !== normalizedCwd) {
|
|
2451
2482
|
return false;
|
|
2452
2483
|
}
|
|
2484
|
+
if (!options.includeClosed && session.closed) {
|
|
2485
|
+
return false;
|
|
2486
|
+
}
|
|
2453
2487
|
if (normalizedName == null) {
|
|
2454
2488
|
return session.name == null;
|
|
2455
2489
|
}
|
|
@@ -2472,8 +2506,10 @@ async function closeSession(sessionId) {
|
|
|
2472
2506
|
if (record.pid != null && isProcessAlive(record.pid) && await isLikelyMatchingProcess(record.pid, record.agentCommand)) {
|
|
2473
2507
|
await terminateProcess(record.pid);
|
|
2474
2508
|
}
|
|
2475
|
-
|
|
2476
|
-
|
|
2509
|
+
record.pid = void 0;
|
|
2510
|
+
record.closed = true;
|
|
2511
|
+
record.closedAt = isoNow();
|
|
2512
|
+
await writeSessionRecord(record);
|
|
2477
2513
|
return record;
|
|
2478
2514
|
}
|
|
2479
2515
|
|
|
@@ -2505,6 +2541,13 @@ function parseTimeoutSeconds(value) {
|
|
|
2505
2541
|
}
|
|
2506
2542
|
return Math.round(parsed * 1e3);
|
|
2507
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
|
+
}
|
|
2508
2551
|
function parseSessionName(value) {
|
|
2509
2552
|
const trimmed = value.trim();
|
|
2510
2553
|
if (trimmed.length === 0) {
|
|
@@ -2569,6 +2612,10 @@ function addGlobalFlags(command) {
|
|
|
2569
2612
|
"--timeout <seconds>",
|
|
2570
2613
|
"Maximum time to wait for agent response",
|
|
2571
2614
|
parseTimeoutSeconds
|
|
2615
|
+
).option(
|
|
2616
|
+
"--ttl <seconds>",
|
|
2617
|
+
"Queue owner idle TTL before shutdown (0 = keep alive forever) (default: 300)",
|
|
2618
|
+
parseTtlSeconds
|
|
2572
2619
|
).option("--verbose", "Enable verbose debug logs");
|
|
2573
2620
|
}
|
|
2574
2621
|
function addSessionOption(command) {
|
|
@@ -2587,6 +2634,7 @@ function resolveGlobalFlags(command) {
|
|
|
2587
2634
|
agent: opts.agent,
|
|
2588
2635
|
cwd: opts.cwd ?? process.cwd(),
|
|
2589
2636
|
timeout: opts.timeout,
|
|
2637
|
+
ttl: opts.ttl ?? DEFAULT_QUEUE_OWNER_TTL_MS,
|
|
2590
2638
|
verbose: opts.verbose,
|
|
2591
2639
|
format: opts.format ?? "text",
|
|
2592
2640
|
approveAll: opts.approveAll,
|
|
@@ -2617,7 +2665,8 @@ function printSessionsByFormat(sessions, format) {
|
|
|
2617
2665
|
}
|
|
2618
2666
|
if (format === "quiet") {
|
|
2619
2667
|
for (const session of sessions) {
|
|
2620
|
-
|
|
2668
|
+
const closedMarker = session.closed ? " [closed]" : "";
|
|
2669
|
+
process.stdout.write(`${session.id}${closedMarker}
|
|
2621
2670
|
`);
|
|
2622
2671
|
}
|
|
2623
2672
|
return;
|
|
@@ -2627,8 +2676,9 @@ function printSessionsByFormat(sessions, format) {
|
|
|
2627
2676
|
return;
|
|
2628
2677
|
}
|
|
2629
2678
|
for (const session of sessions) {
|
|
2679
|
+
const closedMarker = session.closed ? " [closed]" : "";
|
|
2630
2680
|
process.stdout.write(
|
|
2631
|
-
`${session.id} ${session.name ?? "-"} ${session.cwd} ${session.lastUsedAt}
|
|
2681
|
+
`${session.id}${closedMarker} ${session.name ?? "-"} ${session.cwd} ${session.lastUsedAt}
|
|
2632
2682
|
`
|
|
2633
2683
|
);
|
|
2634
2684
|
}
|
|
@@ -2652,6 +2702,33 @@ function printClosedSessionByFormat(record, format) {
|
|
|
2652
2702
|
process.stdout.write(`${record.id}
|
|
2653
2703
|
`);
|
|
2654
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
|
+
}
|
|
2655
2732
|
function printQueuedPromptByFormat(result, format) {
|
|
2656
2733
|
if (format === "json") {
|
|
2657
2734
|
process.stdout.write(
|
|
@@ -2702,6 +2779,7 @@ async function handlePrompt(explicitAgentName, promptParts, flags, command) {
|
|
|
2702
2779
|
permissionMode,
|
|
2703
2780
|
outputFormatter,
|
|
2704
2781
|
timeoutMs: globalFlags.timeout,
|
|
2782
|
+
ttlMs: globalFlags.ttl,
|
|
2705
2783
|
verbose: globalFlags.verbose,
|
|
2706
2784
|
waitForCompletion: flags.wait !== false
|
|
2707
2785
|
});
|
|
@@ -2759,14 +2837,48 @@ async function handleSessionsClose(explicitAgentName, sessionName, command) {
|
|
|
2759
2837
|
const closed = await closeSession(record.id);
|
|
2760
2838
|
printClosedSessionByFormat(closed, globalFlags.format);
|
|
2761
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
|
+
`);
|
|
2854
|
+
}
|
|
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}
|
|
2867
|
+
`);
|
|
2868
|
+
}
|
|
2869
|
+
printNewSessionByFormat(created, replaced, globalFlags.format);
|
|
2870
|
+
}
|
|
2762
2871
|
function registerSessionsCommand(parent, explicitAgentName) {
|
|
2763
|
-
const sessionsCommand = parent.command("sessions").description("List or close sessions for this agent");
|
|
2872
|
+
const sessionsCommand = parent.command("sessions").description("List, create, or close sessions for this agent");
|
|
2764
2873
|
sessionsCommand.action(async function() {
|
|
2765
2874
|
await handleSessionsList(explicitAgentName, this);
|
|
2766
2875
|
});
|
|
2767
2876
|
sessionsCommand.command("list").description("List sessions").action(async function() {
|
|
2768
2877
|
await handleSessionsList(explicitAgentName, this);
|
|
2769
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
|
+
});
|
|
2770
2882
|
sessionsCommand.command("close").description("Close session for current cwd").argument("[name]", "Session name", parseSessionName).action(async function(name) {
|
|
2771
2883
|
await handleSessionsClose(explicitAgentName, name, this);
|
|
2772
2884
|
});
|
|
@@ -2817,11 +2929,11 @@ function detectAgentToken(argv) {
|
|
|
2817
2929
|
hasAgentOverride = true;
|
|
2818
2930
|
continue;
|
|
2819
2931
|
}
|
|
2820
|
-
if (token === "--cwd" || token === "--format" || token === "--timeout") {
|
|
2932
|
+
if (token === "--cwd" || token === "--format" || token === "--timeout" || token === "--ttl") {
|
|
2821
2933
|
index += 1;
|
|
2822
2934
|
continue;
|
|
2823
2935
|
}
|
|
2824
|
-
if (token.startsWith("--cwd=") || token.startsWith("--format=") || token.startsWith("--timeout=")) {
|
|
2936
|
+
if (token.startsWith("--cwd=") || token.startsWith("--format=") || token.startsWith("--timeout=") || token.startsWith("--ttl=")) {
|
|
2825
2937
|
continue;
|
|
2826
2938
|
}
|
|
2827
2939
|
if (token === "--approve-all" || token === "--approve-reads" || token === "--deny-all" || token === "--verbose") {
|
|
@@ -2831,7 +2943,11 @@ function detectAgentToken(argv) {
|
|
|
2831
2943
|
}
|
|
2832
2944
|
return { hasAgentOverride };
|
|
2833
2945
|
}
|
|
2834
|
-
async function main() {
|
|
2946
|
+
async function main(argv = process.argv) {
|
|
2947
|
+
await maybeHandleSkillflag(argv, {
|
|
2948
|
+
skillsRoot: findSkillsRoot(import.meta.url),
|
|
2949
|
+
includeBundledSkill: false
|
|
2950
|
+
});
|
|
2835
2951
|
const program = new Command();
|
|
2836
2952
|
program.name("acpx").description("Headless CLI client for the Agent Client Protocol").showHelpAfterError();
|
|
2837
2953
|
addGlobalFlags(program);
|
|
@@ -2840,7 +2956,7 @@ async function main() {
|
|
|
2840
2956
|
registerAgentCommand(program, agentName);
|
|
2841
2957
|
}
|
|
2842
2958
|
registerDefaultCommands(program);
|
|
2843
|
-
const scan = detectAgentToken(
|
|
2959
|
+
const scan = detectAgentToken(argv.slice(2));
|
|
2844
2960
|
if (!scan.hasAgentOverride && scan.token && !TOP_LEVEL_VERBS.has(scan.token) && !builtInAgents.includes(scan.token)) {
|
|
2845
2961
|
registerAgentCommand(program, scan.token);
|
|
2846
2962
|
}
|
|
@@ -2861,7 +2977,9 @@ Examples:
|
|
|
2861
2977
|
acpx codex exec "what does this repo do"
|
|
2862
2978
|
acpx codex -s backend "fix the API"
|
|
2863
2979
|
acpx codex sessions
|
|
2980
|
+
acpx codex sessions new --name backend
|
|
2864
2981
|
acpx codex sessions close backend
|
|
2982
|
+
acpx --ttl 30 codex "investigate flaky tests"
|
|
2865
2983
|
acpx claude "refactor auth"
|
|
2866
2984
|
acpx gemini "add logging"
|
|
2867
2985
|
acpx --agent ./my-custom-server "do something"`
|
|
@@ -2870,7 +2988,7 @@ Examples:
|
|
|
2870
2988
|
throw error;
|
|
2871
2989
|
});
|
|
2872
2990
|
try {
|
|
2873
|
-
await program.parseAsync(
|
|
2991
|
+
await program.parseAsync(argv);
|
|
2874
2992
|
} catch (error) {
|
|
2875
2993
|
if (error instanceof CommanderError) {
|
|
2876
2994
|
if (error.code === "commander.helpDisplayed" || error.code === "commander.version") {
|
|
@@ -2892,4 +3010,22 @@ Examples:
|
|
|
2892
3010
|
process.exit(EXIT_CODES.ERROR);
|
|
2893
3011
|
}
|
|
2894
3012
|
}
|
|
2895
|
-
|
|
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
|
+
};
|
package/package.json
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "acpx",
|
|
3
|
-
"version": "0.1.
|
|
4
|
-
"description": "Headless CLI client for the Agent Client Protocol (ACP)
|
|
3
|
+
"version": "0.1.3",
|
|
4
|
+
"description": "Headless CLI client for the Agent Client Protocol (ACP) — talk to coding agents from the command line",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"files": [
|
|
7
7
|
"dist",
|
|
8
|
+
"skills",
|
|
8
9
|
"README.md",
|
|
9
10
|
"LICENSE"
|
|
10
11
|
],
|
|
@@ -13,6 +14,10 @@
|
|
|
13
14
|
},
|
|
14
15
|
"scripts": {
|
|
15
16
|
"build": "tsup src/cli.ts --format esm --dts --clean",
|
|
17
|
+
"build:test": "tsc -p tsconfig.test.json",
|
|
18
|
+
"test": "npm run build:test && node --test dist-test/test/*.test.js",
|
|
19
|
+
"prepare": "husky",
|
|
20
|
+
"precommit": "npm exec -- lint-staged && npm run -s build",
|
|
16
21
|
"prepack": "npm run build",
|
|
17
22
|
"dev": "tsx src/cli.ts",
|
|
18
23
|
"typecheck": "tsc --noEmit",
|
|
@@ -42,13 +47,16 @@
|
|
|
42
47
|
},
|
|
43
48
|
"dependencies": {
|
|
44
49
|
"@agentclientprotocol/sdk": "^0.14.1",
|
|
45
|
-
"commander": "^13.0.0"
|
|
50
|
+
"commander": "^13.0.0",
|
|
51
|
+
"skillflag": "^0.1.3"
|
|
46
52
|
},
|
|
47
53
|
"devDependencies": {
|
|
48
54
|
"@eslint/js": "^10.0.1",
|
|
49
55
|
"@types/node": "^22.0.0",
|
|
50
56
|
"eslint": "^10.0.0",
|
|
51
57
|
"globals": "^17.3.0",
|
|
58
|
+
"husky": "^9.1.7",
|
|
59
|
+
"lint-staged": "^16.2.7",
|
|
52
60
|
"prettier": "^3.8.1",
|
|
53
61
|
"release-it": "^19.2.4",
|
|
54
62
|
"tsup": "^8.0.0",
|
|
@@ -56,21 +64,13 @@
|
|
|
56
64
|
"typescript": "^5.7.0",
|
|
57
65
|
"typescript-eslint": "^8.56.0"
|
|
58
66
|
},
|
|
59
|
-
"
|
|
60
|
-
"
|
|
61
|
-
"
|
|
62
|
-
"
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
"
|
|
66
|
-
|
|
67
|
-
"npm": {
|
|
68
|
-
"publish": true,
|
|
69
|
-
"skipChecks": true
|
|
70
|
-
},
|
|
71
|
-
"github": {
|
|
72
|
-
"release": true,
|
|
73
|
-
"autoGenerate": true
|
|
74
|
-
}
|
|
67
|
+
"lint-staged": {
|
|
68
|
+
"*.{js,ts}": [
|
|
69
|
+
"prettier --write --ignore-unknown",
|
|
70
|
+
"eslint --fix"
|
|
71
|
+
],
|
|
72
|
+
"*.{json,md}": [
|
|
73
|
+
"prettier --write --ignore-unknown"
|
|
74
|
+
]
|
|
75
75
|
}
|
|
76
76
|
}
|
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: acpx
|
|
3
|
+
description: Use acpx as a headless ACP CLI for agent-to-agent communication, including prompt/exec/sessions workflows, session scoping, queueing, permissions, and output formats.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# acpx
|
|
7
|
+
|
|
8
|
+
## When to use this skill
|
|
9
|
+
|
|
10
|
+
Use this skill when you need to run coding agents through `acpx`, manage persistent ACP sessions, queue prompts, or consume structured agent output from scripts.
|
|
11
|
+
|
|
12
|
+
## What acpx is
|
|
13
|
+
|
|
14
|
+
`acpx` is a headless, scriptable CLI client for the Agent Client Protocol (ACP). It is built for agent-to-agent communication over the command line and avoids PTY scraping.
|
|
15
|
+
|
|
16
|
+
Core capabilities:
|
|
17
|
+
|
|
18
|
+
- Persistent multi-turn sessions per repo/cwd
|
|
19
|
+
- One-shot execution mode (`exec`)
|
|
20
|
+
- Named parallel sessions (`-s/--session`)
|
|
21
|
+
- Queue-aware prompt submission with optional fire-and-forget (`--no-wait`)
|
|
22
|
+
- Structured streaming output (`text`, `json`, `quiet`)
|
|
23
|
+
- Built-in agent registry plus raw `--agent` escape hatch
|
|
24
|
+
|
|
25
|
+
## Install
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
npm i -g acpx
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
For normal session reuse, prefer a global install over `npx`.
|
|
32
|
+
|
|
33
|
+
## Command model
|
|
34
|
+
|
|
35
|
+
`prompt` is the default verb.
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
acpx [global_options] [prompt_text...]
|
|
39
|
+
acpx [global_options] prompt [prompt_options] [prompt_text...]
|
|
40
|
+
acpx [global_options] exec [prompt_text...]
|
|
41
|
+
acpx [global_options] sessions [list | new [--name <name>] | close [name]]
|
|
42
|
+
|
|
43
|
+
acpx [global_options] <agent> [prompt_options] [prompt_text...]
|
|
44
|
+
acpx [global_options] <agent> prompt [prompt_options] [prompt_text...]
|
|
45
|
+
acpx [global_options] <agent> exec [prompt_text...]
|
|
46
|
+
acpx [global_options] <agent> sessions [list | new [--name <name>] | close [name]]
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
If prompt text is omitted and stdin is piped, `acpx` reads prompt text from stdin.
|
|
50
|
+
|
|
51
|
+
## Built-in agent registry
|
|
52
|
+
|
|
53
|
+
Friendly agent names resolve to commands:
|
|
54
|
+
|
|
55
|
+
- `codex` -> `npx @zed-industries/codex-acp`
|
|
56
|
+
- `claude` -> `npx @zed-industries/claude-agent-acp`
|
|
57
|
+
- `gemini` -> `gemini`
|
|
58
|
+
- `opencode` -> `npx opencode-ai`
|
|
59
|
+
- `pi` -> `npx pi-acp`
|
|
60
|
+
|
|
61
|
+
Rules:
|
|
62
|
+
|
|
63
|
+
- Default agent is `codex` for top-level `prompt`, `exec`, and `sessions`.
|
|
64
|
+
- Unknown positional agent tokens are treated as raw agent commands.
|
|
65
|
+
- `--agent <command>` explicitly sets a raw ACP adapter command.
|
|
66
|
+
- Do not combine a positional agent and `--agent` in the same command.
|
|
67
|
+
|
|
68
|
+
## Commands
|
|
69
|
+
|
|
70
|
+
### Prompt (default, persistent session)
|
|
71
|
+
|
|
72
|
+
Implicit:
|
|
73
|
+
|
|
74
|
+
```bash
|
|
75
|
+
acpx codex 'fix flaky tests'
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
Explicit:
|
|
79
|
+
|
|
80
|
+
```bash
|
|
81
|
+
acpx codex prompt 'fix flaky tests'
|
|
82
|
+
acpx prompt 'fix flaky tests' # defaults to codex
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
Behavior:
|
|
86
|
+
|
|
87
|
+
- Uses a saved session for the session scope key
|
|
88
|
+
- Auto-resumes prior session when one exists for that scope
|
|
89
|
+
- Creates a new session record when none exists
|
|
90
|
+
- Is queue-aware when another prompt is already running for the same session
|
|
91
|
+
|
|
92
|
+
Prompt options:
|
|
93
|
+
|
|
94
|
+
- `-s, --session <name>`: use a named session within the same cwd
|
|
95
|
+
- `--no-wait`: enqueue and return immediately when session is already busy
|
|
96
|
+
|
|
97
|
+
### Exec (one-shot)
|
|
98
|
+
|
|
99
|
+
```bash
|
|
100
|
+
acpx exec 'summarize this repo'
|
|
101
|
+
acpx codex exec 'summarize this repo'
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
Behavior:
|
|
105
|
+
|
|
106
|
+
- Runs a single prompt in a temporary ACP session
|
|
107
|
+
- Does not reuse or save persistent session state
|
|
108
|
+
|
|
109
|
+
### Sessions
|
|
110
|
+
|
|
111
|
+
```bash
|
|
112
|
+
acpx sessions
|
|
113
|
+
acpx sessions list
|
|
114
|
+
acpx sessions new
|
|
115
|
+
acpx sessions new --name backend
|
|
116
|
+
acpx sessions close
|
|
117
|
+
acpx sessions close backend
|
|
118
|
+
|
|
119
|
+
acpx codex sessions
|
|
120
|
+
acpx codex sessions new --name backend
|
|
121
|
+
acpx codex sessions close backend
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
Behavior:
|
|
125
|
+
|
|
126
|
+
- `sessions` and `sessions list` are equivalent
|
|
127
|
+
- `new` creates a fresh session for the current `(agentCommand, cwd, optional name)` scope
|
|
128
|
+
- `new --name <name>` targets a named session scope
|
|
129
|
+
- when `new` replaces an existing open session in that scope, the old one is soft-closed
|
|
130
|
+
- `close` targets current cwd default session
|
|
131
|
+
- `close <name>` targets current cwd named session
|
|
132
|
+
|
|
133
|
+
## Global options
|
|
134
|
+
|
|
135
|
+
- `--agent <command>`: raw ACP agent command (escape hatch)
|
|
136
|
+
- `--cwd <dir>`: working directory for session scope (default: current directory)
|
|
137
|
+
- `--approve-all`: auto-approve all permission requests
|
|
138
|
+
- `--approve-reads`: auto-approve reads/searches, prompt for writes (default mode)
|
|
139
|
+
- `--deny-all`: deny all permission requests
|
|
140
|
+
- `--format <fmt>`: output format (`text`, `json`, `quiet`)
|
|
141
|
+
- `--timeout <seconds>`: max wait time (positive number)
|
|
142
|
+
- `--ttl <seconds>`: queue owner idle TTL before shutdown (default `300`, `0` disables TTL)
|
|
143
|
+
- `--verbose`: verbose ACP/debug logs to stderr
|
|
144
|
+
|
|
145
|
+
Permission flags are mutually exclusive.
|
|
146
|
+
|
|
147
|
+
## Session behavior
|
|
148
|
+
|
|
149
|
+
Persistent prompt sessions are scoped by:
|
|
150
|
+
|
|
151
|
+
- `agentCommand`
|
|
152
|
+
- absolute `cwd`
|
|
153
|
+
- optional session `name`
|
|
154
|
+
|
|
155
|
+
Persistence:
|
|
156
|
+
|
|
157
|
+
- Session records are stored in `~/.acpx/sessions/*.json`.
|
|
158
|
+
- `-s/--session` creates parallel named conversations in the same repo.
|
|
159
|
+
- Changing `--cwd` changes scope and therefore session lookup.
|
|
160
|
+
- closed sessions are retained on disk with `closed: true` and `closedAt`.
|
|
161
|
+
- auto-resume by scope skips closed sessions.
|
|
162
|
+
|
|
163
|
+
Resume behavior:
|
|
164
|
+
|
|
165
|
+
- Prompt mode attempts to reconnect to saved session.
|
|
166
|
+
- If adapter-side session is invalid/not found, `acpx` creates a fresh session and updates the saved record.
|
|
167
|
+
- explicitly selected session records can still be resumed via `loadSession` even if previously closed.
|
|
168
|
+
|
|
169
|
+
## Prompt queueing and `--no-wait`
|
|
170
|
+
|
|
171
|
+
Queueing is per persistent session.
|
|
172
|
+
|
|
173
|
+
- The active `acpx` process for a running prompt becomes the queue owner.
|
|
174
|
+
- Other invocations submit prompts over local IPC.
|
|
175
|
+
- On Unix-like systems, queue IPC uses a Unix socket under `~/.acpx/queues/<hash>.sock`.
|
|
176
|
+
- Ownership is coordinated with a lock file under `~/.acpx/queues/<hash>.lock`.
|
|
177
|
+
- On Windows, named pipes are used instead of Unix sockets.
|
|
178
|
+
- after the queue drains, owner shutdown is governed by TTL (default 300s, configurable with `--ttl`).
|
|
179
|
+
|
|
180
|
+
Submission behavior:
|
|
181
|
+
|
|
182
|
+
- Default: enqueue and wait for queued prompt completion, streaming updates back.
|
|
183
|
+
- `--no-wait`: enqueue and return after queue acknowledgement.
|
|
184
|
+
|
|
185
|
+
## Output formats
|
|
186
|
+
|
|
187
|
+
Use `--format <fmt>`:
|
|
188
|
+
|
|
189
|
+
- `text` (default): human-readable stream with updates/tool status and done line
|
|
190
|
+
- `json`: NDJSON event stream (good for automation)
|
|
191
|
+
- `quiet`: final assistant text only
|
|
192
|
+
|
|
193
|
+
Example automation:
|
|
194
|
+
|
|
195
|
+
```bash
|
|
196
|
+
acpx --format json codex exec 'review changed files' \
|
|
197
|
+
| jq -r 'select(.type=="tool_call") | [.status, .title] | @tsv'
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
## Permission modes
|
|
201
|
+
|
|
202
|
+
- `--approve-all`: no interactive permission prompts
|
|
203
|
+
- `--approve-reads` (default): approve reads/searches, prompt for writes
|
|
204
|
+
- `--deny-all`: deny all permission requests
|
|
205
|
+
|
|
206
|
+
If every permission request is denied/cancelled and none approved, `acpx` exits with permission-denied status.
|
|
207
|
+
|
|
208
|
+
## Practical workflows
|
|
209
|
+
|
|
210
|
+
Persistent repo assistant:
|
|
211
|
+
|
|
212
|
+
```bash
|
|
213
|
+
acpx codex 'inspect failing tests and propose a fix plan'
|
|
214
|
+
acpx codex 'apply the smallest safe fix and run tests'
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
Parallel named streams:
|
|
218
|
+
|
|
219
|
+
```bash
|
|
220
|
+
acpx codex -s backend 'fix API pagination bug'
|
|
221
|
+
acpx codex -s docs 'draft changelog entry for release'
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
Queue follow-up without waiting:
|
|
225
|
+
|
|
226
|
+
```bash
|
|
227
|
+
acpx codex 'run full test suite and investigate failures'
|
|
228
|
+
acpx codex --no-wait 'after tests, summarize root causes and next steps'
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
One-shot script step:
|
|
232
|
+
|
|
233
|
+
```bash
|
|
234
|
+
acpx --format quiet exec 'summarize repo purpose in 3 lines'
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
Machine-readable output for orchestration:
|
|
238
|
+
|
|
239
|
+
```bash
|
|
240
|
+
acpx --format json codex 'review current branch changes' > events.ndjson
|
|
241
|
+
```
|
|
242
|
+
|
|
243
|
+
Raw custom adapter command:
|
|
244
|
+
|
|
245
|
+
```bash
|
|
246
|
+
acpx --agent './bin/custom-acp-server --profile ci' 'run validation checks'
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
Repo-scoped review with permissive mode:
|
|
250
|
+
|
|
251
|
+
```bash
|
|
252
|
+
acpx --cwd ~/repos/shop --approve-all codex -s pr-842 \
|
|
253
|
+
'review PR #842 for regressions and propose minimal patch'
|
|
254
|
+
```
|