chore: add npm-first docs, lint/format tooling, and CI workflows

This commit is contained in:
Bob 2026-02-18 00:00:33 +01:00
parent 9fd2a621bd
commit 38e8e5f330
21 changed files with 1685 additions and 200 deletions

12
.editorconfig Normal file
View File

@ -0,0 +1,12 @@
root = true
[*]
charset = utf-8
end_of_line = lf
insert_final_newline = true
indent_style = space
indent_size = 2
trim_trailing_whitespace = true
[*.md]
trim_trailing_whitespace = false

36
.github/workflows/ci.yml vendored Normal file
View File

@ -0,0 +1,36 @@
name: CI
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
checks:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: 20
cache: npm
- name: Install dependencies
run: npm ci
- name: Typecheck
run: npm run typecheck
- name: Lint
run: npm run lint
- name: Format check
run: npm run format:check
- name: Build
run: npm run build

39
.github/workflows/release.yml vendored Normal file
View File

@ -0,0 +1,39 @@
name: Release
on:
push:
tags:
- "v*"
jobs:
release:
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: 20
registry-url: https://registry.npmjs.org
cache: npm
- name: Install dependencies
run: npm ci
- name: Build
run: npm run build
- name: Publish to npm
run: npm publish
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
- name: Create GitHub release
uses: softprops/action-gh-release@v2
with:
generate_release_notes: true

3
.prettierignore Normal file
View File

@ -0,0 +1,3 @@
dist/
node_modules/
*.tgz

6
.prettierrc Normal file
View File

@ -0,0 +1,6 @@
{
"printWidth": 88,
"singleQuote": false,
"trailingComma": "all",
"semi": true
}

View File

@ -63,13 +63,14 @@ Built-in friendly names map to commands:
```ts
const AGENT_REGISTRY: Record<string, string> = {
codex: 'npx @zed-industries/codex-acp',
claude: 'npx @zed-industries/claude-agent-acp',
gemini: 'gemini',
codex: "npx @zed-industries/codex-acp",
claude: "npx @zed-industries/claude-agent-acp",
gemini: "gemini",
};
```
Rules:
- Known names resolve automatically.
- Unknown names are treated as raw commands.
- Escape hatch: `--agent <command>` sets a raw command explicitly.
@ -140,14 +141,14 @@ Refactored the auth module to use async/await. All 42 tests passing.
## Exit Codes
| Code | Meaning |
|------|---------|
| 0 | Success |
| 1 | Agent/protocol error |
| 2 | CLI usage error |
| 3 | Timeout |
| Code | Meaning |
| ---- | ---------------------------------------- |
| 0 | Success |
| 1 | Agent/protocol error |
| 2 | CLI usage error |
| 3 | Timeout |
| 4 | Permission denied (all options rejected) |
| 130 | Interrupted (Ctrl+C) |
| 130 | Interrupted (Ctrl+C) |
## Tech Stack

15
CONTRIBUTING.md Normal file
View File

@ -0,0 +1,15 @@
# Contributing
1. Fork the repository.
2. Create a branch for your change.
3. Make your changes with focused commits.
4. Run checks locally:
```bash
npm run typecheck
npm run lint
npm run format:check
npm run build
```
5. Open a pull request to `main` with a clear summary.

135
README.md
View File

