Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 9 additions & 3 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,11 +58,17 @@ src/
│ ├── config.ts ENV parsing (gate, mandatory token, hub/bridge ports)
│ ├── endpoint.ts Loopback ACP endpoint + hub registration heartbeat
│ ├── file-endpoint.ts Read-only /fs/list + /fs/file, session-root scoped (ADR-0004)
│ └── hub-server.ts zcode-acp-hub: auth, discovery, byte-level proxy (ACP WS + /fs files), ?probe=1 liveness
│ └── hub-server.ts Hub daemon: auth, discovery, byte-level proxy (ACP WS + /fs files), ?probe=1 liveness
├── quota/ GLM Coding Plan usage API client (/quota command)
├── cli.ts Unified CLI entry (`zcode-acp`): subcommand dispatch
│ (bare invocation → REPL) (ADR-0007)
├── repl/ Interactive REPL (bare `zcode-acp`): Ink UI + ACP client
│ ├── model.ts Pure turn state machine (SessionUpdate → entries)
│ ├── App.tsx Ink components (stream, tools, permission picker)
│ └── run.ts Orchestration: spawn bridge, pump updates
└── bin/
├── hub.ts Standalone zcode-acp-hub daemon entry
└── quota.ts Standalone zcode-quota CLI
├── hub.ts Hub daemon entry (`zcode-acp hub`; spawned by absolute path)
└── quota.ts Quota cards entry (`zcode-acp quota`)
```

**Key boundary**: `backend/` talks to the ZCode subprocess. `handlers/` talks to
Expand Down
14 changes: 12 additions & 2 deletions CONTEXT.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,9 @@ session registry, and the turn loops. One editor connection = one bridge.
_Avoid_: server (ambiguous with the ACP agent role), hub

**Primary Client**:
The editor connection over stdio that spawned the bridge and owns its
lifetime (Zed, JetBrains). When it disconnects, the bridge exits.
The connection over stdio that spawned the bridge and owns its lifetime — an
editor (Zed, JetBrains) or the Unified CLI's REPL in a terminal.
When it disconnects, the bridge exits.
_Avoid_: host, master client

**Remote Client**:
Expand All @@ -39,6 +40,15 @@ remote access. It does token auth, instance discovery, and byte-level
WebSocket proxying — it holds no session state and understands no ACP.
_Avoid_: gateway, broker

**Unified CLI**:
The `zcode-acp` command — the single human-facing command-line entry point.
Bare invocation opens the **REPL** (interactive agent chat in a terminal);
every other surface is a subcommand: `quota` (plan usage cards), `hub`
(running the Hub daemon), and `server` (the editor-facing bridge entry,
identical to the legacy `zcode-acp-server` bin kept for existing editor
configs).
_Avoid_: launcher, wrapper

**Instance**:
One registered bridge as seen through the Hub. A remote connection binds to
exactly one instance; instance switching means reconnecting.
Expand Down
76 changes: 60 additions & 16 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@ automatically. Point `ZCODE_BIN` at the bundled `zcode.cjs`:
| `ZCODE_ACP_DEBUG` | _(unset)_ | Set to `1` to enable verbose diagnostic logs (event flow, probe loops, status updates). Default is quiet — only warnings (backend pipe errors, command/permission failures, lock timeouts) are emitted. Enable this when diagnosing bridge issues; the logs appear in `Zed.log` prefixed with `[zcode-acp]`. |
| `ZCODE_ACP_REMOTE` | _(unset)_ | Set to `1` to enable [remote access](#remote-access) — serve the same sessions to additional ACP clients over WebSocket. |
| `ZCODE_ACP_REMOTE_TOKEN` | _(unset)_ | Auth token for remote access. **Mandatory** when `ZCODE_ACP_REMOTE=1`; remote stays disabled without it. |
| `ZCODE_ACP_HUB_PORT` | `8377` | Port of the machine-level `zcode-acp-hub`. Map exactly this one port in your tunnel. |
| `ZCODE_ACP_HUB_PORT` | `8377` | Port of the machine-level hub daemon. Map exactly this one port in your tunnel. |
| `ZCODE_ACP_HUB_HOST` | `127.0.0.1` | Hub bind address. `0.0.0.0` exposes a token-only, unencrypted surface — only for a containerized tunnel agent on a private interface (see [Remote Access](#remote-access)). |
| `ZCODE_ACP_REMOTE_PORT` | `8378` | First loopback port for the bridge's ACP endpoint. Each bridge (each editor window) auto-increments to the next free port. |

Expand Down Expand Up @@ -130,7 +130,7 @@ environment):
}
```

