acpx 0.1.3 → 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 CHANGED
@@ -17,6 +17,7 @@ One command surface for Codex, Claude, Gemini, OpenCode, Pi, or custom ACP serve
17
17
  - **One-shot mode**: `exec` for stateless fire-and-forget tasks
18
18
 
19
19
  ```bash
20
+ $ acpx codex sessions new
20
21
  $ acpx codex "find the flaky test and fix it"
21
22
 
22
23
  [thinking] Investigating test suite for flaky failures
@@ -44,6 +45,34 @@ reading stale state from the previous run.
44
45
  [done] end_turn
45
46
  ```
46
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
+
47
76
  ## Install
48
77
 
49
78
  ```bash
@@ -67,13 +96,16 @@ The only prerequisite is the underlying coding agent you want to use:
67
96
  ## Usage examples
68
97
 
69
98
  ```bash
70
- acpx codex 'fix the tests' # implicit prompt (persistent session)
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)
71
101
  acpx codex prompt 'fix the tests' # explicit prompt subcommand
72
102
  acpx codex --no-wait 'draft test migration plan' # enqueue without waiting if session is busy
73
103
  acpx exec 'summarize this repo' # default agent shortcut (codex)
74
104
  acpx codex exec 'what does this repo do?' # one-shot, no saved session
75
105
 
76
- acpx codex -s api 'implement cursor pagination' # named session
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
77
109
  acpx codex -s docs 'rewrite API docs' # parallel work in another named session
78
110
 
79
111
  acpx codex sessions # list sessions for codex command
@@ -153,8 +185,10 @@ acpx --agent ./my-custom-acp-server 'do something'
153
185
 
154
186
  ## Session behavior
155
187
 
156
- - Prompt commands use saved sessions scoped to `(agent command, cwd, optional name)`.
157
- - `-s <name>` creates/selects a parallel named session in the same repo.
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.
158
192
  - `sessions new [--name <name>]` creates a fresh session for that scope and soft-closes the prior one.
159
193
  - `sessions close [name]` soft-closes the session: queue owner/processes are terminated, record is kept with `closed: true`.
160
194
  - Auto-resume for cwd scope skips sessions marked closed.
package/dist/cli.d.ts CHANGED
@@ -1,5 +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
+
2
19
  declare function parseTtlSeconds(value: string): number;
20
+ declare function formatPromptSessionBannerLine(record: SessionRecord, currentCwd: string): string;
3
21
  declare function main(argv?: string[]): Promise<void>;
4
22
 
