@yitom/agy-acp-map 0.1.3 → 0.1.6

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
@@ -1,302 +1,371 @@
1
- # agy-acp-map — ACP v2 ↔ agy stream-json bridge (v0.1.2 · Bun + TypeScript)
2
-
3
- **English** | **中文**
4
-
5
- ## What is NDJSON / stream-json?
6
-
7
- NDJSON (Newline-Delimited JSON), also called JSONL, is a framing where each line is one complete JSON value. Both ACP stdio JSON-RPC and `agy --output-format stream-json` use this: the agent writes one JSON-RPC object per line on stdout; agy emits one event object per line (`init`, `step_update`, `result`). No length prefixes, no SQLite — just lines.
8
-
9
- NDJSON(换行分隔 JSON / JSONL)是一种按行分帧的格式:每一行是一个完整的 JSON 值。ACP 的 stdio JSON-RPC 与 `agy --output-format stream-json` 都采用这种方式。不依赖 SQLite,也不使用长度前缀。
10
-
11
- > **Note:** There is **no** `agy acp` subcommand. This bridge (`agy-acp` / `src/server.ts`) is a third-party ACP agent that spawns the normal `agy` CLI with stream-json.
12
- > **说明:** 不存在 `agy acp` 子命令。本仓库是第三方 ACP agent,通过官方 `agy` CLI 的 stream-json 桥接。
13
-
14
- ## Architecture / 架构
15
-
16
- ```
17
- ACP Client ←stdio JSON-RPC NDJSON→ src/server.ts ←stdin/stdout stream-json→ agy CLI
18
- │
19
- ├─ src/lib/map-agy-to-acp.ts
20
- ├─ src/lib/agy-process.ts (spawn/kill/generation)
21
- ├─ src/lib/agy-args.ts (buildAgyArgs / safety / printTimeout)
22
- ├─ src/lib/agy-discovery.ts (agy models / agents)
23
- ├─ src/lib/prompt-normalize.ts (image → files + size/cleanup)
24
- ├─ src/lib/rich-content.ts (paths → ACP image + allowlist)
25
- ├─ src/lib/path-allowlist.ts (session root checks)
26
- ├─ src/lib/soft-deny.ts (stderr soft-deny)
27
- └─ src/lib/session-store.ts (disk id/config index)
28
- ```
29
-
30
- - **No SQLite / no `~/.agy` DB reads**: never opens agy conversation stores.
31
- - **Lightweight SessionStore** (`~/.agy-acp-map/sessions.json`): ACP `sessionId` ↔ agy `conversationId` + launch snapshot only. **Not** a transcript DB.
32
- - **Client owns transcript** (e.g. zustand / gateway UI). Bridge advertises `historyReplay: false`.
33
- - **No Zed / Antigravity plugin code**.
34
- - **Persistent stdin stream-json** (same child across turns until cancel/config change).
35
- - **Resume:** `session/resume` rehydrates from memory or disk; next prompt respawns with `--conversation <id>` + saved flags. No history replay via `session/update`.
36
-
37
- ## Requirements / 环境
38
-
39
- - Bun ≥ 1.1 (tested 1.4.2) + TypeScript sources under `src/`
40
- - `agy` on PATH (`export PATH="/home/box/.local/bin:$PATH"`) and logged in
41
-
42
- ## Run / 运行
43
-
44
- ```bash
45
- export PATH="/home/box/.local/bin:$PATH"
46
- cd /workspace/agy-acp-map
47
-
48
- bun src/server.ts # Minimal ACP stdio agent (zero-runtime-dep)
49
- bun src/sdk-server.ts # Official @agentclientprotocol/sdk stdio agent
50
- bun test # unit tests (src/lib/*.test.ts)
51
- bun tests/test-agy-args.ts # CLI args unit test
52
-
53
- # Live smokes (need logged-in agy)
54
- bun tests/smoke/smoke-basic.ts
55
- bun tests/smoke/smoke-permissions.ts
56
- bun tests/smoke/smoke-image-in.ts
57
- bun tests/smoke/smoke-image-out.ts # may take longer
58
- bun tests/smoke/smoke-flags.ts # model / --conversation / sandbox / json-schema
59
- bun tests/smoke/smoke-robustness.ts # cancel / empty / bad model / set_config / list
60
-
61
- bun run smoke:all # units + full live matrix
62
- ```
63
-
64
- **Windows:** install [Bun](https://bun.sh), put `agy` on `PATH`, then the same commands.
65
-
66
-
67
- ## Launch flags / 启动参数 (v0.1.2)
68
-
69
- Configure on **`session/new`** (preferred) and/or env fallbacks. Stored on the Session; every `spawnAgy` builds argv via `buildAgyArgs(session)`.
70
-
71
- 在 **`session/new`** 上配置(优先),或用环境变量兜底。写入 Session;每次 spawn 由 `buildAgyArgs` 组装参数。
72
-
73
- | agy flag | `session/new` field | env fallback |
74
- |----------|---------------------|--------------|
75
- | `--model` | `model` or `_meta.model` or `config.model` | `AGY_ACP_MODEL` |
76
- | `--effort` | `effort` / `_meta` / `config` | `AGY_ACP_EFFORT` |
77
- | `--mode` | `mode` (`accept-edits` \| `plan`) | `AGY_ACP_MODE` |
78
- | `--agent` | `agent` | `AGY_ACP_AGENT` |
79
- | `--sandbox` | `sandbox: true` | `AGY_ACP_SANDBOX=1` |
80
- | `--json-schema` | `jsonSchema` (string or object→stringify) | `AGY_ACP_JSON_SCHEMA` (string or path) |
81
- | `--conversation` | `conversationId` (resume / switch-model flow) | _(from prior turn)_ |
82
- | `--dangerously-skip-permissions` | `safety: 'autonomous' \| 'autonomous-unsandboxed'` or `skipPermissions: true` | `AGY_ACP_SAFETY=…` / `AGY_ACP_SKIP_PERMISSIONS=1` if safety unset (**default off / safe**) |
83
- | `--disable-slash-commands` | `disableSlashCommands` (default **true**) | `AGY_ACP_DISABLE_SLASH_COMMANDS=0` to omit |
84
- | `--print-timeout` | `printTimeout` (e.g. `30m`, `120s`, `0`) | `AGY_ACP_PRINT_TIMEOUT` (default `0`) |
85
-
86
- Also accepts a simple ACP-ish `configOptions: [{ id|configId, value }, ...]` including `printTimeout`, `safety`, `disableSlashCommands`.
87
-
88
- Invalid model / effort values fail **loud** at agy spawn (stderr + turn ends); the bridge does not validate catalog ids.
89
-
90
- 无效的 model/effort 会在 agy 启动时失败(stderr 可见);桥接层不做模型目录校验。
91
-
92
- ### Session store / 会话索引 (v0.1.2)
93
-
94
- **What is stored / 存什么**
95
-
96
- | Field | Meaning |
97
- |-------|---------|
98
- | `sessionId` | ACP id minted by this bridge |
99
- | `conversationId` | agy conversation id when known |
100
- | `title`, `cwd`, `additionalDirectories` | display / workspace |
101
- | `model`, `effort`, `mode`, `agent`, `safety`, `sandbox`, `jsonSchema`, `printTimeout`, `disableSlashCommands` | launch snapshot for respawn |
102
- | `createdAt`, `updatedAt` | ISO timestamps |
103
-
104
- **What is NOT stored / 不存什么**
105
-
106
- - Messages, tool traces, NDJSON event logs, images, or any full transcript
107
- - Client UI state — **zustand / ACP Client owns the transcript**
108
-
109
- Default path: `~/.agy-acp-map/sessions.json`. Override with `AGY_ACP_STORE` or `AGY_ACP_SESSION_STORE`.
110
-
111
- 默认路径:`~/.agy-acp-map/sessions.json`;可用 `AGY_ACP_STORE` / `AGY_ACP_SESSION_STORE` 覆盖。
112
-
113
- ### Resume vs load / 恢复 vs 加载
114
-
115
- | | `session/resume` (this bridge) | History load / replay |
116
- |--|-------------------------------|------------------------|
117
- | Purpose | Re-attach ACP `sessionId` → memory + spawn with `--conversation` | Stream past turns into UI |
118
- | Bridge behavior | Rehydrate id/config from memory or **disk store**; **no** `session/update` history | **Not implemented** (`historyReplay: false`) |
119
- | Who has messages | Client already has them (or reloads from its own store) | Would be Agent → Client replay |
120
-
121
- 中文:`session/resume` 只恢复 id/配置映射并在下次 prompt 带 `--conversation` 拉起 agy;**不会**通过 `session/update` 重放历史。完整对话记录由 Client(如 zustand)持有。不实现 `session/load` 式 history replay。
122
-
123
- ### Resume & dynamic config / 恢复与动态配置
124
-
125
- 1. **First spawn** of a brand-new session: omit `--conversation`; `session/new` upserts disk store immediately.
126
- 2. Mapper learns `conversation_id` from agy `init`/`result` → `session.conversationId` + store upsert.
127
- 3. Exported in `session/new` / `session/resume` `_meta`, and `session/list` (`_meta.conversationId` when present). `session/list` merges memory + disk (**prefer memory**).
128
- 4. After cancel/kill/crash **respawn**: `--conversation <id>` so Agent-side context resumes.
129
- 5. After **close** or process restart: `session/resume` with stored `sessionId` rehydrates into memory (no child yet); next prompt spawns with saved flags + `--conversation`. Disk row kept on close unless `AGY_ACP_DELETE_ON_CLOSE=1`.
130
- 6. **Change model mid-life (recommended):** wait idle → `session/close` → `session/new` with `{ cwd, conversationId, model, ... }`.
131
- 7. **Or** idle `session/set_config_option`:
132
- ```json
133
- { "sessionId": "...", "configId": "model|effort|mode|agent|sandbox|jsonSchema|printTimeout|safety|disableSlashCommands", "value": "..." }
134
- ```
135
- Updates session fields, kills lingering child; **next** `session/prompt` respawns with new flags + `--conversation`. When busy → error `-32002`.
136
-
137
- `bridgeCapabilities.dynamicConfig: "restart"` · `resume: true` · `historyReplay: false`.
138
-
139
- ## Env / 环境变量
140
-
141
- | Env | Default | Meaning |
142
- |-----|---------|---------|
143
- | `AGY_ACP_SAFETY` | `safe` | `safe` \| `autonomous` \| `autonomous-unsandboxed` (aliases: `unsandboxed`, `autonomous_unsandboxed`). See Safety modes table. |
144
- | `AGY_ACP_SKIP_PERMISSIONS` | `0` | If **safety unset**: `1` ≈ treat as `autonomous`; `0` ≈ `safe`. Explicit `AGY_ACP_SAFETY` / `session.safety` wins. |
145
- | `AGY_ACP_DISABLE_SLASH_COMMANDS` | `1` | When `1` (default), pass `--disable-slash-commands`. Set `0` to omit. |
146
- | `AGY_ACP_PRINT_TIMEOUT` | `0` | Passed as `--print-timeout` (e.g. `30m`, `120s`, `0` = wait until turn completes). |
147
- | `AGY_BIN` | `agy` | Override binary |
148
- | `AGY_ACP_MODEL` | — | Default `--model` |
149
- | `AGY_ACP_EFFORT` | — | Default `--effort` |
150
- | `AGY_ACP_MODE` | — | Default `--mode` |
151
- | `AGY_ACP_AGENT` | — | Default `--agent` |
152
- | `AGY_ACP_SANDBOX` | — | `1`/`0` → force sandbox on/off for `safe`/`autonomous`. Ignored for `autonomous-unsandboxed` (never sandboxed). |
153
- | `AGY_ACP_JSON_SCHEMA` | — | Schema string or file path for `--json-schema` |
154
- | `AGY_ACP_KEEP_STAGING` | — | `1` → keep `.agy-acp-staging` files after turn/close (debug) |
155
- | `AGY_ACP_STORE` / `AGY_ACP_SESSION_STORE` | `~/.agy-acp-map/sessions.json` | Disk session index path (`SESSION_STORE` wins if both set) |
156
- | `AGY_ACP_DELETE_ON_CLOSE` | unset | `1`/`true` → also delete store row on `session/close` (default: **keep** disk for resume) |
157
-
158
- ## `initialize` → bridgeCapabilities
159
-
160
- Returned alongside standard ACP fields (also under `_meta.bridgeCapabilities`):
161
-
162
- | Field | Value | Notes |
163
- |-------|-------|-------|
164
- | `prompt` | `true` | |
165
- | `streaming` | `true` | |
166
- | `tools` | `true` | Mapped from agy tool steps |
167
- | `resume` | `true` | Disk/memory SessionStore; respawn passes `--conversation` |
168
- | `permissionRoundTrip` | `false` | No ACP permission UI |
169
- | `permissionMode` | `safety_tiers` | Launch strategies only; no ACP permission UI |
170
- | `safetyTiers` | `[safe, autonomous, autonomous-unsandboxed]` | See Safety modes table |
171
- | `nativeCancel` | `false` | |
172
- | `cancelMode` | `SIGINT_then_KILL` | |
173
- | `historyReplay` | `false` | No Agent→Client history replay; Client owns transcript |
174
- | `dynamicConfig` | `restart` | `session/new` + idle `set_config_option` / close+new |
175
- | `richContentInput` | `degrade_to_files` | Images → `.agy-acp-staging/` + text path |
176
- | `richContentOutput` | `best_effort` | Detect paths; inline base64 ≤2MB |
177
- | `clientFilesystem` | `false` | |
178
- | `clientTerminal` | `false` | |
179
-
180
-
181
- On `initialize`, the bridge runs `agy models` and `agy agents` (≈10s timeout, cached for process lifetime). Results appear as `availableModels` / `availableAgents` on `bridgeCapabilities` and `_meta`, plus ACP-ish `configOptions`. Discovery failure → empty arrays (initialize still succeeds).
182
-
183
- `initialize` 时会跑 `agy models` / `agy agents`(约 10s 超时,进程内缓存),结果挂在 `bridgeCapabilities.availableModels|availableAgents` 与 `configOptions`。失败则空数组,不阻断 initialize。
184
-
185
- ### Safety modes / 安全模式 (v0.1.1)
186
-
187
- No interactive ACP permission UI — **launch strategies only** (`resolveSafety` → agy flags).
188
-
189
- 无交互式 ACP 权限 UI,仅启动策略(`resolveSafety` → agy 参数)。
190
-
191
- | Mode / 模式 | Skip permissions / 跳过权限 | Sandbox / 沙箱 |
192
- |-------------|------------------------------|----------------|
193
- | **safe** (default) | no — rely on agy `settings.json` allow/deny + soft-deny messages | only if user sets `sandbox: true` / `AGY_ACP_SANDBOX=1` |
194
- | **autonomous** | yes (`--dangerously-skip-permissions`) | **default `--sandbox`** unless user sets `sandbox: false` / `AGY_ACP_SANDBOX=0` |
195
- | **autonomous-unsandboxed** (aliases: `autonomous_unsandboxed`, `unsandboxed`) | yes (`--dangerously-skip-permissions`) | **never** pass `--sandbox` (explicit dangerous tier; ignores sandbox overrides) |
196
-
197
- Accept via:
198
- - `session/new` field `safety`: `'safe' | 'autonomous' | 'autonomous-unsandboxed'`
199
- - env `AGY_ACP_SAFETY` (same values)
200
- - idle `session/set_config_option` with `configId: "safety"`
201
- - Backward compat: `AGY_ACP_SKIP_PERMISSIONS=1` ≈ `autonomous` **if safety unset**; `=0` ≈ `safe`
202
-
203
- 输入:`session/new.safety` / `AGY_ACP_SAFETY` / `set_config_option`;兼容 `AGY_ACP_SKIP_PERMISSIONS`(仅在未设 safety 时生效)。
204
-
205
- ## Image / rich content strategy / 富内容策略
206
-
207
- **Input:** agy stream-json stdin **rejects** non-text content blocks. The bridge stages `image` / binary `resource` / `audio` under `<cwd>/.agy-acp-staging/<uuid>.<ext>` and injects text like:
208
-
209
- ```
210
- User attached an image file at: /abs/path.png
211
- Please open/view that file and answer based on what you see.
212
- ```
213
-
214
- `--add-dir <cwd>` covers the staging directory.
215
-
216
- **Output:** When tools (especially `generate_image`) or agent text mention `png|jpg|webp|gif` paths, the mapper adds ACP `{ type: 'image', mimeType, data }` (base64) if the file exists, is ≤2MB, **and** resolves under session `cwd` / `.agy-acp-staging` / `additionalDirectories`; otherwise path text only.
217
-
218
- ## Soft-deny / 权限
219
-
220
- Without skip-permissions, agy may stderr e.g.:
221
-
222
- > a tool required the "command" permission … auto-denied. Add an allow-rule … (e.g. command(\<target\>))
223
-
224
- `src/lib/soft-deny.ts` parses these; at turn end the server emits an `agent_message_chunk` listing suggested allow rules.
225
-
226
- ## Files / 文件
227
-
228
- | File | Role |
229
- |------|------|
230
- | `src/server.ts` | Minimal ACP v2 stdio server (zero-runtime-dep) |
231
- | `src/agent-sdk.ts` | Official `@agentclientprotocol/sdk` Agent App |
232
- | `src/sdk-server.ts` | Official ACP SDK stdio server entry |
233
- | `tests/test-agy-args.ts` / `bun test` | Unit tests |
234
- | `tests/smoke/*.ts` | Live smoke tests (basic, flags, perms, images, robust) |
235
- | `tests/fixtures/tiny.png` | Blue “HI” PNG for image-in |
236
- | `src/lib/session-store.ts` | Disk session id/config index (atomic JSON) |
237
- | `docs/AGY_ACP_MAP_ANALYSIS.zh-CN.md` | Architecture analysis (kept) |
238
- | `src/test-agy-args.ts / `bun test`` | Unit tests (no live agy) |
239
- | `src/client-smoke*.ts` | Smokes |
240
- | `fixtures/tiny.png` | Blue “HI” PNG for image-in |
241
-
242
-
243
- ## Opinion / 看法(v0.1.2)
244
-
245
- Agree with the analysis: **stream-json > PTY/SQLite** for coupling. 0.1.0+ added process supervision, allowlists, staging cleanup; **0.1.2** adds the lightweight disk SessionStore (id mapping only — Client still owns transcript).
246
-
247
- 认同分析结论:stream-json 在耦合上优于 PTY/SQLite。0.1.0+ 已有进程监督、白名单、staging 清理;**0.1.2** 增加轻量磁盘 SessionStore(仅 id/配置映射,对话正文仍由 Client 持有)。
248
-
249
- ## Engineering baseline (0.1.0) / 工程基线
250
-
251
- | Area | Change |
252
- |------|--------|
253
- | Child `error` | `child.on('error')` → ACP agent_message + idle `error`/`cancelled`; no unhandled EventEmitter errors |
254
- | Process supervision | `src/lib/agy-process.ts`: generation tokens, SIGINT→SIGTERM→force kill (Windows `taskkill`), await old exit before respawn, ignore stale NDJSON |
255
- | Image allowlist | `fileToAcpImageBlock` only reads under session `cwd` / staging / `additionalDirectories` (realpath; relative → session cwd) |
256
- | Staging | Max 8MB/blob, 32MB/turn; cleanup after idle / session close / shutdown; `AGY_ACP_KEEP_STAGING=1` to keep |
257
- | Mapper | `result.response` fallback when no `text_delta`; `CANCELLED`/`INTERRUPTED` → `stopReason: cancelled` |
258
- | Soft-deny | Generic tool ERROR no longer treated as permission deny |
259
- | session/new | Reject missing / non-directory `cwd` |
260
-
261
- See also `docs/AGY_ACP_MAP_ANALYSIS.zh-CN.md` (analysis kept; P0 items addressed in this release).
262
-
263
- ## Session store engineering (0.1.2) / 会话存储工程
264
-
265
- | Area | Behavior |
266
- |------|----------|
267
- | Atomic write | Write `.<name>.<pid>.<ts>.tmp` then `rename` into place |
268
- | `session/list` | Union of memory + disk; **memory wins** on same `sessionId` |
269
- | `session/resume` | Memory hit → existing behavior; disk-only → rehydrate Session (no child); unknown → JSON-RPC `-32001` |
270
- | `session/close` | Kill child, drop memory; **keep** disk row (unless `AGY_ACP_DELETE_ON_CLOSE=1`) |
271
- | History | Never emits past turns on resume |
272
-
273
- ## Non-goals / 明确不做
274
-
275
- - **OneShot `-p` backend**: not planned. Continuous ACP sessions use persistent `stream-json` only. Use the `agy` CLI directly for one-off CI/`-p` scripts.
276
- - **OneShot `-p` 后端**:不做。ACP 连续会话只走常驻 `stream-json`;一次性脚本请直接用 `agy -p`。
277
- - **Interactive ACP permission round-trip**:CLI stream-json 不支持;用 safety 三档 + settings allow。
278
- - **History replay / `session/load` 重放**:不把全文存 bridge;Client(zustand/Zed)自管 transcript,bridge 只做 id 级 `resume`。
279
-
280
- ## Limitations / 限制
281
-
282
- - MCP servers from `session/new` ignored (agy has its own).
283
- - No ACP permission round-trip UI (`permissionRoundTrip: false`).
284
- - Cancel = SIGINT then SIGKILL (no mid-turn stream cancel API).
285
- - No history replay (`historyReplay: false`). Client / zustand owns transcript; store is id+config only.
286
- - Image input depends on agy `view_file` / vision actually reading the staged path.
287
- - Image output inlining is best-effort (path detection + 2MB cap); `generate_image` may be slow or gated.
288
- - `usage_update.size` is a soft floor (200k).
289
- - Structured `--json-schema` output: mapper surfaces `result.structured_output` as a fenced JSON `agent_message_chunk` plus `_meta.structuredOutput`.
290
- - Third-party bridge — review Google ToS yourself. 第三方工程桥接,请自行评估 ToS。
291
-
292
- ## ToS note / 条款说明
293
-
294
- Engineering feasibility ≠ legal permission. 即便只用官方 CLI I/O,仍可能受 ToS 约束。
295
-
296
-
297
- ## Bun + TypeScript / Windows
298
-
299
- - Runtime is **Bun** (not Node). Entry: `bun src/server.ts` (or `bun run start`).
300
- - Sources are TypeScript under `src/`; Bun runs `.ts` directly (no emit step).
301
- - **Windows:** install [Bun](https://bun.sh), ensure `agy` is on `PATH`, then same commands (`bun src/server.ts`, `bun test`, `bun run smoke:all`).
302
- - Unit tests: `bun test` (files `src/lib/*.test.ts`). Full live matrix: `bun run smoke:all`.
1
+ # agy-acp-map — ACP v2 ↔ agy stream-json bridge (v0.1.2 · Bun + TypeScript)
2
+
3
+ **English** | **中文**
4
+
5
+ ## What is NDJSON / stream-json?
6
+
7
+ NDJSON (Newline-Delimited JSON), also called JSONL, is a framing where each line is one complete JSON value. Both ACP stdio JSON-RPC and `agy --output-format stream-json` use this: the agent writes one JSON-RPC object per line on stdout; agy emits one event object per line (`init`, `step_update`, `result`). No length prefixes, no SQLite — just lines.
8
+
9
+ NDJSON(换行分隔 JSON / JSONL)是一种按行分帧的格式:每一行是一个完整的 JSON 值。ACP 的 stdio JSON-RPC 与 `agy --output-format stream-json` 都采用这种方式。不依赖 SQLite,也不使用长度前缀。
10
+
11
+ > **Note:** There is **no** `agy acp` subcommand. This bridge (`agy-acp` / `src/server.ts`) is a third-party ACP agent that spawns the normal `agy` CLI with stream-json.
12
+ > **说明:** 不存在 `agy acp` 子命令。本仓库是第三方 ACP agent,通过官方 `agy` CLI 的 stream-json 桥接。
13
+
14
+ ## Architecture / 架构
15
+
16
+ ```
17
+ ACP Client ←stdio JSON-RPC NDJSON→ src/server.ts ←stdin/stdout stream-json→ agy CLI
18
+ │
19
+ ├─ src/lib/map-agy-to-acp.ts
20
+ ├─ src/lib/agy-process.ts (spawn/kill/generation)
21
+ ├─ src/lib/agy-args.ts (buildAgyArgs / safety / printTimeout)
22
+ ├─ src/lib/agy-discovery.ts (agy models / agents)
23
+ ├─ src/lib/prompt-normalize.ts (image → files + size/cleanup)
24
+ ├─ src/lib/rich-content.ts (paths → ACP image + allowlist)
25
+ ├─ src/lib/path-allowlist.ts (session root checks)
26
+ ├─ src/lib/soft-deny.ts (stderr soft-deny)
27
+ └─ src/lib/session-store.ts (disk id/config index)
28
+ ```
29
+
30
+ - **No SQLite / no `~/.agy` DB reads**: never opens agy conversation stores.
31
+ - **Lightweight SessionStore** (`~/.agy-acp-map/sessions.json`): ACP `sessionId` ↔ agy `conversationId` + launch snapshot only. **Not** a transcript DB.
32
+ - **Small display-history journal** (`~/.agy-acp-map/history/<sessionId>.jsonl`, or `AGY_ACP_HISTORY_DIR`): stores only the visible user prompt and final assistant text. Tool calls, tool output, thoughts, images, and raw NDJSON are excluded.
33
+ - **Client owns the live transcript** (e.g. zustand / gateway UI); the JSONL journal is only a restart/reload fallback. Bridge advertises `historyReplay: true`.
34
+ - **No Zed / Antigravity plugin code**.
35
+ - **Persistent stdin stream-json** (same child across turns until cancel/config change).
36
+ - **Resume:** `session/resume` rehydrates from memory or disk; next prompt respawns with `--conversation <id>` + saved flags. v1 `session/load` and v2 `session/resume` with `replayFrom: {"type":"start"}` replay the small JSONL display journal.
37
+
38
+ ## Requirements / 环境
39
+
40
+ - Bun ≥ 1.1 (tested 1.4.2) + TypeScript sources under `src/`
41
+ - `agy` on PATH (`export PATH="/home/box/.local/bin:$PATH"`) and logged in
42
+
43
+ ## Run / 运行
44
+
45
+ ```bash
46
+ export PATH="/home/box/.local/bin:$PATH"
47
+ cd /workspace/agy-acp-map
48
+
49
+ bun src/server.ts # Minimal ACP stdio agent (zero-runtime-dep)
50
+ bun src/sdk-server.ts # Official @agentclientprotocol/sdk stdio agent
51
+ bun test # unit tests (src/lib/*.test.ts)
52
+ bun tests/test-agy-args.ts # CLI args unit test
53
+
54
+ # Live smokes (need logged-in agy)
55
+ bun tests/smoke/smoke-basic.ts
56
+ bun tests/smoke/smoke-permissions.ts
57
+ bun tests/smoke/smoke-image-in.ts
58
+ bun tests/smoke/smoke-image-out.ts # may take longer
59
+ bun tests/smoke/smoke-flags.ts # model / --conversation / sandbox / json-schema
60
+ bun tests/smoke/smoke-robustness.ts # cancel / empty / bad model / set_config / list
61
+
62
+ bun run smoke:all # units + full live matrix
63
+ ```
64
+
65
+ **Windows:** install [Bun](https://bun.sh), put `agy` on `PATH`, then the same commands.
66
+
67
+
68
+ ## Launch flags / 启动参数 (v0.1.2)
69
+
70
+ Configure on **`session/new`** (preferred) and/or env fallbacks. Stored on the Session; every `spawnAgy` builds argv via `buildAgyArgs(session)`.
71
+
72
+ 在 **`session/new`** 上配置(优先),或用环境变量兜底。写入 Session;每次 spawn 由 `buildAgyArgs` 组装参数。
73
+
74
+ | agy flag | `session/new` field | env fallback |
75
+ |----------|---------------------|--------------|
76
+ | `--model` | `model` or `_meta.model` or `config.model` | `AGY_ACP_MODEL` |
77
+ | `--effort` | `effort` / `_meta` / `config` | `AGY_ACP_EFFORT` |
78
+ | `--mode` | `mode` (`accept-edits` \| `plan`) | `AGY_ACP_MODE` |
79
+ | `--agent` | `agent` | `AGY_ACP_AGENT` |
80
+ | `--sandbox` | `sandbox: true` | `AGY_ACP_SANDBOX=1` |
81
+ | `--json-schema` | `jsonSchema` (string or object→stringify) | `AGY_ACP_JSON_SCHEMA` (string or path) |
82
+ | `--conversation` | `conversationId` (resume / switch-model flow) | _(from prior turn)_ |
83
+ | `--dangerously-skip-permissions` | `safety: 'autonomous' \| 'autonomous-unsandboxed'` or `skipPermissions: true` | `AGY_ACP_SAFETY=…` / `AGY_ACP_SKIP_PERMISSIONS=1` if safety unset (**default off / safe**) |
84
+ | `--disable-slash-commands` | `disableSlashCommands` (default **true**) | `AGY_ACP_DISABLE_SLASH_COMMANDS=0` to omit |
85
+ | `--print-timeout` | `printTimeout` (e.g. `30m`, `120s`, `0`) | `AGY_ACP_PRINT_TIMEOUT` (default `0`) |
86
+
87
+ Also accepts a simple ACP-ish `configOptions: [{ id|configId, value }, ...]` including `printTimeout`, `safety`, `disableSlashCommands`.
88
+
89
+ Invalid model / effort values fail **loud** at agy spawn (stderr + turn ends); the bridge does not validate catalog ids.
90
+
91
+ 无效的 model/effort 会在 agy 启动时失败(stderr 可见);桥接层不做模型目录校验。
92
+
93
+ ### Session store / 会话索引 (v0.1.2)
94
+
95
+ **What is stored / 存什么**
96
+
97
+ | Field | Meaning |
98
+ |-------|---------|
99
+ | `sessionId` | ACP id minted by this bridge |
100
+ | `conversationId` | agy conversation id when known |
101
+ | `title`, `cwd`, `additionalDirectories` | display / workspace |
102
+ | `model`, `effort`, `mode`, `agent`, `safety`, `sandbox`, `jsonSchema`, `printTimeout`, `disableSlashCommands` | launch snapshot for respawn |
103
+ | `createdAt`, `updatedAt` | ISO timestamps |
104
+
105
+ **What is NOT stored / 不存什么**
106
+
107
+ - agy's canonical transcript, tool traces, NDJSON event logs, images, or thought/reasoning events
108
+ - Any tool output or internal state; **the journal is display history, not a second agy context database**
109
+ - Client UI state — **zustand / ACP Client owns the live transcript**
110
+
111
+ Default path: `~/.agy-acp-map/sessions.json`. Override with `AGY_ACP_STORE` or `AGY_ACP_SESSION_STORE`.
112
+ Display-history path: `~/.agy-acp-map/history/`. Override with `AGY_ACP_HISTORY_DIR`.
113
+
114
+ 默认路径:`~/.agy-acp-map/sessions.json`;可用 `AGY_ACP_STORE` / `AGY_ACP_SESSION_STORE` 覆盖。
115
+
116
+ ### Resume vs load / 恢复 vs 加载
117
+
118
+ | | `session/resume` (this bridge) | History load / replay |
119
+ |--|-------------------------------|------------------------|
120
+ | Purpose | Re-attach ACP `sessionId` → memory + spawn with `--conversation` | Stream past turns into UI |
121
+ | Bridge behavior | Rehydrate id/config from memory or **disk store**; replay only when explicitly requested | v1 `session/load`, or v2 `session/resume` with `replayFrom: {"type":"start"}` |
122
+ | Who has messages | Client already has them (or reloads from its own store) | Bridge replays visible text from its JSONL journal |
123
+
124
+ 中文:`session/resume` 默认只恢复 id/配置映射并在下次 prompt 带 `--conversation` 继续 agy 的真实上下文;只有 v1 `session/load` 或 v2 `replayFrom: {"type":"start"}` 才会通过 `session/update` 重放 JSONL 中的用户文本和最终输出。它不是 agy 内部对话数据库的替代品。
125
+
126
+ ### Resume & dynamic config / 恢复与动态配置
127
+
128
+ 1. **First spawn** of a brand-new session: omit `--conversation`; `session/new` upserts disk store immediately.
129
+ 2. Mapper learns `conversation_id` from agy `init`/`result` → `session.conversationId` + store upsert.
130
+ 3. Exported in `session/new` / `session/resume` `_meta`, and `session/list` (`_meta.conversationId` when present). `session/list` merges memory + disk (**prefer memory**).
131
+ 4. After cancel/kill/crash **respawn**: `--conversation <id>` so Agent-side context resumes.
132
+ 5. After **close** or process restart: `session/resume` with stored `sessionId` rehydrates into memory (no child yet); next prompt spawns with saved flags + `--conversation`. Disk row kept on close unless `AGY_ACP_DELETE_ON_CLOSE=1`.
133
+ 6. **Change model mid-life (recommended):** wait idle → `session/close` → `session/new` with `{ cwd, conversationId, model, ... }`.
134
+ 7. **Or** idle `session/set_config_option`:
135
+ ```json
136
+ { "sessionId": "...", "configId": "model|effort|mode|agent|sandbox|jsonSchema|printTimeout|safety|disableSlashCommands", "value": "..." }
137
+ ```
138
+ Updates session fields, kills lingering child; **next** `session/prompt` respawns with new flags + `--conversation`. When busy → error `-32002`.
139
+
140
+ `bridgeCapabilities.dynamicConfig: "restart"` · `resume: true` · `historyReplay: true`.
141
+
142
+ ## Env / 环境变量
143
+
144
+ | Env | Default | Meaning |
145
+ |-----|---------|---------|
146
+ | `AGY_ACP_SAFETY` | `safe` | `safe` \| `autonomous` \| `autonomous-unsandboxed` (aliases: `unsandboxed`, `autonomous_unsandboxed`). See Safety modes table. |
147
+ | `AGY_ACP_SKIP_PERMISSIONS` | `0` | If **safety unset**: `1` ≈ treat as `autonomous`; `0` ≈ `safe`. Explicit `AGY_ACP_SAFETY` / `session.safety` wins. |
148
+ | `AGY_ACP_DISABLE_SLASH_COMMANDS` | `1` | When `1` (default), pass `--disable-slash-commands`. Set `0` to omit. |
149
+ | `AGY_ACP_PRINT_TIMEOUT` | `0` | Passed as `--print-timeout` (e.g. `30m`, `120s`, `0` = wait until turn completes). |
150
+ | `AGY_BIN` | `agy` | Override binary |
151
+ | `AGY_ACP_MODEL` | — | Default `--model` |
152
+ | `AGY_ACP_EFFORT` | — | Default `--effort` |
153
+ | `AGY_ACP_MODE` | — | Default `--mode` |
154
+ | `AGY_ACP_AGENT` | — | Default `--agent` |
155
+ | `AGY_ACP_SANDBOX` | — | `1`/`0` → force sandbox on/off for `safe`/`autonomous`. Ignored for `autonomous-unsandboxed` (never sandboxed). |
156
+ | `AGY_ACP_JSON_SCHEMA` | — | Schema string or file path for `--json-schema` |
157
+ | `AGY_ACP_KEEP_STAGING` | — | `1` → keep `.agy-acp-staging` files after turn/close (debug) |
158
+ | `AGY_ACP_STORE` / `AGY_ACP_SESSION_STORE` | `~/.agy-acp-map/sessions.json` | Disk session index path (`SESSION_STORE` wins if both set) |
159
+ | `AGY_ACP_HISTORY_DIR` | `~/.agy-acp-map/history` | JSONL display-history directory; one `<sessionId>.jsonl` file per session |
160
+ | `AGY_ACP_DELETE_ON_CLOSE` | unset | `1`/`true` → also delete store row on `session/close` (default: **keep** disk for resume) |
161
+
162
+ ## `initialize` → bridgeCapabilities
163
+
164
+ Returned alongside standard ACP fields (also under `_meta.bridgeCapabilities`):
165
+
166
+ | Field | Value | Notes |
167
+ |-------|-------|-------|
168
+ | `prompt` | `true` | |
169
+ | `streaming` | `true` | |
170
+ | `tools` | `true` | Mapped from agy tool steps |
171
+ | `resume` | `true` | Disk/memory SessionStore; respawn passes `--conversation` |
172
+ | `permissionRoundTrip` | `false` | No ACP permission UI |
173
+ | `permissionMode` | `safety_tiers` | Launch strategies only; no ACP permission UI |
174
+ | `safetyTiers` | `[safe, autonomous, autonomous-unsandboxed]` | See Safety modes table |
175
+ | `nativeCancel` | `false` | |
176
+ | `cancelMode` | `SIGINT_then_KILL` | |
177
+ | `historyReplay` | `true` | Replays only user prompts and final assistant text from the JSONL display journal |
178
+ | `dynamicConfig` | `restart` | `session/new` + idle `set_config_option` / close+new |
179
+ | `richContentInput` | `degrade_to_files` | Images → `.agy-acp-staging/` + text path |
180
+ | `richContentOutput` | `best_effort` | Detect paths; inline base64 ≤2MB |
181
+ | `clientFilesystem` | `false` | |
182
+ | `clientTerminal` | `false` | |
183
+
184
+
185
+ On `initialize`, the bridge runs `agy models` and `agy agents` (≈10s timeout, cached for process lifetime). Results appear as `availableModels` / `availableAgents` on `bridgeCapabilities` and `_meta`, plus ACP-ish `configOptions`. Discovery failure → empty arrays (initialize still succeeds).
186
+
187
+ `initialize` 时会跑 `agy models` / `agy agents`(约 10s 超时,进程内缓存),结果挂在 `bridgeCapabilities.availableModels|availableAgents` 与 `configOptions`。失败则空数组,不阻断 initialize。
188
+
189
+ ### Safety modes / 安全模式 (v0.1.1)
190
+
191
+ No interactive ACP permission UI — **launch strategies only** (`resolveSafety` → agy flags).
192
+
193
+ 无交互式 ACP 权限 UI,仅启动策略(`resolveSafety` → agy 参数)。
194
+
195
+ | Mode / 模式 | Skip permissions / 跳过权限 | Sandbox / 沙箱 |
196
+ |-------------|------------------------------|----------------|
197
+ | **safe** (default) | no — rely on agy `settings.json` allow/deny + soft-deny messages | only if user sets `sandbox: true` / `AGY_ACP_SANDBOX=1` |
198
+ | **autonomous** | yes (`--dangerously-skip-permissions`) | **default `--sandbox`** unless user sets `sandbox: false` / `AGY_ACP_SANDBOX=0` |
199
+ | **autonomous-unsandboxed** (aliases: `autonomous_unsandboxed`, `unsandboxed`) | yes (`--dangerously-skip-permissions`) | **never** pass `--sandbox` (explicit dangerous tier; ignores sandbox overrides) |
200
+
201
+ Accept via:
202
+ - `session/new` field `safety`: `'safe' | 'autonomous' | 'autonomous-unsandboxed'`
203
+ - env `AGY_ACP_SAFETY` (same values)
204
+ - idle `session/set_config_option` with `configId: "safety"`
205
+ - Backward compat: `AGY_ACP_SKIP_PERMISSIONS=1` ≈ `autonomous` **if safety unset**; `=0` ≈ `safe`
206
+
207
+ 输入:`session/new.safety` / `AGY_ACP_SAFETY` / `set_config_option`;兼容 `AGY_ACP_SKIP_PERMISSIONS`(仅在未设 safety 时生效)。
208
+
209
+ ## Image / rich content strategy / 富内容策略
210
+
211
+ **Input:** agy stream-json stdin **rejects** non-text content blocks. The bridge stages `image` / binary `resource` / `audio` under `<cwd>/.agy-acp-staging/<uuid>.<ext>` and injects text like:
212
+
213
+ ```
214
+ User attached an image file at: /abs/path.png
215
+ Please open/view that file and answer based on what you see.
216
+ ```
217
+
218
+ `--add-dir <cwd>` covers the staging directory.
219
+
220
+ **Output:** When tools (especially `generate_image`) or agent text mention `png|jpg|webp|gif` paths, the mapper adds ACP `{ type: 'image', mimeType, data }` (base64) if the file exists, is ≤2MB, **and** resolves under session `cwd` / `.agy-acp-staging` / `additionalDirectories`; otherwise path text only.
221
+
222
+ ## Soft-deny / 权限
223
+
224
+ Without skip-permissions, agy may stderr e.g.:
225
+
226
+ > a tool required the "command" permission … auto-denied. Add an allow-rule … (e.g. command(\<target\>))
227
+
228
+ `src/lib/soft-deny.ts` parses these; at turn end the server emits an `agent_message_chunk` listing suggested allow rules.
229
+
230
+ ## Files / 文件
231
+
232
+ | File | Role |
233
+ |------|------|
234
+ | `src/server.ts` | Minimal ACP v2 stdio server (zero-runtime-dep) |
235
+ | `src/agent-sdk.ts` | Official `@agentclientprotocol/sdk` Agent App |
236
+ | `src/sdk-server.ts` | Official ACP SDK stdio server entry |
237
+ | `tests/test-agy-args.ts` / `bun test` | Unit tests |
238
+ | `tests/smoke/*.ts` | Live smoke tests (basic, flags, perms, images, robust) |
239
+ | `tests/fixtures/tiny.png` | Blue “HI” PNG for image-in |
240
+ | `src/lib/session-store.ts` | Disk session id/config index (atomic JSON) |
241
+ | `docs/AGY_ACP_MAP_ANALYSIS.zh-CN.md` | Architecture analysis (kept) |
242
+ | `src/test-agy-args.ts / `bun test`` | Unit tests (no live agy) |
243
+ | `src/client-smoke*.ts` | Smokes |
244
+ | `fixtures/tiny.png` | Blue “HI” PNG for image-in |
245
+
246
+
247
+ ## Opinion / 看法(v0.1.2)
248
+
249
+ Agree with the analysis: **stream-json > PTY/SQLite** for coupling. 0.1.0+ added process supervision, allowlists, staging cleanup; **0.1.2** adds the lightweight disk SessionStore (id mapping only — Client still owns transcript).
250
+
251
+ 认同分析结论:stream-json 在耦合上优于 PTY/SQLite。0.1.0+ 已有进程监督、白名单、staging 清理;**0.1.2** 增加轻量磁盘 SessionStore(仅 id/配置映射,对话正文仍由 Client 持有)。
252
+
253
+ ## Engineering baseline (0.1.0) / 工程基线
254
+
255
+ | Area | Change |
256
+ |------|--------|
257
+ | Child `error` | `child.on('error')` → ACP agent_message + idle `error`/`cancelled`; no unhandled EventEmitter errors |
258
+ | Process supervision | `src/lib/agy-process.ts`: generation tokens, SIGINT→SIGTERM→force kill (Windows `taskkill`), await old exit before respawn, ignore stale NDJSON |
259
+ | Image allowlist | `fileToAcpImageBlock` only reads under session `cwd` / staging / `additionalDirectories` (realpath; relative → session cwd) |
260
+ | Staging | Max 8MB/blob, 32MB/turn; cleanup after idle / session close / shutdown; `AGY_ACP_KEEP_STAGING=1` to keep |
261
+ | Mapper | `result.response` fallback when no `text_delta`; `CANCELLED`/`INTERRUPTED` → `stopReason: cancelled` |
262
+ | Soft-deny | Generic tool ERROR no longer treated as permission deny |
263
+ | session/new | Reject missing / non-directory `cwd` |
264
+
265
+ See also `docs/AGY_ACP_MAP_ANALYSIS.zh-CN.md` (analysis kept; P0 items addressed in this release).
266
+
267
+ ## Session store engineering (0.1.2) / 会话存储工程
268
+
269
+ | Area | Behavior |
270
+ |------|----------|
271
+ | Atomic write | Write `.<name>.<pid>.<ts>.tmp` then `rename` into place |
272
+ | `session/list` | Union of memory + disk; **memory wins** on same `sessionId` |
273
+ | `session/resume` | Memory hit → existing behavior; disk-only → rehydrate Session (no child); unknown → JSON-RPC `-32001` |
274
+ | `session/close` | Kill child, drop memory; **keep** disk row (unless `AGY_ACP_DELETE_ON_CLOSE=1`) |
275
+ | History | `session/load` (v1) and `session/resume` + `replayFrom: {"type":"start"}` (v2) emit JSONL display history |
276
+
277
+ ## Non-goals / 明确不做
278
+
279
+ - **OneShot `-p` backend**: not planned. Continuous ACP sessions use persistent `stream-json` only. Use the `agy` CLI directly for one-off CI/`-p` scripts.
280
+ - **OneShot `-p` 后端**:不做。ACP 连续会话只走常驻 `stream-json`;一次性脚本请直接用 `agy -p`。
281
+ - **Interactive ACP permission round-trip**:CLI stream-json 不支持;用 safety 三档 + settings allow。
282
+ - **Full agy transcript export**:agy CLI does not expose a supported export API. The bridge only keeps a minimal visible-text JSONL journal; it does not attempt to reconstruct tool/thought history.
283
+
284
+ ## Limitations / 限制
285
+
286
+ - MCP servers from `session/new` ignored (agy has its own).
287
+ - No ACP permission round-trip UI (`permissionRoundTrip: false`).
288
+ - Cancel = SIGINT then SIGKILL (no mid-turn stream cancel API).
289
+ - History replay is intentionally minimal: no tool results, thoughts, images, partial replay cursors, or forked-session transcript reconstruction. Client / zustand still owns the live transcript; agy remains the source of model context.
290
+ - Image input depends on agy `view_file` / vision actually reading the staged path.
291
+ - Image output inlining is best-effort (path detection + 2MB cap); `generate_image` may be slow or gated.
292
+ - `usage_update.size` is the model's real context window (`modelContextWindow`: gemini-3.x 1,048,576; claude 4.6 1,000,000; gpt-oss-120b 131,072; unknown 200k; override via `AGY_ACP_CONTEXT_SIZE`). `used` is cumulative input tokens; step-level updates stream progress, `result` finalizes.
293
+ - Structured `--json-schema` output: mapper surfaces `result.structured_output` as a fenced JSON `agent_message_chunk` plus `_meta.structuredOutput`.
294
+ - Third-party bridge — review Google ToS yourself. 第三方工程桥接,请自行评估 ToS。
295
+
296
+ ## ToS note / 条款说明
297
+
298
+ Engineering feasibility ≠ legal permission. 即便只用官方 CLI I/O,仍可能受 ToS 约束。
299
+
300
+
301
+ ## ACP V1 vs V2 Draft Specifications / 协议版本说明
302
+
303
+ - **ACP V1 (Stable)**: Production-grade implementation fully conforming to canonical ACP v1 JSON-RPC specifications. `initialize` returns a minimal Zed-verified shape (`protocolVersion`, `agentInfo`, `agentCapabilities`, `authMethods: []`; models arrive via `session/new` `configOptions`). Includes deterministic keyset cursor pagination on `session/list`, atomic session deletion, and strict session/resume semantics (`sessionId` + `cwd` required, cwd matching, omitted `additionalDirectories` reset to empty).
304
+ - **ACP V2 (Draft)**: Experimental v2 draft support built on `@agentclientprotocol/sdk@1.4.0` (`@agentclientprotocol/sdk/experimental/v2`). Conforms to the SDK's schema where `session/prompt` acknowledges prompt acceptance with an immediate `{}` (empty object) response, and subsequent progress is streamed via `session/update` notifications (`state_update: running -> chunks -> state_update: idle`).
305
+ - **Prompt Validation**: All content blocks (`text`, `resource`, `resource_link`, `image`, `audio`) are validated synchronously before sending ACK, preventing background silent failures.
306
+
307
+ ## Windows Black Box & Process Supervision / Windows 无黑框配置
308
+
309
+ - **Process Supervision**: The bridge spawns all child processes (and executes process tree cleanup via `taskkill /T /F`) using `windowsHide: true`.
310
+ - **Native Windows launcher**: `dist/agy-headless.exe` is a GUI-subsystem wrapper that starts console children with `CREATE_NO_WINDOW` and forwards stdio. Real `agy.exe` sessions, discovery probes, and `taskkill` are routed through it when it is present.
311
+ - **Zed / Editor Configuration**: When configuring the bridge in Zed on Windows, point directly to `bun` or `node` with `dist/bin.js` (or a windowless shim) rather than a `.cmd` or `.bat` wrapper. Batch scripts cause `cmd.exe` to flash a console window on launch before passing control to Node/Bun.
312
+ - **If a custom Zed server still flashes**: use the native wrapper as the custom command itself, for example `command: "<repo>\\dist\\agy-headless.exe"` with args `["<path-to-bun-or-node>", "<repo>\\src\\sdk-server.ts"]` (or `dist\\bin.js`). You can also set `AGY_HEADLESS_LAUNCHER` to an absolute wrapper path. Packaging the bridge on npm improves distribution, but npm packaging alone does not change Windows process creation flags.
313
+
314
+ ## Zed via npm (global install) / Zed 全局安装用法
315
+
316
+ No Bun / TypeScript needed at runtime — the published package ships compiled
317
+ `dist/` artifacts (`bin.js` for Node, `agy-headless.exe`, plus the optional
318
+ single-file `agy-acp-win-x64.exe`). A **global** install is recommended so Zed
319
+ gets a stable absolute path (repo moves would break a local path):
320
+
321
+ ```powershell
322
+ npm install -g @yitom/agy-acp-map
323
+ npm root -g # e.g. C:\Users\<you>\AppData\Roaming\npm\node_modules
324
+ ```
325
+
326
+ Recommended Zed `agent_servers` entry (zero console flash: GUI shim starts
327
+ plain `node`, only `node` is required on the machine):
328
+
329
+ ```jsonc
330
+ "agy-acp-map-local": {
331
+ "type": "custom",
332
+ "command": "C:\\Users\\<you>\\AppData\\Roaming\\npm\\node_modules\\@yitom\\agy-acp-map\\dist\\agy-headless.exe",
333
+ "args": ["node", "C:\\Users\\<you>\\AppData\\Roaming\\npm\\node_modules\\@yitom\\agy-acp-map\\dist\\bin.js"],
334
+ "env": {}
335
+ }
336
+ ```
337
+
338
+ Fallback without the shim (works, but `node.exe` flashes one console frame on
339
+ launch because Zed spawns agents without `CREATE_NO_WINDOW`):
340
+
341
+ ```jsonc
342
+ "agy-acp-map-local": {
343
+ "type": "custom",
344
+ "command": "node",
345
+ "args": ["C:\\Users\\<you>\\AppData\\Roaming\\npm\\node_modules\\@yitom\\agy-acp-map\\dist\\bin.js"],
346
+ "env": {}
347
+ }
348
+ ```
349
+
350
+ Do NOT point Zed at the `agy-acp` / `agy-acp.cmd` global bin wrappers (extra
351
+ `cmd.exe` flash), at `src/*.ts` sources (needs Bun, dev only), or at
352
+ `bun`/`node` without an absolute `dist` path. `initialize` intentionally
353
+ returns a minimal response (`protocolVersion`, `agentInfo`,
354
+ `agentCapabilities`, `authMethods: []`); models arrive via `session/new`
355
+ `configOptions`, so the Zed model picker keeps working.
356
+
357
+ Troubleshooting: the bridge hides nothing from disk — append-only file log at
358
+ `%TEMP%\agy-acp.log` (override with `AGY_ACP_LOG`; ~1MB rotation). If Zed says
359
+ `Server exited with status exit code: 0` right after launch, check the tail of
360
+ that file: `START` with no following `V1 initialize called` means Zed never
361
+ sent bytes (spawn/config issue); `V1 initialize called` followed by
362
+ `stdin END` means Zed rejected the handshake (capture the exchange and compare
363
+ against a known-good agent such as `opencode acp`).
364
+
365
+ ## Bun + TypeScript / Windows
366
+
367
+ - Runtime is **Bun** (not Node). Entry: `bun src/server.ts` (or `bun run start`).
368
+ - Sources are TypeScript under `src/`; Bun runs `.ts` directly (no emit step).
369
+ - **Windows:** install [Bun](https://bun.sh), ensure `agy` is on `PATH`, then same commands (`bun src/server.ts`, `bun test`, `bun run smoke:all`).
370
+ - Unit & Integration tests: `bun test --timeout 30000`.
371
+