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 +38 -4
- package/dist/cli.d.ts +19 -1
- package/dist/cli.js +130 -16
- package/package.json +1 -1
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
|
|
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
|
|
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
|
|
157
|
-
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
2763
|
-
|
|
2764
|
-
|
|
2765
|
-
|
|
2766
|
-
|
|
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
|
};
|