**Hub.** The first bridge with remote enabled spawns `zcode-acp-hub` as a
**Hub.** The first bridge with remote enabled spawns the hub daemon as a
detached, machine-level singleton on `ZCODE_ACP_HUB_PORT` (it can also be run
manually). It does three things only: token auth, instance discovery, and
byte-level proxying (ACP WebSocket plus read-only session files) — no session
Expand Down Expand Up @@ -185,10 +185,34 @@ the tunnel agent runs in its own container) is exactly as safe as the network
it lands on. Keep the bind loopback unless that interface is private to the
tunnel agent, and put TLS in front before mapping it anywhere untrusted.

## Standalone Quota CLI
## Unified CLI (`zcode-acp`)

Besides the ACP server, the package ships a `zcode-quota` bin that queries
your usage **from the terminal** — no editor or running server needed. By
Every surface of this package is available under one command — `zcode-acp` —
installed alongside the `zcode-acp-server` bin your editor configures.

### Interactive REPL

Bare `zcode-acp` opens an interactive terminal chat against this same bridge
(built with [Ink](https://github.com/vadimdemedes/ink), the same renderer
Claude Code and Gemini CLI use):

```bash
zcode-acp # chat in this directory
```

Streaming output with code-fence coloring, dim thinking lines, live tool rows,
and arrow-key permission prompts. `Ctrl-C` cancels a running turn; while idle,
press it twice to quit. `/exit` leaves; the session itself persists in the
ZCode backend and is available to your editor.

Without a TTY (pipes, Windows editor shims — where the bin name is lost from
`argv`), bare `zcode-acp` falls back to the stdio server, so editor configs
pointing at either bin name keep working. Ask for the REPL explicitly with
`zcode-acp repl`; without a TTY that errors instead of falling back.

### Quota cards

Check plan usage from the terminal — no editor or running server needed. By
default it shows both **GLM Coding Plan** and **Opencode Go** in one card;
pass a provider to focus on one.

Expand All @@ -198,21 +222,21 @@ cookie — see [Opencode Go setup](#opencode-go-setup) below).

```bash
# Both providers (default): GLM + Opencode Go in one card
zcode-quota
zcode-acp quota

# Focus on one provider
zcode-quota glm # GLM Coding Plan only
zcode-quota go # Opencode Go only (rolling + weekly + monthly)
zcode-acp quota glm # GLM Coding Plan only
zcode-acp quota go # Opencode Go only (rolling + weekly + monthly)

# Live monitor: clear the screen and refresh every 30s (default)
zcode-quota -w
zcode-quota go -w # watch Opencode Go only
zcode-acp quota -w
zcode-acp quota go -w # watch Opencode Go only

# Refresh at a custom interval (seconds; minimum 10)
zcode-quota --watch --interval 60
zcode-acp quota --watch --interval 60

# Plain monochrome bars (color is the default on a terminal)
zcode-quota --plain
zcode-acp quota --plain
```

By default the CLI renders heat-colored (green→yellow→red) progress bars with
Expand All @@ -229,7 +253,7 @@ value.
When the package isn't globally installed, run the built file directly:

```bash
node dist/bin/quota.js -w
node dist/cli.js quota -w
```

### Opencode Go setup
Expand Down Expand Up @@ -262,7 +286,27 @@ How to get the values:
`Fe26.2**`).

Without credentials, the default dual-provider mode silently shows GLM only
(no error). Running `zcode-quota go` without credentials prints a setup hint.
(no error). Running `zcode-acp quota go` without credentials prints a setup hint.

### Hub and server subcommands

`zcode-acp hub` runs the remote-access hub daemon manually (normally
auto-spawned by bridges — see [Remote Access](#remote-access)). `zcode-acp
server` speaks ACP on stdio — that is what editors invoke through the
`zcode-acp-server` bin; you rarely need it by hand.

### Upgrading from 0.11

0.12.0 folds the old standalone bins into the unified CLI (see
[ADR-0007](docs/adr/0007-unified-cli-entry-and-bin-pruning.md)):

| Old (≤0.11) | New (0.12) |
| --------------------- | ----------------------- |
| `zcode-acp-server` | unchanged (kept for editor configs) |
| `zcode-quota [args]` | `zcode-acp quota [args]` (same flags) |
| `zcode-acp-hub` | `zcode-acp hub` |

Editor configs referencing `zcode-acp-server` keep working unchanged.

## ACP Registry

Expand Down Expand Up @@ -298,8 +342,8 @@ The server is organised in layers that mirror the ACP protocol:
- `interaction/` — bridge ZCode `interaction/*` server requests to ACP, preferring `elicitation/create` and falling back to `session/request_permission` (tool auth, ExitPlanMode, AskUserQuestion)
- `handlers/` — ACP method handlers (`session/new`, `session/prompt`, ...) and the turn engine
- `config/` — model / mode / thought-level configOptions and runtime model switching
- `remote/` — opt-in remote access: loopback ACP endpoint, multi-client broadcast, `zcode-acp-hub` registration
- `quota/` — GLM Coding Plan / Opencode Go usage API client (`/quota` command, `zcode-quota` bin)
- `remote/` — opt-in remote access: loopback ACP endpoint, multi-client broadcast, hub registration
- `quota/` — GLM Coding Plan / Opencode Go usage API client (`/quota` command, `zcode-acp quota` subcommand)
- `server.ts` — shared state and handler registration
- `index.ts` — stdio wiring via the ACP SDK

Expand Down
2 changes: 1 addition & 1 deletion docs/ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -211,7 +211,7 @@ When enabled, the same `AgentApp` serves the stdio editor and a loopback
WebSocket endpoint. Every connection (editor or remote) joins the broadcast
registry via `trackConnections`, so one turn's notifications reach all clients
regardless of who prompted. The bridge registers itself with the machine-level
`zcode-acp-hub` (`bin/hub.ts`), which is the only public entry point and holds
the hub daemon (`zcode-acp hub`, `bin/hub.ts`), which is the only public entry point and holds
no session state (see `docs/adr/0002`). The bridge's lifetime still follows the
stdio client (ADR-0001); the listener is `unref()`'d so remote clients alone
never keep the process alive.
Expand Down
4 changes: 2 additions & 2 deletions docs/REMOTE-CLIENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

How to attach any out-of-editor client — browser SPA, mobile app, CLI, desktop
tool — to bridge sessions over the network. This document IS the contract:
everything here is implemented by `zcode-acp-hub` and the bridge's remote
everything here is implemented by the hub daemon (`zcode-acp hub`) and the bridge's remote
endpoint; anything not written here is not part of the contract.

ACP method semantics are defined by the [ACP spec](https://agentclientprotocol.com);
Expand Down Expand Up @@ -182,7 +182,7 @@ pull-only request — callable any time after `initialize`, no session required.
Fetch once after attach and on demand; quota changes are slow, there is no
push.

Both channels return the same payload, mirroring the `zcode-quota` CLI card's
Both channels return the same payload, mirroring the `zcode-acp quota` CLI card's
data model — one GLM section plus one Opencode Go section — so clients can
reproduce the CLI layout exactly:

Expand Down
2 changes: 1 addition & 1 deletion docs/TROUBLESHOOTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -341,7 +341,7 @@ http://127.0.0.1:<hub-port>/api/health` fails, or `/api/*` returns 401.
1. 401 means a token mismatch — `ZCODE_ACP_REMOTE_TOKEN` must be identical in
the bridge env, the hub env (if run manually), and the client request.
2. A dead hub self-heals: the next bridge heartbeat (≤10s; worst ~1min under
the spawn throttle) re-spawns `zcode-acp-hub`. Retry with backoff rather
the spawn throttle) re-spawns the hub daemon. Retry with backoff rather
than restarting anything by hand.
3. Confirm the ports match: the client must reach `ZCODE_ACP_HUB_PORT`
(default 8377) through the tunnel, and the tunnel maps exactly that one
Expand Down
40 changes: 40 additions & 0 deletions docs/adr/0007-unified-cli-entry-and-bin-pruning.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
# Unified CLI entry and bin pruning

The package grew three separate bins (`zcode-acp-server`, `zcode-acp-hub`,
`zcode-quota`) plus a planned terminal chat surface. Installing four sibling
commands for one tool is noisy, and each new surface would add another. We
decided to ship one human-facing entry point — the Unified CLI, `zcode-acp` —
with every surface as a subcommand: `quota`, `hub`, and `server`. Bare
`zcode-acp` opens the interactive REPL: an Ink-rendered chat UI (the renderer
Claude Code and Gemini CLI use) speaking ACP to a spawned bridge over stdio,
with the update pump, permission picker, and turn state machine owned by this
package. The REPL is written in-house rather than borrowed — no embeddable
interactive ACP client library exists (acpx is headless-only; the interactive
clients Toad/Hydra/Nori are external tools or heavyweight daemons), and we
control both protocol ends already.

Bin declarations are pruned to `zcode-acp` and `zcode-acp-server`. The server
bin stays because it is the command name hardcoded in existing editor configs
(Zed, JetBrains) across ~2.5k monthly downloads — removing it would break every
current user at once; it is an editor contract, not a human alias. The
`zcode-acp-hub` and `zcode-quota` bins are removed in 0.12.0 with a README
migration note (`zcode-acp hub` / `zcode-acp quota`); the hub's own binary file
stays in `dist/bin/hub.js` because bridges spawn it by absolute path, not by
command name, so the remote feature is unaffected.

Bin-name detection is `basename(argv[1])`, which works through Unix symlinks
but not through Windows `.cmd` shims (they spawn `node ...\dist\cli.js`, so
the bin name never reaches argv). Bare invocation without a TTY therefore
falls back to the stdio server — the only sensible reading of a piped, no-TTY
launch, and exactly what a Windows editor spawn needs. The explicit
`zcode-acp repl` subcommand keeps a hard TTY check and errors instead.

We rejected keeping all old bins as silent aliases (five installed commands;
the redundancy was the original complaint), removing every old bin including
`zcode-acp-server` (breaks all existing editor configs at once; revisit at
1.0), and bundling [acpx](https://github.com/openclaw/acpx) as a `chat`
passthrough subcommand (evaluated first: its engine is headless-only, so it
cannot host the interactive REPL that is the actual requirement, and keeping
it beside the in-house REPL would mean two terminal surfaces, a pre-1.0
dependency, and an extra Node engine bump for no remaining use — dropped
before release).
12 changes: 8 additions & 4 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "zcode-acp-server",
"version": "0.11.9",
"version": "0.12.0",
"description": "Agent Client Protocol (ACP) server bridging headless ZCode to editors like Zed and JetBrains.",
"type": "module",
"license": "Apache-2.0",
Expand All @@ -23,9 +23,8 @@
"zcode"
],
"bin": {
"zcode-acp-server": "dist/index.js",
"zcode-acp-hub": "dist/bin/hub.js",
"zcode-quota": "dist/bin/quota.js"
"zcode-acp": "dist/cli.js",
"zcode-acp-server": "dist/cli.js"
},
"main": "dist/index.js",
"types": "dist/index.d.ts",
Expand All @@ -48,6 +47,10 @@
},
"dependencies": {
"@agentclientprotocol/sdk": "^1.3.0",
"chalk": "^6.0.0",
"ink": "^7.1.1",
"ink-spinner": "^5.0.0",
"react": "^19.2.8",
"ws": "^8.21.3"
},
"peerDependencies": {
Expand All @@ -56,6 +59,7 @@
"devDependencies": {
"@eslint/js": "^9.0.0",
"@types/node": "^22.0.0",
"@types/react": "^19.2.18",
"@types/ws": "^8.18.1",
"@typescript-eslint/eslint-plugin": "^8.0.0",
"@typescript-eslint/parser": "^8.0.0",
Expand Down
Loading
Loading