@ -1,139 +1,60 @@
# acpx
Headless CLI client for the [Agent Client Protocol (ACP)](https://agentclientprotocol.com) — talk to coding agents from the command line.
Headless CLI client for the [Agent Client Protocol (ACP)](https://agentclientprotocol.com).
```bash
# Conversational prompt (persistent session, auto-resume by agent+cwd)
acpx codex "fix the tests"
# Explicit prompt verb (same behavior)
acpx codex prompt "fix the tests"
# One-shot execution (no saved session)
acpx codex exec "what does this repo do"
# Named session
acpx codex -s backend "fix the API"
# Session management
acpx codex sessions
acpx codex sessions close
acpx codex sessions close backend
```
## Why?
ACP adapters exist for every major coding agent ([Codex](https://github.com/zed-industries/codex-acp), [Claude Code](https://github.com/zed-industries/claude-code-acp), [Gemini CLI](https://github.com/google-gemini/gemini-cli), etc.) but every ACP client is a GUI app or editor plugin.
`acpx` is the missing piece: a simple CLI that lets **agents talk to agents** (or humans script agents) over structured ACP instead of scraping terminal output.
`acpx` is built for scriptable, session-aware agent usage from the terminal.
## Install
```bash
npm install -g acpx
# or
npx acpx codex "hello"
npm i -g acpx
```
### Prerequisites
`acpx` manages persistent sessions, so prefer a global install. Avoid `npx acpx ...` for normal use.
You need an ACP-compatible agent installed:
## Agent prerequisites
Install at least one ACP-compatible agent adapter:
```bash
# Codex ACP adapter
npm install -g @zed-industries/codex-acp
# Claude ACP adapter
npm install -g @zed-industries/claude-agent-acp
# Gemini CLI (native ACP support)
npm install -g @google/gemini-cli
```
## Usage
### Command grammar
## Core usage
```bash
acpx <agent> [prompt] <text>
acpx <agent> exec <text>
acpx <agent> sessions [list|close]
acpx codex 'fix the tests' # implicit prompt, auto-resume session
acpx codex prompt 'fix the tests' # same, explicit
acpx codex exec 'what does this repo do' # one-shot, no session
acpx codex -s backend 'fix the API' # named session
acpx codex sessions # list sessions
acpx claude 'refactor auth' # different agent
```
`prompt` is the default verb, so `acpx codex "..."` and `acpx codex prompt "..."` are equivalent.
## Session behavior
### Built-in agent registry
- Default mode is conversational: prompts use a saved session scoped to `(agent command, cwd)`.
- `-s <name>` switches to a named session scoped to `(agent command, cwd, name)`.
- `exec` is fire-and-forget: temporary session, prompt once, then discard.
Friendly names are resolved automatically:
Session files are stored in `~/.acpx/sessions/`.
- `codex` -> `npx @zed-industries/codex-acp`
- `claude` -> `npx @zed-industries/claude-agent-acp`
- `gemini` -> `gemini`
## Built-in agents and custom servers
Unknown agent names are treated as raw commands. You can also use the explicit escape hatch:
Built-ins:
- `codex`
- `claude`
- `gemini`
Use `--agent` as an escape hatch for custom ACP servers:
```bash
acpx --agent ./my-custom-server "do something"
acpx --agent ./my-custom-acp-server 'do something'
```
### Session behavior
- `prompt` always uses a saved session.
- Sessions auto-resume by `(agent command, cwd)`.
- `-s, --session <name>` uses a named session for that `(agent command, cwd)`.
- `exec` is fire-and-forget (temporary session, not saved).
Examples:
```bash
acpx codex "fix the tests"
acpx codex -s backend "fix the API"
acpx claude "refactor auth"
acpx gemini "add logging"
```
### Default agent shortcuts
If agent is omitted, default agent is `codex`:
```bash
acpx prompt "fix tests"
acpx exec "summarize this repo"
acpx sessions
```
### Global options (before agent name)
```text
--agent <command> Raw ACP agent command (escape hatch)
--cwd <dir> Working directory (default: .)
--approve-all Auto-approve all permission requests
--approve-reads Auto-approve reads/searches, prompt for writes
--deny-all Deny all permission requests
--format <fmt> Output format: text (default), json, quiet
--timeout <seconds> Maximum time to wait for agent response
--verbose Enable debug output on stderr
```
### Output formats
| Format | Flag | Description |
|--------|------|-------------|
| text | `--format text` | Human-readable streaming (default) |
| json | `--format json` | Structured ndjson for machines |
| quiet | `--format quiet` | Final text output only |
## How it works
```
┌─────────┐ stdio/ndjson ┌──────────────┐ wraps ┌─────────┐
│ acpx │ ◄──────────────────► │ ACP adapter │ ◄───────────► │ Agent │
│ (client)│ ACP protocol │ (codex-acp) │ │ (Codex) │
└─────────┘ └──────────────┘ └─────────┘
```
acpx spawns the ACP adapter as a child process, communicates over JSON-RPC/ndjson over stdio, and streams structured events (tool calls, text, permissions).
## License
Apache-2.0

View File

@ -0,0 +1,51 @@
---
title: acpx Agent Registry
description: Built-in agent mappings, name resolution rules, and custom adapter usage with --agent.
author: Bob <bob@dutifulbob.com>
date: 2026-02-17
---
## Built-in registry
`src/agent-registry.ts` defines friendly names:
- `codex -> npx @zed-industries/codex-acp`
- `claude -> npx @zed-industries/claude-agent-acp`
- `gemini -> gemini`
Default agent is `codex`.
## Resolution behavior
When you run `acpx <agent> ...`:
1. agent token is normalized (trim + lowercase)
2. if it matches a built-in key, `acpx` uses the mapped command
3. if it does not match, `acpx` treats it as a raw command
This means custom names work without any registry file edits.
## `--agent` escape hatch
`--agent <command>` forces a raw adapter command and bypasses positional agent resolution.
Example:
```bash
acpx --agent ./my-custom-acp-server 'summarize this repo'
```
Rules:
- do not combine a positional agent with `--agent`
- the command string is parsed into executable + args before spawn
- the chosen command is what session scoping uses
## Practical guidance
Use built-ins for common adapters (`codex`, `claude`, `gemini`).
Use `--agent` when you need:
- local development adapters
- pinned binaries/scripts
- non-standard ACP servers

View File

@ -0,0 +1,69 @@
---
title: acpx Architecture
description: Internal architecture and runtime flow from CLI command to ACP session updates.
author: Bob <bob@dutifulbob.com>
date: 2026-02-17
---
## Overview
`acpx` is a CLI client that speaks ACP over stdio.
Data path:
`CLI command -> AcpClient -> ndjson/stdio -> ACP adapter -> coding agent`
The CLI never scrapes terminal text from an interactive UI. It talks structured ACP JSON-RPC messages directly.
## Core components
- `src/cli.ts`: command grammar, flags, output mode selection, and top-level command handling.
- `src/client.ts`: ACP transport and protocol methods. Spawns the adapter process and connects with `ClientSideConnection` + `ndJsonStream`.
- `src/session.ts`: session persistence, resume/create logic, timeout/interrupt handling, and lifecycle cleanup.
- `src/permissions.ts`: permission policy (`approve-all`, `approve-reads`, `deny-all`) and interactive fallback.
- `src/output.ts`: streaming text/json/quiet output formatters.
## Protocol flow
Typical prompt flow:
1. `initialize`
2. `newSession` or `loadSession`
3. `prompt`
4. stream `sessionUpdate` notifications until done
Details:
- `initialize` advertises client capabilities (`fs.readTextFile`, `fs.writeTextFile`, `terminal`).
- If a saved session exists and agent supports it, `loadSession` is attempted.
- If load fails with not-found style errors, `acpx` falls back to `newSession`.
- Prompt responses and notifications are streamed through the active formatter.
## Session persistence
Session metadata is stored in `~/.acpx/sessions/*.json`.
Each record includes:
- stable file/session id
- ACP session id
- agent command
- cwd
- optional named session
- timestamps (`createdAt`, `lastUsedAt`)
- adapter process pid (when known)
- protocol/version capabilities snapshot
This lets `acpx` resume conversational context by default.
## Permission handling
Permission requests come in through ACP `requestPermission` callbacks.
Modes:
- `approve-all`: auto-approve first allow option
- `approve-reads`: auto-approve read/search; prompt for others
- `deny-all`: reject when possible
`acpx` tracks permission stats (requested/approved/denied/cancelled) and uses them for exit-code behavior.

View File

@ -0,0 +1,83 @@
---
title: acpx Session Management
description: How acpx resumes, names, stores, and closes sessions including pid tracking and subprocess lifecycle.
author: Bob <bob@dutifulbob.com>
date: 2026-02-17
---
## Session model
`acpx` is conversational by default.
Session lookup is scoped by:
- agent command
- cwd
- optional session name (`-s <name>`)
No `-s` means the default cwd session for that agent command.
## Auto-resume behavior
For prompt commands:
1. `findSession` searches stored records by `(agentCommand, cwd, name?)`.
2. If no record exists, `createSession` creates ACP session + record.
3. `sendSession` starts a fresh adapter process and tries `loadSession`.
4. If load is unsupported or fails with known not-found/invalid errors, it falls back to `newSession`.
5. After prompt completes, record metadata is updated and re-written.
## Named sessions
`-s backend` creates a parallel conversation stream for the same agent and cwd.
Example:
- default session: `acpx codex 'fix tests'`
- named session: `acpx codex -s backend 'fix API'`
Both can coexist because names are part of the scope key.
## Session files
Stored under `~/.acpx/sessions/` as JSON files.
Record fields include:
- `id`
- `sessionId`
- `agentCommand`
- `cwd`
- `name` (optional)
- `createdAt`, `lastUsedAt`
- `pid` (adapter process pid, optional)
- `protocolVersion`, `agentCapabilities` (optional)
Writes are done via temp file + rename for safer updates.
## loadSession protocol flow
Resume path in `sendSession`:
1. start ACP client process
2. initialize protocol
3. `loadSession(sessionId, cwd, mcpServers: [])`
4. suppress replayed updates during load
5. wait for session-update drain
6. send new prompt
If resume fails with a fallback-eligible error, `newSession` is used and stored `sessionId` is replaced.
## PID tracking and process lifecycle
`acpx` stores the adapter pid in each session record to help with cleanup and diagnostics.
Lifecycle behavior:
- each command launches a new adapter subprocess
- records track pid of the latest process used
- `closeSession` tries to terminate the stored pid if still alive and likely matches expected command
- process termination uses `SIGTERM` then `SIGKILL` fallback
- signal handling (`SIGINT`, `SIGTERM`) closes client resources before exit
This keeps session files and local processes in sync while remaining robust to stale pids.

36
eslint.config.js Normal file
View File

@ -0,0 +1,36 @@
import js from "@eslint/js";
import globals from "globals";
import tseslint from "typescript-eslint";
export default tseslint.config(
{
ignores: ["dist/**", "node_modules/**", "*.tgz"],
},
js.configs.recommended,
...tseslint.configs.recommended,
{
files: ["src/**/*.ts"],
languageOptions: {
globals: {
...globals.node,
},
},
rules: {
"@typescript-eslint/no-unused-vars": [
"error",
{
argsIgnorePattern: "^_",
varsIgnorePattern: "^_",
},
],
"@typescript-eslint/consistent-type-imports": [
"error",
{
prefer: "type-imports",
fixStyle: "inline-type-imports",
},
],
"prefer-promise-reject-errors": "off",
},
},
);

1203
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -15,7 +15,10 @@
"build": "tsup src/cli.ts --format esm --dts --clean",
"prepack": "npm run build",
"dev": "tsx src/cli.ts",
"typecheck": "tsc --noEmit"
"typecheck": "tsc --noEmit",
"lint": "eslint src --max-warnings=0",
"format": "prettier --write .",
"format:check": "prettier --check ."
},
"keywords": [
"acp",
@ -40,9 +43,14 @@
"commander": "^13.0.0"
},
"devDependencies": {
"@eslint/js": "^10.0.1",
"@types/node": "^22.0.0",
"eslint": "^10.0.0",
"globals": "^17.3.0",
"prettier": "^3.8.1",
"tsup": "^8.0.0",
"tsx": "^4.0.0",
"typescript": "^5.7.0",
"@types/node": "^22.0.0"
"typescript-eslint": "^8.56.0"
}
}

View File

@ -110,9 +110,7 @@ async function main() {
// Create streams to communicate with the agent
const input = Writable.toWeb(agentProcess.stdin!);
const output = Readable.toWeb(
agentProcess.stdout!,
) as ReadableStream<Uint8Array>;
const output = Readable.toWeb(agentProcess.stdout!) as ReadableStream<Uint8Array>;
// Create the client connection
const client = new ExampleClient();
@ -131,9 +129,7 @@ async function main() {
},
});
console.log(
`✅ Connected to agent (protocol v${initResult.protocolVersion})`,
);
console.log(`✅ Connected to agent (protocol v${initResult.protocolVersion})`);
// Create a new session
const sessionResult = await connection.newSession({

View File

@ -69,14 +69,19 @@ function resolveToolKindForPermission(
params: RequestPermissionRequest,
toolName: string | undefined,
): string | undefined {
const toolCall = params.toolCall as unknown as { kind?: unknown; title?: unknown } | undefined;
const kindRaw = typeof toolCall?.kind === "string" ? toolCall.kind.trim().toLowerCase() : "";
const toolCall = params.toolCall as unknown as
| { kind?: unknown; title?: unknown }
| undefined;
const kindRaw =
typeof toolCall?.kind === "string" ? toolCall.kind.trim().toLowerCase() : "";
if (kindRaw) {
return kindRaw;
}
const name =
toolName ??
parseToolNameFromTitle(typeof toolCall?.title === "string" ? toolCall.title : undefined);
parseToolNameFromTitle(
typeof toolCall?.title === "string" ? toolCall.title : undefined,
);
if (!name) {
return undefined;
}
@ -98,7 +103,11 @@ function resolveToolKindForPermission(
if (normalized.includes("fetch") || normalized.includes("http")) {
return "fetch";
}
if (normalized.includes("write") || normalized.includes("edit") || normalized.includes("patch")) {
if (
normalized.includes("write") ||
normalized.includes("edit") ||
normalized.includes("patch")
) {
return "edit";
}
if (normalized.includes("delete") || normalized.includes("remove")) {
@ -107,19 +116,30 @@ function resolveToolKindForPermission(
if (normalized.includes("move") || normalized.includes("rename")) {
return "move";
}
if (normalized.includes("exec") || normalized.includes("run") || normalized.includes("bash")) {
if (
normalized.includes("exec") ||
normalized.includes("run") ||
normalized.includes("bash")
) {
return "execute";
}
return "other";
}
function resolveToolNameForPermission(params: RequestPermissionRequest): string | undefined {
function resolveToolNameForPermission(
params: RequestPermissionRequest,
): string | undefined {
const toolCall = params.toolCall;
const toolMeta = asRecord(toolCall?._meta);
const rawInput = asRecord(toolCall?.rawInput);
const fromMeta = readFirstStringValue(toolMeta, ["toolName", "tool_name", "name"]);
const fromRawInput = readFirstStringValue(rawInput, ["tool", "toolName", "tool_name", "name"]);
const fromRawInput = readFirstStringValue(rawInput, [
"tool",
"toolName",
"tool_name",
"name",
]);
const fromTitle = parseToolNameFromTitle(toolCall?.title);
return normalizeToolName(fromMeta ?? fromRawInput ?? fromTitle ?? "");
}
@ -145,9 +165,14 @@ function cancelledPermission(): RequestPermissionResponse {
return { outcome: { outcome: "cancelled" } };
}
function promptUserPermission(toolName: string | undefined, toolTitle?: string): Promise<boolean> {
function promptUserPermission(
toolName: string | undefined,
toolTitle?: string,
): Promise<boolean> {
if (!process.stdin.isTTY || !process.stderr.isTTY) {
console.error(`[permission denied] ${toolName ?? "unknown"}: non-interactive terminal`);
console.error(
`[permission denied] ${toolName ?? "unknown"}: non-interactive terminal`,
);
return Promise.resolve(false);
}
return new Promise((resolve) => {
@ -179,7 +204,9 @@ function promptUserPermission(toolName: string | undefined, toolTitle?: string):
: (toolName ?? "unknown tool");
rl.question(`\n[permission] Allow "${label}"? (y/N) `, (answer) => {
const approved = answer.trim().toLowerCase() === "y";
console.error(`[permission ${approved ? "approved" : "denied"}] ${toolName ?? "unknown"}`);
console.error(
`[permission ${approved ? "approved" : "denied"}] ${toolName ?? "unknown"}`,
);
finish(approved);
});
});
@ -317,17 +344,23 @@ function printSessionUpdate(notification: SessionNotification): void {
}
}
export async function createAcpClient(opts: AcpClientOptions = {}): Promise<AcpClientHandle> {
export async function createAcpClient(
opts: AcpClientOptions = {},
): Promise<AcpClientHandle> {
const cwd = opts.cwd ?? process.cwd();
const verbose = Boolean(opts.verbose);
const log = verbose ? (msg: string) => console.error(`[acp-client] ${msg}`) : () => {};
const log = verbose
? (msg: string) => console.error(`[acp-client] ${msg}`)
: () => {};
ensureOpenClawCliOnPath();
const serverArgs = buildServerArgs(opts);
const entryPath = resolveSelfEntryPath();
const serverCommand = opts.serverCommand ?? (entryPath ? process.execPath : "openclaw");
const effectiveArgs = opts.serverCommand || !entryPath ? serverArgs : [entryPath, ...serverArgs];
const serverCommand =
opts.serverCommand ?? (entryPath ? process.execPath : "openclaw");
const effectiveArgs =
opts.serverCommand || !entryPath ? serverArgs : [entryPath, ...serverArgs];
log(`spawning: ${serverCommand} ${effectiveArgs.join(" ")}`);
@ -379,7 +412,9 @@ export async function createAcpClient(opts: AcpClientOptions = {}): Promise<AcpC
};
}
export async function runAcpClientInteractive(opts: AcpClientOptions = {}): Promise<void> {
export async function runAcpClientInteractive(
opts: AcpClientOptions = {},
): Promise<void> {
const { client, agent, sessionId } = await createAcpClient(opts);
const rl = readline.createInterface({

View File

@ -49,7 +49,7 @@ const TOP_LEVEL_VERBS = new Set(["prompt", "exec", "sessions", "help"]);
function parseOutputFormat(value: string): OutputFormat {
if (!OUTPUT_FORMATS.includes(value as OutputFormat)) {
throw new InvalidArgumentError(
`Invalid format \"${value}\". Expected one of: ${OUTPUT_FORMATS.join(", ")}`,
`Invalid format "${value}". Expected one of: ${OUTPUT_FORMATS.join(", ")}`,
);
}
return value as OutputFormat;
@ -207,10 +207,7 @@ function resolveAgentInvocation(
};
}
function printSessionsByFormat(
sessions: SessionRecord[],
format: OutputFormat,
): void {
function printSessionsByFormat(sessions: SessionRecord[], format: OutputFormat): void {
if (format === "json") {
process.stdout.write(`${JSON.stringify(sessions)}\n`);
return;
@ -235,10 +232,7 @@ function printSessionsByFormat(
}
}
function printClosedSessionByFormat(
record: SessionRecord,
format: OutputFormat,
): void {
function printClosedSessionByFormat(record: SessionRecord, format: OutputFormat): void {
if (format === "json") {
process.stdout.write(
`${JSON.stringify({
@ -287,7 +281,7 @@ async function handlePrompt(
});
if (globalFlags.verbose) {
const scope = flags.session ? `named session \"${flags.session}\"` : "cwd session";
const scope = flags.session ? `named session "${flags.session}"` : "cwd session";
process.stderr.write(`[acpx] created ${scope}: ${record.id}\n`);
}
}
@ -361,13 +355,11 @@ async function handleSessionsClose(
if (!record) {
if (sessionName) {
throw new Error(
`No named session \"${sessionName}\" for cwd ${agent.cwd} and agent ${agent.agentName}`,
`No named session "${sessionName}" for cwd ${agent.cwd} and agent ${agent.agentName}`,
);
}
throw new Error(
`No cwd session for ${agent.cwd} and agent ${agent.agentName}`,
);
throw new Error(`No cwd session for ${agent.cwd} and agent ${agent.agentName}`);
}
const closed = await closeSession(record.id);
@ -559,30 +551,31 @@ async function main(): Promise<void> {
registerAgentCommand(program, scan.token);
}
program
.argument("[prompt...]", "Prompt text")
.action(async function (this: Command, promptParts: string[]) {
if (promptParts.length === 0 && process.stdin.isTTY) {
this.outputHelp();
return;
}
program.argument("[prompt...]", "Prompt text").action(async function (
this: Command,
promptParts: string[],
) {
if (promptParts.length === 0 && process.stdin.isTTY) {
this.outputHelp();
return;
}
await handlePrompt(undefined, promptParts, {}, this);
});
await handlePrompt(undefined, promptParts, {}, this);
});
program.addHelpText(
"after",
`
Examples:
acpx codex \"fix the tests\"
acpx codex prompt \"fix the tests\"
acpx codex exec \"what does this repo do\"
acpx codex -s backend \"fix the API\"
acpx codex "fix the tests"
acpx codex prompt "fix the tests"
acpx codex exec "what does this repo do"
acpx codex -s backend "fix the API"
acpx codex sessions
acpx codex sessions close backend
acpx claude \"refactor auth\"
acpx gemini \"add logging\"
acpx --agent ./my-custom-server \"do something\"`,
acpx claude "refactor auth"
acpx gemini "add logging"
acpx --agent ./my-custom-server "do something"`,
);
program.exitOverride((error) => {

View File

@ -22,11 +22,7 @@ import {
type WriteTextFileRequest,
type WriteTextFileResponse,
} from "@agentclientprotocol/sdk";
import {
spawn,
type ChildProcess,
type ChildProcessByStdio,
} from "node:child_process";
import { spawn, type ChildProcess, type ChildProcessByStdio } from "node:child_process";
import { randomUUID } from "node:crypto";
import fs from "node:fs/promises";
import path from "node:path";
@ -145,9 +141,7 @@ function asAbsoluteCwd(cwd: string): string {
return path.resolve(cwd);
}
function toEnvObject(
env: CreateTerminalRequest["env"],
): NodeJS.ProcessEnv | undefined {
function toEnvObject(env: CreateTerminalRequest["env"]): NodeJS.ProcessEnv | undefined {
if (!env || env.length === 0) {
return undefined;
}
@ -488,7 +482,8 @@ export class AcpClient {
throw new Error(`Unknown terminal: ${params.terminalId}`);
}
const hasExitStatus = terminal.exitCode !== undefined || terminal.signal !== undefined;
const hasExitStatus =
terminal.exitCode !== undefined || terminal.signal !== undefined;
return {
output: terminal.output.toString("utf8"),

View File

@ -77,9 +77,7 @@ function isAutoApprovedReadKind(kind: ToolKind | undefined): boolean {
return kind === "read" || kind === "search";
}
async function promptForPermission(
params: RequestPermissionRequest,
): Promise<boolean> {
async function promptForPermission(params: RequestPermissionRequest): Promise<boolean> {
if (!process.stdin.isTTY || !process.stderr.isTTY) {
return false;
}
@ -160,10 +158,7 @@ export function classifyPermissionDecision(
return "cancelled";
}
if (
selectedOption.kind === "allow_once" ||
selectedOption.kind === "allow_always"
) {
if (selectedOption.kind === "allow_once" || selectedOption.kind === "allow_always") {
return "approved";
}

View File

@ -66,10 +66,7 @@ async function ensureSessionDir(): Promise<void> {
await fs.mkdir(SESSION_BASE_DIR, { recursive: true });
}
async function withTimeout<T>(
promise: Promise<T>,
timeoutMs?: number,
): Promise<T> {
async function withTimeout<T>(promise: Promise<T>, timeoutMs?: number): Promise<T> {
if (!timeoutMs || timeoutMs <= 0) {
return promise;
}
@ -309,10 +306,7 @@ function isProcessAlive(pid: number | undefined): boolean {
}
}
async function waitForProcessExit(
pid: number,
timeoutMs: number,
): Promise<boolean> {
async function waitForProcessExit(pid: number, timeoutMs: number): Promise<boolean> {
const deadline = Date.now() + Math.max(0, timeoutMs);
while (Date.now() <= deadline) {
if (!isProcessAlive(pid)) {

View File

@ -18,11 +18,7 @@ export type ExitCode = (typeof EXIT_CODES)[keyof typeof EXIT_CODES];
export const OUTPUT_FORMATS = ["text", "json", "quiet"] as const;
export type OutputFormat = (typeof OUTPUT_FORMATS)[number];
export const PERMISSION_MODES = [
"approve-all",
"approve-reads",
"deny-all",
] as const;
export const PERMISSION_MODES = ["approve-all", "approve-reads", "deny-all"] as const;
export type PermissionMode = (typeof PERMISSION_MODES)[number];
export type PermissionStats = {