zcode-acp-server 0.18.0 → 0.18.2

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/docs/CLI.md ADDED
@@ -0,0 +1,188 @@
1
+ # Unified CLI (`zcode-acp`)
2
+
3
+ Every surface of this package is available under one command — `zcode-acp` —
4
+ installed alongside the `zcode-acp-server` bin your editor configures.
5
+
6
+ ## Interactive REPL
7
+
8
+ Bare `zcode-acp` opens an interactive terminal chat against this same bridge
9
+ (built with [Ink](https://github.com/vadimdemedes/ink), the same renderer
10
+ Claude Code and Gemini CLI use). Completed messages print once into the
11
+ terminal's **native scrollback** — smooth wheel scrolling, text selection,
12
+ search, and history that survives exit are all just your terminal, unchanged.
13
+ Only a compact dynamic footer ever repaints: the live-turn tail (capped at
14
+ half the screen), queued-prompt panel, completion menu, and the prompt box.
15
+
16
+ ```bash
17
+ zcode-acp # chat in this directory
18
+ ```
19
+
20
+ A startup welcome panel (version, session directory, seeded config, key
21
+ hints) lands in scrollback first. Streaming output has code-fence coloring,
22
+ dim thinking lines, and live tool rows. The prompt line wraps across rows
23
+ with a CJK-aware block cursor — `←`/`→` (or Ctrl-B/Ctrl-F) move inside the
24
+ typed text, Backspace/Delete edit at the caret, Ctrl-A/Ctrl-E jump to the
25
+ line's ends, Ctrl-U clears the line; completion keeps precedence while its
26
+ menu is open. The status row carries a compact plan-quota readout
27
+ (`5h 16% · wk 4%`) refreshed every 10 minutes — `/quota` prints the full
28
+ card. Pasted or dragged-in content (error logs, file drops) is sanitized and
29
+ size-capped before it reaches the editor, so long pastes batch cleanly.
30
+ `esc` interrupts a running turn (immediately — the bridge resolves the prompt
31
+ as cancelled without waiting for the backend); `Ctrl-C` also interrupts, and
32
+ while idle press it twice to quit. `/exit` (or a bare `exit`, shell-style)
33
+ leaves; the session itself persists
34
+ in the ZCode backend and is available to your editor.
35
+
36
+ Sending a follow-up right after an interrupt waits for the backend to finish
37
+ the cancelled generation first — a `[上一个回复仍在生成,等待结束后发送…]`
38
+ note explains the pause (the Aug-28 app-server accepts mid-generation sends
39
+ as steer input but drops them when the old turn ends; the bridge polls until
40
+ the session is idle, up to 90s, so the message actually runs).
41
+
42
+ Messages typed while a turn is running (or the session is still starting) are
43
+ queued, not lost: each shows up in the transcript immediately and a `⏸ queued`
44
+ panel above the prompt box lists everything waiting to run. When the current
45
+ turn ends the queue drains one prompt at a time — through the same command
46
+ parsing as direct submits, so queued `/help` and `/exit` still work.
47
+
48
+ `/sessions` lists this project's previous conversations in an 8-row sliding
49
+ window (position counter, plus "N newer above / N older below" hints); arrow
50
+ keys move over the full list even though only part of it renders. Resuming
51
+ loads the most recent tail of the conversation (last 50 messages, turn-aligned)
52
+ instead of dumping thousands of lines at once — when older history exists the
53
+ note says exactly what was loaded:
54
+ `resumed "<title>" — showing last 50 of 1234 messages`.
55
+
56
+ The full-width prompt box mirrors the editor's dropdowns: its bottom row shows
57
+ the current `model · mode · thought level`, and typing `/` opens an interactive
58
+ completion menu — `↑`/`↓` move, `enter` picks the highlighted entry (or `tab` /
59
+ `→`; typing the exact form already sends), `esc` dismisses. After picking
60
+ `/model`, `/mode`, or `/thought` the same menu lists the config options (the
61
+ current one marked `●`) and **enter on a row switches immediately** — no second
62
+ confirmation. Argument-free commands (`/exit`, `/help`, `/sessions`, `/new`,
63
+ `/compact`, `/mcp`, `/quota`) run on pick as well; every other completion
64
+ (skills, plugins) only fills the line, since those usually expect arguments.
65
+ The arg-less forms still print a static listing over the same slash-command
66
+ path the editor uses. `/help` lists every command the bridge advertises,
67
+ including plugin commands. `/new` swaps in a fresh session without leaving
68
+ the terminal (the old conversation stays in `/sessions` and in scrollback).
69
+
70
+ Submitted prompts are history: `↑`/`↓` (with the completion menu closed)
71
+ recall them per project across restarts — the first `↑` stashes the draft
72
+ and `↓` past the newest entry restores it. Pasted text folds to a single
73
+ line (newlines and tabs become spaces), so a multi-paragraph paste lands in
74
+ the box as one prompt instead of firing line-by-line. While a reply streams,
75
+ the footer shows a live status row — `⠋ working… (12s · esc to interrupt)` —
76
+ so stretches with no streamed output (long tool calls) still visibly tick.
77
+
78
+ Unexpected internal errors never take the REPL down silently: they print to
79
+ stderr and surface as an `-- error absorbed: …` note in the transcript while
80
+ everything keeps running. Only repeated failures within ten seconds trip a
81
+ breaker that shuts the UI down cleanly.
82
+
83
+ While remote access is enabled, turns started from other clients (the mobile
84
+ app, a second editor) render live in the REPL too, and questions or permission
85
+ requests answered elsewhere dismiss the local picker automatically.
86
+
87
+ Without a TTY (pipes, Windows editor shims — where the bin name is lost from
88
+ `argv`), bare `zcode-acp` falls back to the stdio server, so editor configs
89
+ pointing at either bin name keep working. Ask for the REPL explicitly with
90
+ `zcode-acp repl`; without a TTY that errors instead of falling back.
91
+
92
+ ## Quota cards
93
+
94
+ Check plan usage from the terminal — no editor or running server needed. By
95
+ default it shows both **GLM Coding Plan** and **Opencode Go** in one card;
96
+ pass a provider to focus on one.
97
+
98
+ GLM credentials are read from `~/.zcode/v2/config.json`. Opencode Go
99
+ credentials come from environment variables (the dashboard needs a browser
100
+ cookie — see [Opencode Go setup](#opencode-go-setup) below).
101
+
102
+ ```bash
103
+ # Both providers (default): GLM + Opencode Go in one card
104
+ zcode-acp quota
105
+
106
+ # Focus on one provider
107
+ zcode-acp quota glm # GLM Coding Plan only
108
+ zcode-acp quota go # Opencode Go only (rolling + weekly + monthly)
109
+
110
+ # Live monitor: clear the screen and refresh every 30s (default)
111
+ zcode-acp quota -w
112
+ zcode-acp quota go -w # watch Opencode Go only
113
+
114
+ # Refresh at a custom interval (seconds; minimum 10)
115
+ zcode-acp quota --watch --interval 60
116
+
117
+ # Plain monochrome bars (color is the default on a terminal)
118
+ zcode-acp quota --plain
119
+ ```
120
+
121
+ By default the CLI renders heat-colored (green→yellow→red) progress bars with
122
+ the usage numbers overlaid inside the bar, so each line stays short. Pass
123
+ `--plain` (or `-p`) for the classic monochrome `█`/`░` layout. Color is also
124
+ disabled automatically when stdout is piped or redirected, so captured output
125
+ stays clean.
126
+
127
+ The watch mode clears and redraws the card in place, like `top`/`htop`. Press
128
+ `Ctrl-C` to exit. The 10s minimum exists because the quota API is cached for
129
+ 10s internally — a shorter interval would just keep returning the stale cached
130
+ value.
131
+
132
+ When the package isn't globally installed, run the built file directly:
133
+
134
+ ```bash
135
+ node dist/cli.js quota -w
136
+ ```
137
+
138
+ ## Opencode Go setup
139
+
140
+ Opencode Go has no JSON API for subscription usage — the CLI scrapes the
141
+ authenticated dashboard at `opencode.ai/workspace/<id>/go`, so it needs your
142
+ browser `auth` cookie. Credentials are read from two sources, **merged
143
+ field-by-field with environment variables taking precedence** over the config
144
+ file:
145
+
146
+ - **Config file**: `~/.pi/agent/opencode-go.json` — same convention as the
147
+ `@beyona/pi-zai-usage` Pi extension, so if you already configured it there
148
+ you're done.
149
+ ```json
150
+ { "workspaceId": "wrk_your_workspace_id", "authCookie": "Fe26.2**your_cookie_value" }
151
+ ```
152
+ - **Environment variables** (override the matching file field):
153
+ ```bash
154
+ export OPENCODE_GO_WORKSPACE_ID="wrk_your_workspace_id"
155
+ export OPENCODE_GO_AUTH_COOKIE="Fe26.2**your_cookie_value"
156
+ ```
157
+
158
+ How to get the values:
159
+
160
+ 1. **Workspace ID** — open `https://opencode.ai`, navigate to your Go
161
+ workspace, and copy the `wrk_…` id from the URL
162
+ (`https://opencode.ai/workspace/<wrk_…>/go`).
163
+ 2. **Auth cookie** — open browser DevTools (F12) → Application → Cookies →
164
+ `opencode.ai` → copy the value of the cookie named `auth` (it starts with
165
+ `Fe26.2**`).
166
+
167
+ Without credentials, the default dual-provider mode silently shows GLM only
168
+ (no error). Running `zcode-acp quota go` without credentials prints a setup hint.
169
+
170
+ ## Hub and server subcommands
171
+
172
+ `zcode-acp hub` runs the remote-access hub daemon manually (normally
173
+ auto-spawned by bridges — see [Remote Access](REMOTE.md)). `zcode-acp
174
+ server` speaks ACP on stdio — that is what editors invoke through the
175
+ `zcode-acp-server` bin; you rarely need it by hand.
176
+
177
+ ## Upgrading from 0.11
178
+
179
+ 0.12.0 folds the old standalone bins into the unified CLI (see
180
+ [ADR-0007](adr/0007-unified-cli-entry-and-bin-pruning.md)):
181
+
182
+ | Old (≤0.11) | New (0.12) |
183
+ | -------------------- | ------------------------------------- |
184
+ | `zcode-acp-server` | unchanged (kept for editor configs) |
185
+ | `zcode-quota [args]` | `zcode-acp quota [args]` (same flags) |
186
+ | `zcode-acp-hub` | `zcode-acp hub` |
187
+
188
+ Editor configs referencing `zcode-acp-server` keep working unchanged.
package/docs/REMOTE.md ADDED
@@ -0,0 +1,115 @@
1
+ # Remote Access
2
+
3
+ With `ZCODE_ACP_REMOTE=1` the bridge additionally accepts ACP connections over
4
+ WebSocket, so a phone or browser can watch and drive the **same sessions** as
5
+ your editor. Zed (or any ACP editor over stdio) remains the primary client and
6
+ owns the process: when the editor disconnects, the bridge — and every remote
7
+ attachment — exits with it.
8
+
9
+ A ready-made client — Android APK plus a self-hostable web build — lives at
10
+ [william0wang/zcode-acp-remote](https://github.com/william0wang/zcode-acp-remote).
11
+
12
+ ```text
13
+ phone / browser ──WS── tunnel ── hub (127.0.0.1:8377, single entry)
14
+ │ byte-level proxy
15
+
16
+ bridge ACP endpoint (127.0.0.1:8378+n)
17
+ │ same AgentApp as stdio
18
+ Zed ──────── stdio ────────────────┘
19
+ ```
20
+
21
+ Enable it per-agent in Zed's settings (Zed merges these into the agent's
22
+ environment):
23
+
24
+ ```json
25
+ "agents": {
26
+ "ZCode": {
27
+ "command": "zcode-acp-server",
28
+ "env": {
29
+ "ZCODE_ACP_REMOTE": "1",
30
+ "ZCODE_ACP_REMOTE_TOKEN": "<a-long-random-secret>"
31
+ }
32
+ }
33
+ }
34
+ ```
35
+
36
+ **Hub.** The first bridge with remote enabled spawns the hub daemon as a
37
+ detached, machine-level singleton on `ZCODE_ACP_HUB_PORT` (it can also be run
38
+ manually). It does three things only: token auth, instance discovery, and
39
+ byte-level proxying (ACP WebSocket plus read-only session files) — no session
40
+ state, no path semantics. It exits after ~10 idle minutes and is re-spawned
41
+ on demand. Each bridge registers every 10s as a heartbeat and drops out of
42
+ discovery ~30s after it stops.
43
+
44
+ **Discovery API** (for client authors; fields are additive-only):
45
+
46
+ ```text
47
+ GET /api/instances → [{"id","port","pid","startedAt","workspace",
48
+ "origin","sessions":[{"sessionId","title?","updatedAt"}]}]
49
+ GET /api/instances?probe=1 → same list, but unreachable bridges are pruned first
50
+ WS /acp?instance=<id> → proxied to that bridge's endpoint
51
+ GET /api/instances/{id}/fs/… → read-only session files (list + raw bytes, ADR-0004)
52
+ ```
53
+
54
+ `origin` labels how an instance was started: `"editor"` (a bridge an editor
55
+ spawned over stdio) or `"serve"` (a headless bridge created remotely, see
56
+ below).
57
+
58
+ **Remote session-create** (ADR-0014). A remote client can start a NEW agent
59
+ session in any of the machine's known projects — no editor required:
60
+
61
+ ```text
62
+ GET /api/projects → [{"workspacePath","sessions","lastActive"}]
63
+ POST /api/instances {workspacePath} → {"id","reused"}
64
+ ```
65
+
66
+ `/api/projects` aggregates the App's tasks index: every workspace that ever
67
+ ran a session (system temp trees, `~/.zcode` itself, and vanished
68
+ directories filtered out), newest activity first. The list gates the POST —
69
+ paths outside it get 403 (a convenience bound, not a security boundary: a
70
+ token holder can already drive an editor-bridge session in any cwd; the
71
+ trust boundary is the token). On create the hub
72
+ spawns `zcode-acp serve` — a headless bridge — in the project's cwd; it
73
+ registers back within seconds and is reachable like any other instance. A
74
+ live serve instance for the same workspace is reused instead of re-spawned
75
+ (`reused:true`). The serve bridge lives for remote interest only: it exits
76
+ ~10 minutes after the last client detaches and the last turn finishes, and
77
+ its `session/new` always uses the project cwd regardless of what a client
78
+ sends.
79
+
80
+ `sessions` lists the project's **currently running** conversations (live
81
+ editor tabs and remote attachments) under the same ACP session ids the
82
+ editor uses — attaching by id joins the conversation's live notification
83
+ stream, and the hub dedupes sessions shared by several bridges of the same
84
+ project.
85
+
86
+ Auth is `Authorization: Bearer <token>` or `?token=` (browsers cannot set WS
87
+ headers); `/api/*` sends `Access-Control-Allow-Origin: *` — the token is the
88
+ security boundary. A proxied connection stays bound to one instance; switching
89
+ instances means reconnecting. Remote clients can also pull plan quota via the
90
+ non-standard `account/usage_stats` ACP method (no session required), and
91
+ browse/download the files of a session's project through the `/fs` routes
92
+ above. During replay, compaction summaries and rewritten tool calls arrive as
93
+ collapsed `tool_call` updates instead of walls of text
94
+ ([REPLAY-GUIDE.md](REPLAY-GUIDE.md)).
95
+
96
+ Building a remote client — web, mobile, or CLI? The full integration contract
97
+ (endpoints, framing, lifecycle timings, failure recovery, platform notes)
98
+ lives in [REMOTE-CLIENTS.md](REMOTE-CLIENTS.md).
99
+
100
+ **Semantics.** All agent notifications are broadcast to every client.
101
+ Permission / elicitation requests go to every client and the **first answer
102
+ wins**; losing clients receive `$/cancel_request` so their dialogs close.
103
+ Concurrent prompts for one session are serialized exactly as they are for a
104
+ single editor. Capabilities declared by any client are OR-merged.
105
+
106
+ **Tunnels.** Designed for one-port tunnels (Cloudflare Tunnel, frp): map the
107
+ hub port only. frp's `tcp` mode passes WebSocket as-is; Cloudflare Tunnel
108
+ drops idle WebSocket connections, so the hub sends 30s keepalive pings on both
109
+ legs. The bridge endpoint itself is loopback-only and never exposed.
110
+
111
+ **Binding beyond loopback.** The hub speaks plain HTTP/WS — the token travels
112
+ and authorizes in cleartext, so `ZCODE_ACP_HUB_HOST=0.0.0.0` (needed only when
113
+ the tunnel agent runs in its own container) is exactly as safe as the network
114
+ it lands on. Keep the bind loopback unless that interface is private to the
115
+ tunnel agent, and put TLS in front before mapping it anywhere untrusted.
@@ -0,0 +1,56 @@
1
+ # Sandbox
2
+
3
+ Optional macOS Seatbelt confinement for the file writes the agent performs.
4
+ This page is the full manual; the README keeps only a summary.
5
+
6
+ Two switches arm the sandbox (macOS only), whichever comes first:
7
+
8
+ - globally: `ZCODE_ACP_SANDBOX=1`, or
9
+ - per project: `"enabled": true` in `<workspace>/.zcode/acp/sandbox.json`.
10
+ The bridge auto-creates that file with `"enabled": false` the first time
11
+ you open the workspace — flip the flag to opt this project in, no global
12
+ env needed. Flipping it mid-run takes effect on the next prompt (the
13
+ backend restarts sandboxed); flipping back takes effect the next time the
14
+ backend restarts on its own.
15
+
16
+ Once armed, the zcode backend subprocess — and every Bash/Edit/Write it
17
+ performs, including all child processes — runs wrapped in a
18
+ Seatbelt (`sandbox-exec`) profile that denies file writes everywhere except:
19
+
20
+ - the workspace root(s) of your live sessions,
21
+ - `~/.zcode*` (the backend's own sessions/db/logs),
22
+ - the well-known system temp trees — `/tmp` (→ `/private/tmp`), `/var/tmp`,
23
+ and the per-user `/var/folders` tree that `$TMPDIR` lives in — plus
24
+ regenerable tool caches (`~/Library/Caches`, `~/.cache`, `~/.npm`,
25
+ `~/Library/pnpm`, `~/.node-gyp`),
26
+ - paths granted via the per-project config or the allow popup.
27
+
28
+ Reads and process execution stay open: deletion (`rm`, `mv`, truncation) is a
29
+ write-class syscall, so the write denial stops it regardless of which binary
30
+ performs it — including `/bin/rm`, `python shutil.rmtree`, or shell
31
+ redirections.
32
+
33
+ When a write outside the whitelist is attempted, the tool fails with
34
+ `Operation not permitted` and the bridge asks via the editor's permission
35
+ popup with four choices: allow **once**, allow **always**, reject **once**,
36
+ reject **always**. The "always" choices are persisted by the bridge into
37
+ `<workspace>/.zcode/acp/sandbox.json` (created on first run — allows to the
38
+ `allow` list, rejections to a `deny` list that suppresses future asks; edit
39
+ the file to undo either). "Once" choices and dismissed popups persist
40
+ nothing and will ask again. The agent itself cannot edit
41
+ that file — the sandbox denies
42
+ writes to `.zcode/acp/` inside the workspace while the bridge (outside the
43
+ sandbox) writes it on your behalf. After an allow, the backend restarts with
44
+ the widened profile (a few seconds; the bridge auto-continues the interrupted
45
+ task). Set `"strictGit": true` in the config to also put `.git` behind the
46
+ popup.
47
+
48
+ Ordinary filesystem permission failures (`Permission denied`, EACCES — a
49
+ chmod/ownership problem no popup can fix) are not sandbox denials; the bridge
50
+ surfaces them as a one-time hint instead of raising an ask.
51
+
52
+ This targets accident prevention, not malice: indirect escapes (an
53
+ agent-edited `.bashrc`, build scripts, or git hooks that you later run
54
+ yourself outside the sandbox) are out of scope — treat its output like any
55
+ other code review. Verify a profile manually with
56
+ `bash scripts/verify-sandbox.sh` (macOS, after `pnpm build`).
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "zcode-acp-server",
3
- "version": "0.18.0",
3
+ "version": "0.18.2",
4
4
  "description": "Agent Client Protocol (ACP) server bridging headless ZCode to editors like Zed and JetBrains.",
5
5
  "type": "module",
6
6
  "license": "Apache-2.0",