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 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
@@ -1 +1,5 @@
1
1
  #!/usr/bin/env node
2
+ declare function parseTtlSeconds(value: string): number;
3
+ declare function main(argv?: string[]): Promise<void>;
4
+
5
+ export { 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 = {
@@ -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 QUEUE_IDLE_DRAIN_WAIT_MS = 150;
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
- 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) {
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 timer = setTimeout(
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
- clearTimeout(timer);
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(QUEUE_IDLE_DRAIN_WAIT_MS);
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
- const file = sessionFilePath(record.id);
2476
- await fs2.unlink(file);
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
- process.stdout.write(`${session.id}
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(process.argv.slice(2));
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(process.argv);
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
- void main();
3013
+ function isCliEntrypoint(argv) {
3014
+ const entry = argv[1];
3015
+ if (!entry) {
3016
+ return false;
3017
+ }
3018
+ try {
3019
+ const resolved = pathToFileURL(realpathSync(entry)).href;
3020
+ return import.meta.url === resolved;
3021
+ } catch {
3022
+ return false;
3023
+ }
3024
+ }
3025
+ if (isCliEntrypoint(process.argv)) {
3026
+ void main(process.argv);
3027
+ }
3028
+ export {
3029
+ main,
3030
+ parseTtlSeconds
3031
+ };
package/package.json CHANGED
@@ -1,10 +1,11 @@
1
1
  {
2
2
  "name": "acpx",
3
- "version": "0.1.2",
4
- "description": "Headless CLI client for the Agent Client Protocol (ACP) \u2014 talk to coding agents from the command line",
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
- "release-it": {
60
- "git": {
61
- "requireCleanWorkingDir": true,
62
- "requireBranch": "main",
63
- "commitMessage": "chore(release): ${version}",
64
- "tagName": "v${version}",
65
- "push": true
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
+ ```