5
- export { main, parseTtlSeconds };
23
+ export { formatPromptSessionBannerLine, main, parseTtlSeconds };
package/dist/cli.js CHANGED
@@ -706,6 +706,7 @@ function createOutputFormatter(format, options = {}) {
706
706
 
707
707
  // src/session.ts
708
708
  import { createHash, randomUUID as randomUUID2 } from "crypto";
709
+ import { statSync } from "fs";
709
710
  import fs2 from "fs/promises";
710
711
  import net from "net";
711
712
  import os from "os";
@@ -1448,6 +1449,35 @@ function toPromptResult(stopReason, sessionId, client) {
1448
1449
  function absolutePath(value) {
1449
1450
  return path2.resolve(value);
1450
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
+ }
1451
1481
  function normalizeName(value) {
1452
1482
  if (value == null) {
1453
1483
  return void 0;
@@ -2490,6 +2520,40 @@ async function findSession(options) {
2490
2520
  return session.name === normalizedName;
2491
2521
  });
2492
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
+ }
2493
2557
  async function terminateQueueOwnerForSession(sessionId) {
2494
2558
  const owner = await readQueueOwnerRecord(sessionId);
2495
2559
  if (!owner) {
@@ -2519,12 +2583,19 @@ var EXIT_CODES = {
2519
2583
  ERROR: 1,
2520
2584
  USAGE: 2,
2521
2585
  TIMEOUT: 3,
2522
- PERMISSION_DENIED: 4,
2586
+ NO_SESSION: 4,
2587
+ PERMISSION_DENIED: 5,
2523
2588
  INTERRUPTED: 130
2524
2589
  };
2525
2590
  var OUTPUT_FORMATS = ["text", "json", "quiet"];
2526
2591
 
2527
2592
  // src/cli.ts
2593
+ var NoSessionError = class extends Error {
2594
+ constructor(message) {
2595
+ super(message);
2596
+ this.name = "NoSessionError";
2597
+ }
2598
+ };
2528
2599
  var TOP_LEVEL_VERBS = /* @__PURE__ */ new Set(["prompt", "exec", "sessions", "help"]);
2529
2600
  function parseOutputFormat(value) {
2530
2601
  if (!OUTPUT_FORMATS.includes(value)) {
@@ -2747,32 +2818,67 @@ function printQueuedPromptByFormat(result, format) {
2747
2818
  process.stdout.write(`[queued] ${result.requestId}
2748
2819
  `);
2749
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
+ }
2750
2860
  async function handlePrompt(explicitAgentName, promptParts, flags, command) {
2751
2861
  const globalFlags = resolveGlobalFlags(command);
2752
2862
  const permissionMode = resolvePermissionMode(globalFlags);
2753
2863
  const prompt = await readPrompt(promptParts);
2754
2864
  const outputFormatter = createOutputFormatter(globalFlags.format);
2755
2865
  const agent = resolveAgentInvocation(explicitAgentName, globalFlags);
2756
- let record = await findSession({
2866
+ const gitRoot = findGitRepositoryRoot(agent.cwd);
2867
+ const walkBoundary = gitRoot ?? agent.cwd;
2868
+ const record = await findSessionByDirectoryWalk({
2757
2869
  agentCommand: agent.agentCommand,
2758
2870
  cwd: agent.cwd,
2759
- name: flags.session
2871
+ name: flags.session,
2872
+ boundary: walkBoundary
2760
2873
  });
2761
2874
  if (!record) {
2762
- record = await createSession({
2763
- agentCommand: agent.agentCommand,
2764
- cwd: agent.cwd,
2765
- name: flags.session,
2766
- permissionMode,
2767
- timeoutMs: globalFlags.timeout,
2768
- verbose: globalFlags.verbose
2769
- });
2770
- if (globalFlags.verbose) {
2771
- const scope = flags.session ? `named session "${flags.session}"` : "cwd session";
2772
- process.stderr.write(`[acpx] created ${scope}: ${record.id}
2773
- `);
2774
- }
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
+ );
2775
2880
  }
2881
+ printPromptSessionBanner(record, agent.cwd, globalFlags.format);
2776
2882
  const result = await sendSession({
2777
2883
  sessionId: record.id,
2778
2884
  message: prompt,
@@ -2861,6 +2967,7 @@ async function handleSessionsNew(explicitAgentName, flags, command) {
2861
2967
  timeoutMs: globalFlags.timeout,
2862
2968
  verbose: globalFlags.verbose
2863
2969
  });
2970
+ printCreatedSessionBanner(created, agent.agentName, globalFlags.format);
2864
2971
  if (globalFlags.verbose) {
2865
2972
  const scope = flags.name ? `named session "${flags.name}"` : "cwd session";
2866
2973
  process.stderr.write(`[acpx] created ${scope}: ${created.id}
@@ -2971,6 +3078,7 @@ async function main(argv = process.argv) {
2971
3078
  "after",
2972
3079
  `
2973
3080
  Examples:
3081
+ acpx codex sessions new
2974
3082
  acpx codex "fix the tests"
2975
3083
  acpx codex prompt "fix the tests"
2976
3084
  acpx codex --no-wait "queue follow-up task"
@@ -3004,6 +3112,11 @@ Examples:
3004
3112
  `);
3005
3113
  process.exit(EXIT_CODES.TIMEOUT);
3006
3114
  }
3115
+ if (error instanceof NoSessionError) {
3116
+ process.stderr.write(`${error.message}
3117
+ `);
3118
+ process.exit(EXIT_CODES.NO_SESSION);
3119
+ }
3007
3120
  const message = error instanceof Error ? error.message : String(error);
3008
3121
  process.stderr.write(`${message}
3009
3122
  `);
@@ -3026,6 +3139,7 @@ if (isCliEntrypoint(process.argv)) {
3026
3139
  void main(process.argv);
3027
3140
  }
3028
3141
  export {
3142
+ formatPromptSessionBannerLine,
3029
3143
  main,
3030
3144
  parseTtlSeconds
3031
3145
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "acpx",
3
- "version": "0.1.3",
3
+ "version": "0.1.4",
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": [