acpx 0.1.2 → 0.1.4
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 +47 -4
- package/dist/cli.d.ts +22 -0
- package/dist/cli.js +282 -32
- package/package.json +19 -19
- package/skills/acpx/SKILL.md +254 -0
package/README.md
CHANGED
|
@@ -9,12 +9,15 @@ 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
|
|
15
17
|
- **One-shot mode**: `exec` for stateless fire-and-forget tasks
|
|
16
18
|
|
|
17
19
|
```bash
|
|
20
|
+
$ acpx codex sessions new
|
|
18
21
|
$ acpx codex "find the flaky test and fix it"
|
|
19
22
|
|
|
20
23
|
[thinking] Investigating test suite for flaky failures
|
|
@@ -42,6 +45,34 @@ reading stale state from the previous run.
|
|
|
42
45
|
[done] end_turn
|
|
43
46
|
```
|
|
44
47
|
|
|
48
|
+
## Quick setup — tell your agent about acpx
|
|
49
|
+
|
|
50
|
+
Copy the block below and paste it into your OpenClaw, Pi, Claude Code, or similar agent harness. It will install acpx, read the skill reference, and know how to use ACP for all future coding agent tasks.
|
|
51
|
+
|
|
52
|
+
```text
|
|
53
|
+
I want you to use acpx to run coding agents over the Agent Client Protocol
|
|
54
|
+
(ACP) instead of scraping PTY sessions. Please do the following:
|
|
55
|
+
|
|
56
|
+
1. Install acpx globally:
|
|
57
|
+
npm i -g acpx
|
|
58
|
+
|
|
59
|
+
2. Install the acpx skill so you have the full reference available:
|
|
60
|
+
acpx --skill export acpx | npx skillflag install
|
|
61
|
+
|
|
62
|
+
3. Read the acpx skill reference so you know every command, flag, and
|
|
63
|
+
workflow pattern:
|
|
64
|
+
https://raw.githubusercontent.com/janitrai/acpx/main/skills/acpx/SKILL.md
|
|
65
|
+
|
|
66
|
+
4. If you need the full CLI reference with all options and examples:
|
|
67
|
+
https://raw.githubusercontent.com/janitrai/acpx/main/docs/CLI.md
|
|
68
|
+
|
|
69
|
+
From now on, when you need to delegate work to a coding agent, use acpx
|
|
70
|
+
instead of spawning raw terminal sessions. For example:
|
|
71
|
+
acpx codex "fix the failing tests"
|
|
72
|
+
acpx claude "refactor the auth module"
|
|
73
|
+
acpx codex exec "one-shot: summarize this repo"
|
|
74
|
+
```
|
|
75
|
+
|
|
45
76
|
## Install
|
|
46
77
|
|
|
47
78
|
```bash
|
|
@@ -65,17 +96,22 @@ The only prerequisite is the underlying coding agent you want to use:
|
|
|
65
96
|
## Usage examples
|
|
66
97
|
|
|
67
98
|
```bash
|
|
68
|
-
acpx codex
|
|
99
|
+
acpx codex sessions new # create a session (explicit) for this project dir
|
|
100
|
+
acpx codex 'fix the tests' # implicit prompt (routes via directory-walk)
|
|
69
101
|
acpx codex prompt 'fix the tests' # explicit prompt subcommand
|
|
70
102
|
acpx codex --no-wait 'draft test migration plan' # enqueue without waiting if session is busy
|
|
71
103
|
acpx exec 'summarize this repo' # default agent shortcut (codex)
|
|
72
104
|
acpx codex exec 'what does this repo do?' # one-shot, no saved session
|
|
73
105
|
|
|
74
|
-
acpx codex
|
|
106
|
+
acpx codex sessions new --name api # create named session
|
|
107
|
+
acpx codex -s api 'implement cursor pagination' # prompt in named session
|
|
108
|
+
acpx codex sessions new --name docs # create another named session
|
|
75
109
|
acpx codex -s docs 'rewrite API docs' # parallel work in another named session
|
|
76
110
|
|
|
77
111
|
acpx codex sessions # list sessions for codex command
|
|
78
112
|
acpx codex sessions list # explicit list
|
|
113
|
+
acpx codex sessions new # create fresh cwd-scoped default session
|
|
114
|
+
acpx codex sessions new --name api # create fresh named session
|
|
79
115
|
acpx codex sessions close # close cwd-scoped default session
|
|
80
116
|
acpx codex sessions close api # close cwd-scoped named session
|
|
81
117
|
|
|
@@ -111,6 +147,7 @@ acpx --format json codex exec 'review changed files'
|
|
|
111
147
|
acpx --format quiet codex 'final recommendation only'
|
|
112
148
|
|
|
113
149
|
acpx --timeout 90 codex 'investigate intermittent test timeout'
|
|
150
|
+
acpx --ttl 30 codex 'keep queue owner alive for quick follow-ups'
|
|
114
151
|
acpx --verbose codex 'debug why adapter startup is failing'
|
|
115
152
|
```
|
|
116
153
|
|
|
@@ -148,9 +185,15 @@ acpx --agent ./my-custom-acp-server 'do something'
|
|
|
148
185
|
|
|
149
186
|
## Session behavior
|
|
150
187
|
|
|
151
|
-
- Prompt commands
|
|
152
|
-
-
|
|
188
|
+
- Prompt commands require an existing saved session record (created via `sessions new`).
|
|
189
|
+
- Prompts route by walking up from `cwd` (or `--cwd`) to the nearest git root (inclusive) and selecting the nearest active session matching `(agent command, dir, optional name)`.
|
|
190
|
+
- If no git root is found, prompts only match an exact `cwd` session (no parent-directory walk).
|
|
191
|
+
- `-s <name>` selects a parallel named session during that directory walk.
|
|
192
|
+
- `sessions new [--name <name>]` creates a fresh session for that scope and soft-closes the prior one.
|
|
193
|
+
- `sessions close [name]` soft-closes the session: queue owner/processes are terminated, record is kept with `closed: true`.
|
|
194
|
+
- Auto-resume for cwd scope skips sessions marked closed.
|
|
153
195
|
- Prompt submissions are queue-aware per session. If a prompt is already running, new prompts are queued and drained by the running `acpx` process.
|
|
196
|
+
- Queue owners use an idle TTL (default 300s). `--ttl <seconds>` overrides it; `--ttl 0` keeps owners alive indefinitely.
|
|
154
197
|
- `--no-wait` submits to that queue and returns immediately.
|
|
155
198
|
- `exec` is always one-shot and does not reuse saved sessions.
|
|
156
199
|
- Session metadata is stored under `~/.acpx/sessions/`.
|
package/dist/cli.d.ts
CHANGED
|
@@ -1 +1,23 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
import { AgentCapabilities } from '@agentclientprotocol/sdk';
|
|
3
|
+
|
|
4
|
+
type SessionRecord = {
|
|
5
|
+
id: string;
|
|
6
|
+
sessionId: string;
|
|
7
|
+
agentCommand: string;
|
|
8
|
+
cwd: string;
|
|
9
|
+
name?: string;
|
|
10
|
+
createdAt: string;
|
|
11
|
+
lastUsedAt: string;
|
|
12
|
+
closed?: boolean;
|
|
13
|
+
closedAt?: string;
|
|
14
|
+
pid?: number;
|
|
15
|
+
protocolVersion?: number;
|
|
16
|
+
agentCapabilities?: AgentCapabilities;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
declare function parseTtlSeconds(value: string): number;
|
|
20
|
+
declare function formatPromptSessionBannerLine(record: SessionRecord, currentCwd: string): string;
|
|
21
|
+
declare function main(argv?: string[]): Promise<void>;
|
|
22
|
+
|
|
23
|
+
export { formatPromptSessionBannerLine, main, parseTtlSeconds };
|
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 = {
|
|
@@ -703,6 +706,7 @@ function createOutputFormatter(format, options = {}) {
|
|
|
703
706
|
|
|
704
707
|
// src/session.ts
|
|
705
708
|
import { createHash, randomUUID as randomUUID2 } from "crypto";
|
|
709
|
+
import { statSync } from "fs";
|
|
706
710
|
import fs2 from "fs/promises";
|
|
707
711
|
import net from "net";
|
|
708
712
|
import os from "os";
|
|
@@ -1300,7 +1304,7 @@ var PROCESS_EXIT_GRACE_MS = 1500;
|
|
|
1300
1304
|
var PROCESS_POLL_MS = 50;
|
|
1301
1305
|
var QUEUE_CONNECT_ATTEMPTS = 40;
|
|
1302
1306
|
var QUEUE_CONNECT_RETRY_MS = 50;
|
|
1303
|
-
var
|
|
1307
|
+
var DEFAULT_QUEUE_OWNER_TTL_MS = 3e5;
|
|
1304
1308
|
var TimeoutError = class extends Error {
|
|
1305
1309
|
constructor(timeoutMs) {
|
|
1306
1310
|
super(`Timed out after ${timeoutMs}ms`);
|
|
@@ -1375,7 +1379,9 @@ function parseSessionRecord(raw) {
|
|
|
1375
1379
|
const record = raw;
|
|
1376
1380
|
const name = record.name == null ? void 0 : typeof record.name === "string" && record.name.trim().length > 0 ? record.name.trim() : null;
|
|
1377
1381
|
const pid = record.pid == null ? void 0 : Number.isInteger(record.pid) && record.pid > 0 ? record.pid : null;
|
|
1378
|
-
|
|
1382
|
+
const closed = record.closed == null ? false : typeof record.closed === "boolean" ? record.closed : null;
|
|
1383
|
+
const closedAt = record.closedAt == null ? void 0 : typeof record.closedAt === "string" ? record.closedAt : null;
|
|
1384
|
+
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
1385
|
return null;
|
|
1380
1386
|
}
|
|
1381
1387
|
return {
|
|
@@ -1387,6 +1393,8 @@ function parseSessionRecord(raw) {
|
|
|
1387
1393
|
name,
|
|
1388
1394
|
createdAt: record.createdAt,
|
|
1389
1395
|
lastUsedAt: record.lastUsedAt,
|
|
1396
|
+
closed,
|
|
1397
|
+
closedAt,
|
|
1390
1398
|
pid
|
|
1391
1399
|
};
|
|
1392
1400
|
}
|
|
@@ -1441,6 +1449,35 @@ function toPromptResult(stopReason, sessionId, client) {
|
|
|
1441
1449
|
function absolutePath(value) {
|
|
1442
1450
|
return path2.resolve(value);
|
|
1443
1451
|
}
|
|
1452
|
+
function hasGitDirectory(dir) {
|
|
1453
|
+
const gitPath = path2.join(dir, ".git");
|
|
1454
|
+
try {
|
|
1455
|
+
return statSync(gitPath).isDirectory();
|
|
1456
|
+
} catch {
|
|
1457
|
+
return false;
|
|
1458
|
+
}
|
|
1459
|
+
}
|
|
1460
|
+
function isWithinBoundary(boundary, target) {
|
|
1461
|
+
const relative = path2.relative(boundary, target);
|
|
1462
|
+
return relative.length === 0 || !relative.startsWith("..") && !path2.isAbsolute(relative);
|
|
1463
|
+
}
|
|
1464
|
+
function findGitRepositoryRoot(startDir) {
|
|
1465
|
+
let current = absolutePath(startDir);
|
|
1466
|
+
const root = path2.parse(current).root;
|
|
1467
|
+
for (; ; ) {
|
|
1468
|
+
if (hasGitDirectory(current)) {
|
|
1469
|
+
return current;
|
|
1470
|
+
}
|
|
1471
|
+
if (current === root) {
|
|
1472
|
+
return void 0;
|
|
1473
|
+
}
|
|
1474
|
+
const parent = path2.dirname(current);
|
|
1475
|
+
if (parent === current) {
|
|
1476
|
+
return void 0;
|
|
1477
|
+
}
|
|
1478
|
+
current = parent;
|
|
1479
|
+
}
|
|
1480
|
+
}
|
|
1444
1481
|
function normalizeName(value) {
|
|
1445
1482
|
if (value == null) {
|
|
1446
1483
|
return void 0;
|
|
@@ -1451,6 +1488,15 @@ function normalizeName(value) {
|
|
|
1451
1488
|
function isoNow() {
|
|
1452
1489
|
return (/* @__PURE__ */ new Date()).toISOString();
|
|
1453
1490
|
}
|
|
1491
|
+
function normalizeQueueOwnerTtlMs(ttlMs) {
|
|
1492
|
+
if (ttlMs == null) {
|
|
1493
|
+
return DEFAULT_QUEUE_OWNER_TTL_MS;
|
|
1494
|
+
}
|
|
1495
|
+
if (!Number.isFinite(ttlMs) || ttlMs < 0) {
|
|
1496
|
+
return DEFAULT_QUEUE_OWNER_TTL_MS;
|
|
1497
|
+
}
|
|
1498
|
+
return Math.round(ttlMs);
|
|
1499
|
+
}
|
|
1454
1500
|
function formatError(error) {
|
|
1455
1501
|
if (error instanceof Error) {
|
|
1456
1502
|
return error.message;
|
|
@@ -1906,7 +1952,8 @@ var SessionQueueOwner = class _SessionQueueOwner {
|
|
|
1906
1952
|
return void 0;
|
|
1907
1953
|
}
|
|
1908
1954
|
return await new Promise((resolve) => {
|
|
1909
|
-
const
|
|
1955
|
+
const shouldTimeout = timeoutMs != null;
|
|
1956
|
+
const timer = shouldTimeout && setTimeout(
|
|
1910
1957
|
() => {
|
|
1911
1958
|
const index = this.waiters.indexOf(waiter);
|
|
1912
1959
|
if (index >= 0) {
|
|
@@ -1917,7 +1964,9 @@ var SessionQueueOwner = class _SessionQueueOwner {
|
|
|
1917
1964
|
Math.max(0, timeoutMs)
|
|
1918
1965
|
);
|
|
1919
1966
|
const waiter = (task) => {
|
|
1920
|
-
|
|
1967
|
+
if (timer) {
|
|
1968
|
+
clearTimeout(timer);
|
|
1969
|
+
}
|
|
1921
1970
|
resolve(task);
|
|
1922
1971
|
};
|
|
1923
1972
|
this.waiters.push(waiter);
|
|
@@ -2268,6 +2317,8 @@ async function runSessionPrompt(options) {
|
|
|
2268
2317
|
output.onDone(response.stopReason);
|
|
2269
2318
|
output.flush();
|
|
2270
2319
|
record.lastUsedAt = isoNow();
|
|
2320
|
+
record.closed = false;
|
|
2321
|
+
record.closedAt = void 0;
|
|
2271
2322
|
record.protocolVersion = client.initializeResult?.protocolVersion;
|
|
2272
2323
|
record.agentCapabilities = client.initializeResult?.agentCapabilities;
|
|
2273
2324
|
await writeSessionRecord(record);
|
|
@@ -2343,6 +2394,8 @@ async function createSession(options) {
|
|
|
2343
2394
|
name: normalizeName(options.name),
|
|
2344
2395
|
createdAt: now,
|
|
2345
2396
|
lastUsedAt: now,
|
|
2397
|
+
closed: false,
|
|
2398
|
+
closedAt: void 0,
|
|
2346
2399
|
pid: client.getAgentPid(),
|
|
2347
2400
|
protocolVersion: client.initializeResult?.protocolVersion,
|
|
2348
2401
|
agentCapabilities: client.initializeResult?.agentCapabilities
|
|
@@ -2360,6 +2413,7 @@ async function createSession(options) {
|
|
|
2360
2413
|
}
|
|
2361
2414
|
async function sendSession(options) {
|
|
2362
2415
|
const waitForCompletion = options.waitForCompletion !== false;
|
|
2416
|
+
const queueOwnerTtlMs = normalizeQueueOwnerTtlMs(options.ttlMs);
|
|
2363
2417
|
const queuedToOwner = await trySubmitToRunningOwner({
|
|
2364
2418
|
sessionId: options.sessionId,
|
|
2365
2419
|
message: options.message,
|
|
@@ -2401,9 +2455,16 @@ async function sendSession(options) {
|
|
|
2401
2455
|
timeoutMs: options.timeoutMs,
|
|
2402
2456
|
verbose: options.verbose
|
|
2403
2457
|
});
|
|
2458
|
+
const idleWaitMs = queueOwnerTtlMs === 0 ? void 0 : Math.max(0, queueOwnerTtlMs);
|
|
2404
2459
|
while (true) {
|
|
2405
|
-
const task = await owner.nextTask(
|
|
2460
|
+
const task = await owner.nextTask(idleWaitMs);
|
|
2406
2461
|
if (!task) {
|
|
2462
|
+
if (queueOwnerTtlMs > 0 && options.verbose) {
|
|
2463
|
+
process.stderr.write(
|
|
2464
|
+
`[acpx] queue owner TTL expired after ${Math.round(queueOwnerTtlMs / 1e3)}s for session ${options.sessionId}; shutting down
|
|
2465
|
+
`
|
|
2466
|
+
);
|
|
2467
|
+
}
|
|
2407
2468
|
break;
|
|
2408
2469
|
}
|
|
2409
2470
|
await runQueuedTask(options.sessionId, task, options.verbose);
|
|
@@ -2450,12 +2511,49 @@ async function findSession(options) {
|
|
|
2450
2511
|
if (session.cwd !== normalizedCwd) {
|
|
2451
2512
|
return false;
|
|
2452
2513
|
}
|
|
2514
|
+
if (!options.includeClosed && session.closed) {
|
|
2515
|
+
return false;
|
|
2516
|
+
}
|
|
2453
2517
|
if (normalizedName == null) {
|
|
2454
2518
|
return session.name == null;
|
|
2455
2519
|
}
|
|
2456
2520
|
return session.name === normalizedName;
|
|
2457
2521
|
});
|
|
2458
2522
|
}
|
|
2523
|
+
async function findSessionByDirectoryWalk(options) {
|
|
2524
|
+
const normalizedName = normalizeName(options.name);
|
|
2525
|
+
const normalizedStart = absolutePath(options.cwd);
|
|
2526
|
+
const normalizedBoundary = absolutePath(options.boundary ?? normalizedStart);
|
|
2527
|
+
const walkBoundary = isWithinBoundary(normalizedBoundary, normalizedStart) ? normalizedBoundary : normalizedStart;
|
|
2528
|
+
const sessions = await listSessionsForAgent(options.agentCommand);
|
|
2529
|
+
const matchesScope = (session, dir2) => {
|
|
2530
|
+
if (session.cwd !== dir2) {
|
|
2531
|
+
return false;
|
|
2532
|
+
}
|
|
2533
|
+
if (session.closed) {
|
|
2534
|
+
return false;
|
|
2535
|
+
}
|
|
2536
|
+
if (normalizedName == null) {
|
|
2537
|
+
return session.name == null;
|
|
2538
|
+
}
|
|
2539
|
+
return session.name === normalizedName;
|
|
2540
|
+
};
|
|
2541
|
+
let dir = normalizedStart;
|
|
2542
|
+
for (; ; ) {
|
|
2543
|
+
const match = sessions.find((session) => matchesScope(session, dir));
|
|
2544
|
+
if (match) {
|
|
2545
|
+
return match;
|
|
2546
|
+
}
|
|
2547
|
+
if (dir === walkBoundary) {
|
|
2548
|
+
return void 0;
|
|
2549
|
+
}
|
|
2550
|
+
const parent = path2.dirname(dir);
|
|
2551
|
+
if (parent === dir) {
|
|
2552
|
+
return void 0;
|
|
2553
|
+
}
|
|
2554
|
+
dir = parent;
|
|
2555
|
+
}
|
|
2556
|
+
}
|
|
2459
2557
|
async function terminateQueueOwnerForSession(sessionId) {
|
|
2460
2558
|
const owner = await readQueueOwnerRecord(sessionId);
|
|
2461
2559
|
if (!owner) {
|
|
@@ -2472,8 +2570,10 @@ async function closeSession(sessionId) {
|
|
|
2472
2570
|
if (record.pid != null && isProcessAlive(record.pid) && await isLikelyMatchingProcess(record.pid, record.agentCommand)) {
|
|
2473
2571
|
await terminateProcess(record.pid);
|
|
2474
2572
|
}
|
|
2475
|
-
|
|
2476
|
-
|
|
2573
|
+
record.pid = void 0;
|
|
2574
|
+
record.closed = true;
|
|
2575
|
+
record.closedAt = isoNow();
|
|
2576
|
+
await writeSessionRecord(record);
|
|
2477
2577
|
return record;
|
|
2478
2578
|
}
|
|
2479
2579
|
|
|
@@ -2483,12 +2583,19 @@ var EXIT_CODES = {
|
|
|
2483
2583
|
ERROR: 1,
|
|
2484
2584
|
USAGE: 2,
|
|
2485
2585
|
TIMEOUT: 3,
|
|
2486
|
-
|
|
2586
|
+
NO_SESSION: 4,
|
|
2587
|
+
PERMISSION_DENIED: 5,
|
|
2487
2588
|
INTERRUPTED: 130
|
|
2488
2589
|
};
|
|
2489
2590
|
var OUTPUT_FORMATS = ["text", "json", "quiet"];
|
|
2490
2591
|
|
|
2491
2592
|
// src/cli.ts
|
|
2593
|
+
var NoSessionError = class extends Error {
|
|
2594
|
+
constructor(message) {
|
|
2595
|
+
super(message);
|
|
2596
|
+
this.name = "NoSessionError";
|
|
2597
|
+
}
|
|
2598
|
+
};
|
|
2492
2599
|
var TOP_LEVEL_VERBS = /* @__PURE__ */ new Set(["prompt", "exec", "sessions", "help"]);
|
|
2493
2600
|
function parseOutputFormat(value) {
|
|
2494
2601
|
if (!OUTPUT_FORMATS.includes(value)) {
|
|
@@ -2505,6 +2612,13 @@ function parseTimeoutSeconds(value) {
|
|
|
2505
2612
|
}
|
|
2506
2613
|
return Math.round(parsed * 1e3);
|
|
2507
2614
|
}
|
|
2615
|
+
function parseTtlSeconds(value) {
|
|
2616
|
+
const parsed = Number(value);
|
|
2617
|
+
if (!Number.isFinite(parsed) || parsed < 0) {
|
|
2618
|
+
throw new InvalidArgumentError("TTL must be a non-negative number of seconds");
|
|
2619
|
+
}
|
|
2620
|
+
return Math.round(parsed * 1e3);
|
|
2621
|
+
}
|
|
2508
2622
|
function parseSessionName(value) {
|
|
2509
2623
|
const trimmed = value.trim();
|
|
2510
2624
|
if (trimmed.length === 0) {
|
|
@@ -2569,6 +2683,10 @@ function addGlobalFlags(command) {
|
|
|
2569
2683
|
"--timeout <seconds>",
|
|
2570
2684
|
"Maximum time to wait for agent response",
|
|
2571
2685
|
parseTimeoutSeconds
|
|
2686
|
+
).option(
|
|
2687
|
+
"--ttl <seconds>",
|
|
2688
|
+
"Queue owner idle TTL before shutdown (0 = keep alive forever) (default: 300)",
|
|
2689
|
+
parseTtlSeconds
|
|
2572
2690
|
).option("--verbose", "Enable verbose debug logs");
|
|
2573
2691
|
}
|
|
2574
2692
|
function addSessionOption(command) {
|
|
@@ -2587,6 +2705,7 @@ function resolveGlobalFlags(command) {
|
|
|
2587
2705
|
agent: opts.agent,
|
|
2588
2706
|
cwd: opts.cwd ?? process.cwd(),
|
|
2589
2707
|
timeout: opts.timeout,
|
|
2708
|
+
ttl: opts.ttl ?? DEFAULT_QUEUE_OWNER_TTL_MS,
|
|
2590
2709
|
verbose: opts.verbose,
|
|
2591
2710
|
format: opts.format ?? "text",
|
|
2592
2711
|
approveAll: opts.approveAll,
|
|
@@ -2617,7 +2736,8 @@ function printSessionsByFormat(sessions, format) {
|
|
|
2617
2736
|
}
|
|
2618
2737
|
if (format === "quiet") {
|
|
2619
2738
|
for (const session of sessions) {
|
|
2620
|
-
|
|
2739
|
+
const closedMarker = session.closed ? " [closed]" : "";
|
|
2740
|
+
process.stdout.write(`${session.id}${closedMarker}
|
|
2621
2741
|
`);
|
|
2622
2742
|
}
|
|
2623
2743
|
return;
|
|
@@ -2627,8 +2747,9 @@ function printSessionsByFormat(sessions, format) {
|
|
|
2627
2747
|
return;
|
|
2628
2748
|
}
|
|
2629
2749
|
for (const session of sessions) {
|
|
2750
|
+
const closedMarker = session.closed ? " [closed]" : "";
|
|
2630
2751
|
process.stdout.write(
|
|
2631
|
-
`${session.id} ${session.name ?? "-"} ${session.cwd} ${session.lastUsedAt}
|
|
2752
|
+
`${session.id}${closedMarker} ${session.name ?? "-"} ${session.cwd} ${session.lastUsedAt}
|
|
2632
2753
|
`
|
|
2633
2754
|
);
|
|
2634
2755
|
}
|
|
@@ -2652,6 +2773,33 @@ function printClosedSessionByFormat(record, format) {
|
|
|
2652
2773
|
process.stdout.write(`${record.id}
|
|
2653
2774
|
`);
|
|
2654
2775
|
}
|
|
2776
|
+
function printNewSessionByFormat(record, replaced, format) {
|
|
2777
|
+
if (format === "json") {
|
|
2778
|
+
process.stdout.write(
|
|
2779
|
+
`${JSON.stringify({
|
|
2780
|
+
type: "session_created",
|
|
2781
|
+
id: record.id,
|
|
2782
|
+
sessionId: record.sessionId,
|
|
2783
|
+
name: record.name,
|
|
2784
|
+
replacedSessionId: replaced?.id
|
|
2785
|
+
})}
|
|
2786
|
+
`
|
|
2787
|
+
);
|
|
2788
|
+
return;
|
|
2789
|
+
}
|
|
2790
|
+
if (format === "quiet") {
|
|
2791
|
+
process.stdout.write(`${record.id}
|
|
2792
|
+
`);
|
|
2793
|
+
return;
|
|
2794
|
+
}
|
|
2795
|
+
if (replaced) {
|
|
2796
|
+
process.stdout.write(`${record.id} (replaced ${replaced.id})
|
|
2797
|
+
`);
|
|
2798
|
+
return;
|
|
2799
|
+
}
|
|
2800
|
+
process.stdout.write(`${record.id}
|
|
2801
|
+
`);
|
|
2802
|
+
}
|
|
2655
2803
|
function printQueuedPromptByFormat(result, format) {
|
|
2656
2804
|
if (format === "json") {
|
|
2657
2805
|
process.stdout.write(
|
|
@@ -2670,38 +2818,74 @@ function printQueuedPromptByFormat(result, format) {
|
|
|
2670
2818
|
process.stdout.write(`[queued] ${result.requestId}
|
|
2671
2819
|
`);
|
|
2672
2820
|
}
|
|
2821
|
+
function formatSessionLabel(record) {
|
|
2822
|
+
return record.name ?? "cwd";
|
|
2823
|
+
}
|
|
2824
|
+
function formatRoutedFrom(sessionCwd, currentCwd) {
|
|
2825
|
+
const relative = path3.relative(sessionCwd, currentCwd);
|
|
2826
|
+
if (!relative || relative === ".") {
|
|
2827
|
+
return void 0;
|
|
2828
|
+
}
|
|
2829
|
+
return relative.startsWith(".") ? relative : `.${path3.sep}${relative}`;
|
|
2830
|
+
}
|
|
2831
|
+
function formatPromptSessionBannerLine(record, currentCwd) {
|
|
2832
|
+
const label = formatSessionLabel(record);
|
|
2833
|
+
const normalizedSessionCwd = path3.resolve(record.cwd);
|
|
2834
|
+
const normalizedCurrentCwd = path3.resolve(currentCwd);
|
|
2835
|
+
const routedFrom = normalizedSessionCwd === normalizedCurrentCwd ? void 0 : formatRoutedFrom(normalizedSessionCwd, normalizedCurrentCwd);
|
|
2836
|
+
if (routedFrom) {
|
|
2837
|
+
return `[acpx] session ${label} (${record.id}) \xB7 ${normalizedSessionCwd} (routed from ${routedFrom})`;
|
|
2838
|
+
}
|
|
2839
|
+
return `[acpx] session ${label} (${record.id}) \xB7 ${normalizedSessionCwd}`;
|
|
2840
|
+
}
|
|
2841
|
+
function printPromptSessionBanner(record, currentCwd, format) {
|
|
2842
|
+
if (format === "quiet") {
|
|
2843
|
+
return;
|
|
2844
|
+
}
|
|
2845
|
+
process.stderr.write(`${formatPromptSessionBannerLine(record, currentCwd)}
|
|
2846
|
+
`);
|
|
2847
|
+
}
|
|
2848
|
+
function printCreatedSessionBanner(record, agentName, format) {
|
|
2849
|
+
if (format === "quiet") {
|
|
2850
|
+
return;
|
|
2851
|
+
}
|
|
2852
|
+
const label = formatSessionLabel(record);
|
|
2853
|
+
process.stderr.write(`[acpx] created session ${label} (${record.id})
|
|
2854
|
+
`);
|
|
2855
|
+
process.stderr.write(`[acpx] agent: ${agentName}
|
|
2856
|
+
`);
|
|
2857
|
+
process.stderr.write(`[acpx] cwd: ${record.cwd}
|
|
2858
|
+
`);
|
|
2859
|
+
}
|
|
2673
2860
|
async function handlePrompt(explicitAgentName, promptParts, flags, command) {
|
|
2674
2861
|
const globalFlags = resolveGlobalFlags(command);
|
|
2675
2862
|
const permissionMode = resolvePermissionMode(globalFlags);
|
|
2676
2863
|
const prompt = await readPrompt(promptParts);
|
|
2677
2864
|
const outputFormatter = createOutputFormatter(globalFlags.format);
|
|
2678
2865
|
const agent = resolveAgentInvocation(explicitAgentName, globalFlags);
|
|
2679
|
-
|
|
2866
|
+
const gitRoot = findGitRepositoryRoot(agent.cwd);
|
|
2867
|
+
const walkBoundary = gitRoot ?? agent.cwd;
|
|
2868
|
+
const record = await findSessionByDirectoryWalk({
|
|
2680
2869
|
agentCommand: agent.agentCommand,
|
|
2681
2870
|
cwd: agent.cwd,
|
|
2682
|
-
name: flags.session
|
|
2871
|
+
name: flags.session,
|
|
2872
|
+
boundary: walkBoundary
|
|
2683
2873
|
});
|
|
2684
2874
|
if (!record) {
|
|
2685
|
-
|
|
2686
|
-
|
|
2687
|
-
|
|
2688
|
-
|
|
2689
|
-
|
|
2690
|
-
timeoutMs: globalFlags.timeout,
|
|
2691
|
-
verbose: globalFlags.verbose
|
|
2692
|
-
});
|
|
2693
|
-
if (globalFlags.verbose) {
|
|
2694
|
-
const scope = flags.session ? `named session "${flags.session}"` : "cwd session";
|
|
2695
|
-
process.stderr.write(`[acpx] created ${scope}: ${record.id}
|
|
2696
|
-
`);
|
|
2697
|
-
}
|
|
2875
|
+
const createCmd = flags.session ? `acpx ${agent.agentName} sessions new --name ${flags.session}` : `acpx ${agent.agentName} sessions new`;
|
|
2876
|
+
throw new NoSessionError(
|
|
2877
|
+
`\u26A0 No acpx session found (searched up to ${walkBoundary}).
|
|
2878
|
+
Create one: ${createCmd}`
|
|
2879
|
+
);
|
|
2698
2880
|
}
|
|
2881
|
+
printPromptSessionBanner(record, agent.cwd, globalFlags.format);
|
|
2699
2882
|
const result = await sendSession({
|
|
2700
2883
|
sessionId: record.id,
|
|
2701
2884
|
message: prompt,
|
|
2702
2885
|
permissionMode,
|
|
2703
2886
|
outputFormatter,
|
|
2704
2887
|
timeoutMs: globalFlags.timeout,
|
|
2888
|
+
ttlMs: globalFlags.ttl,
|
|
2705
2889
|
verbose: globalFlags.verbose,
|
|
2706
2890
|
waitForCompletion: flags.wait !== false
|
|
2707
2891
|
});
|
|
@@ -2759,14 +2943,49 @@ async function handleSessionsClose(explicitAgentName, sessionName, command) {
|
|
|
2759
2943
|
const closed = await closeSession(record.id);
|
|
2760
2944
|
printClosedSessionByFormat(closed, globalFlags.format);
|
|
2761
2945
|
}
|
|
2946
|
+
async function handleSessionsNew(explicitAgentName, flags, command) {
|
|
2947
|
+
const globalFlags = resolveGlobalFlags(command);
|
|
2948
|
+
const permissionMode = resolvePermissionMode(globalFlags);
|
|
2949
|
+
const agent = resolveAgentInvocation(explicitAgentName, globalFlags);
|
|
2950
|
+
const replaced = await findSession({
|
|
2951
|
+
agentCommand: agent.agentCommand,
|
|
2952
|
+
cwd: agent.cwd,
|
|
2953
|
+
name: flags.name
|
|
2954
|
+
});
|
|
2955
|
+
if (replaced) {
|
|
2956
|
+
await closeSession(replaced.id);
|
|
2957
|
+
if (globalFlags.verbose) {
|
|
2958
|
+
process.stderr.write(`[acpx] soft-closed prior session: ${replaced.id}
|
|
2959
|
+
`);
|
|
2960
|
+
}
|
|
2961
|
+
}
|
|
2962
|
+
const created = await createSession({
|
|
2963
|
+
agentCommand: agent.agentCommand,
|
|
2964
|
+
cwd: agent.cwd,
|
|
2965
|
+
name: flags.name,
|
|
2966
|
+
permissionMode,
|
|
2967
|
+
timeoutMs: globalFlags.timeout,
|
|
2968
|
+
verbose: globalFlags.verbose
|
|
2969
|
+
});
|
|
2970
|
+
printCreatedSessionBanner(created, agent.agentName, globalFlags.format);
|
|
2971
|
+
if (globalFlags.verbose) {
|
|
2972
|
+
const scope = flags.name ? `named session "${flags.name}"` : "cwd session";
|
|
2973
|
+
process.stderr.write(`[acpx] created ${scope}: ${created.id}
|
|
2974
|
+
`);
|
|
2975
|
+
}
|
|
2976
|
+
printNewSessionByFormat(created, replaced, globalFlags.format);
|
|
2977
|
+
}
|
|
2762
2978
|
function registerSessionsCommand(parent, explicitAgentName) {
|
|
2763
|
-
const sessionsCommand = parent.command("sessions").description("List or close sessions for this agent");
|
|
2979
|
+
const sessionsCommand = parent.command("sessions").description("List, create, or close sessions for this agent");
|
|
2764
2980
|
sessionsCommand.action(async function() {
|
|
2765
2981
|
await handleSessionsList(explicitAgentName, this);
|
|
2766
2982
|
});
|
|
2767
2983
|
sessionsCommand.command("list").description("List sessions").action(async function() {
|
|
2768
2984
|
await handleSessionsList(explicitAgentName, this);
|
|
2769
2985
|
});
|
|
2986
|
+
sessionsCommand.command("new").description("Create a fresh session for current cwd").option("--name <name>", "Session name", parseSessionName).action(async function(flags) {
|
|
2987
|
+
await handleSessionsNew(explicitAgentName, flags, this);
|
|
2988
|
+
});
|
|
2770
2989
|
sessionsCommand.command("close").description("Close session for current cwd").argument("[name]", "Session name", parseSessionName).action(async function(name) {
|
|
2771
2990
|
await handleSessionsClose(explicitAgentName, name, this);
|
|
2772
2991
|
});
|
|
@@ -2817,11 +3036,11 @@ function detectAgentToken(argv) {
|
|
|
2817
3036
|
hasAgentOverride = true;
|
|
2818
3037
|
continue;
|
|
2819
3038
|
}
|
|
2820
|
-
if (token === "--cwd" || token === "--format" || token === "--timeout") {
|
|
3039
|
+
if (token === "--cwd" || token === "--format" || token === "--timeout" || token === "--ttl") {
|
|
2821
3040
|
index += 1;
|
|
2822
3041
|
continue;
|
|
2823
3042
|
}
|
|
2824
|
-
if (token.startsWith("--cwd=") || token.startsWith("--format=") || token.startsWith("--timeout=")) {
|
|
3043
|
+
if (token.startsWith("--cwd=") || token.startsWith("--format=") || token.startsWith("--timeout=") || token.startsWith("--ttl=")) {
|
|
2825
3044
|
continue;
|
|
2826
3045
|
}
|
|
2827
3046
|
if (token === "--approve-all" || token === "--approve-reads" || token === "--deny-all" || token === "--verbose") {
|
|
@@ -2831,7 +3050,11 @@ function detectAgentToken(argv) {
|
|
|
2831
3050
|
}
|
|
2832
3051
|
return { hasAgentOverride };
|
|
2833
3052
|
}
|
|
2834
|
-
async function main() {
|
|
3053
|
+
async function main(argv = process.argv) {
|
|
3054
|
+
await maybeHandleSkillflag(argv, {
|
|
3055
|
+
skillsRoot: findSkillsRoot(import.meta.url),
|
|
3056
|
+
includeBundledSkill: false
|
|
3057
|
+
});
|
|
2835
3058
|
const program = new Command();
|
|
2836
3059
|
program.name("acpx").description("Headless CLI client for the Agent Client Protocol").showHelpAfterError();
|
|
2837
3060
|
addGlobalFlags(program);
|
|
@@ -2840,7 +3063,7 @@ async function main() {
|
|
|
2840
3063
|
registerAgentCommand(program, agentName);
|
|
2841
3064
|
}
|
|
2842
3065
|
registerDefaultCommands(program);
|
|
2843
|
-
const scan = detectAgentToken(
|
|
3066
|
+
const scan = detectAgentToken(argv.slice(2));
|
|
2844
3067
|
if (!scan.hasAgentOverride && scan.token && !TOP_LEVEL_VERBS.has(scan.token) && !builtInAgents.includes(scan.token)) {
|
|
2845
3068
|
registerAgentCommand(program, scan.token);
|
|
2846
3069
|
}
|
|
@@ -2855,13 +3078,16 @@ async function main() {
|
|
|
2855
3078
|
"after",
|
|
2856
3079
|
`
|
|
2857
3080
|
Examples:
|
|
3081
|
+
acpx codex sessions new
|
|
2858
3082
|
acpx codex "fix the tests"
|
|
2859
3083
|
acpx codex prompt "fix the tests"
|
|
2860
3084
|
acpx codex --no-wait "queue follow-up task"
|
|
2861
3085
|
acpx codex exec "what does this repo do"
|
|
2862
3086
|
acpx codex -s backend "fix the API"
|
|
2863
3087
|
acpx codex sessions
|
|
3088
|
+
acpx codex sessions new --name backend
|
|
2864
3089
|
acpx codex sessions close backend
|
|
3090
|
+
acpx --ttl 30 codex "investigate flaky tests"
|
|
2865
3091
|
acpx claude "refactor auth"
|
|
2866
3092
|
acpx gemini "add logging"
|
|
2867
3093
|
acpx --agent ./my-custom-server "do something"`
|
|
@@ -2870,7 +3096,7 @@ Examples:
|
|
|
2870
3096
|
throw error;
|
|
2871
3097
|
});
|
|
2872
3098
|
try {
|
|
2873
|
-
await program.parseAsync(
|
|
3099
|
+
await program.parseAsync(argv);
|
|
2874
3100
|
} catch (error) {
|
|
2875
3101
|
if (error instanceof CommanderError) {
|
|
2876
3102
|
if (error.code === "commander.helpDisplayed" || error.code === "commander.version") {
|
|
@@ -2886,10 +3112,34 @@ Examples:
|
|
|
2886
3112
|
`);
|
|
2887
3113
|
process.exit(EXIT_CODES.TIMEOUT);
|
|
2888
3114
|
}
|
|
3115
|
+
if (error instanceof NoSessionError) {
|
|
3116
|
+
process.stderr.write(`${error.message}
|
|
3117
|
+
`);
|
|
3118
|
+
process.exit(EXIT_CODES.NO_SESSION);
|
|
3119
|
+
}
|
|
2889
3120
|
const message = error instanceof Error ? error.message : String(error);
|
|
2890
3121
|
process.stderr.write(`${message}
|
|
2891
3122
|
`);
|
|
2892
3123
|
process.exit(EXIT_CODES.ERROR);
|
|
2893
3124
|
}
|
|
2894
3125
|
}
|
|
2895
|
-
|
|
3126
|
+
function isCliEntrypoint(argv) {
|
|
3127
|
+
const entry = argv[1];
|
|
3128
|
+
if (!entry) {
|
|
3129
|
+
return false;
|
|
3130
|
+
}
|
|
3131
|
+
try {
|
|
3132
|
+
const resolved = pathToFileURL(realpathSync(entry)).href;
|
|
3133
|
+
return import.meta.url === resolved;
|
|
3134
|
+
} catch {
|
|
3135
|
+
return false;
|
|
3136
|
+
}
|
|
3137
|
+
}
|
|
3138
|
+
if (isCliEntrypoint(process.argv)) {
|
|
3139
|
+
void main(process.argv);
|
|
3140
|
+
}
|
|
3141
|
+
export {
|
|
3142
|
+
formatPromptSessionBannerLine,
|
|
3143
|
+
main,
|
|
3144
|
+
parseTtlSeconds
|
|
3145
|
+
};
|
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.4",
|
|
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
|
+
```
|