zcode-acp-server 0.77.0 → 0.79.0

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.
Files changed (52) hide show
  1. package/README.md +22 -22
  2. package/README.zh-CN.md +16 -16
  3. package/dist/closed-sessions.d.ts +44 -0
  4. package/dist/closed-sessions.d.ts.map +1 -0
  5. package/dist/closed-sessions.js +128 -0
  6. package/dist/closed-sessions.js.map +1 -0
  7. package/dist/code-fingerprint.json +2 -2
  8. package/dist/config/account-provider.d.ts +13 -0
  9. package/dist/config/account-provider.d.ts.map +1 -1
  10. package/dist/config/account-provider.js +119 -2
  11. package/dist/config/account-provider.js.map +1 -1
  12. package/dist/handlers/authenticate.d.ts +17 -0
  13. package/dist/handlers/authenticate.d.ts.map +1 -0
  14. package/dist/handlers/authenticate.js +25 -0
  15. package/dist/handlers/authenticate.js.map +1 -0
  16. package/dist/handlers/replay.d.ts +9 -0
  17. package/dist/handlers/replay.d.ts.map +1 -1
  18. package/dist/handlers/replay.js +28 -7
  19. package/dist/handlers/replay.js.map +1 -1
  20. package/dist/handlers/session.d.ts.map +1 -1
  21. package/dist/handlers/session.js +48 -9
  22. package/dist/handlers/session.js.map +1 -1
  23. package/dist/i18n.d.ts +3 -0
  24. package/dist/i18n.d.ts.map +1 -1
  25. package/dist/i18n.js +4 -0
  26. package/dist/i18n.js.map +1 -1
  27. package/dist/index.d.ts.map +1 -1
  28. package/dist/index.js +5 -0
  29. package/dist/index.js.map +1 -1
  30. package/dist/remote/hub-server.d.ts.map +1 -1
  31. package/dist/remote/hub-server.js +98 -1
  32. package/dist/remote/hub-server.js.map +1 -1
  33. package/dist/remote/session-close-endpoint.d.ts.map +1 -1
  34. package/dist/remote/session-close-endpoint.js +32 -7
  35. package/dist/remote/session-close-endpoint.js.map +1 -1
  36. package/dist/server.d.ts +8 -0
  37. package/dist/server.d.ts.map +1 -1
  38. package/dist/server.js +23 -5
  39. package/dist/server.js.map +1 -1
  40. package/dist/translators/index.d.ts +1 -1
  41. package/dist/translators/index.d.ts.map +1 -1
  42. package/dist/translators/index.js +1 -1
  43. package/dist/translators/index.js.map +1 -1
  44. package/dist/translators/tool-helpers.d.ts +9 -0
  45. package/dist/translators/tool-helpers.d.ts.map +1 -1
  46. package/dist/translators/tool-helpers.js +44 -0
  47. package/dist/translators/tool-helpers.js.map +1 -1
  48. package/docs/REMOTE-CLIENTS.md +44 -4
  49. package/docs/REPLAY-GUIDE.md +5 -0
  50. package/docs/TROUBLESHOOTING.md +28 -8
  51. package/docs/adr/0006-remote-session-close.md +23 -0
  52. package/package.json +1 -1
package/README.md CHANGED
@@ -193,27 +193,27 @@ before any config would be readable.
193
193
  Every `ZCODE_ACP_*` preference variable in this table has a config-file
194
194
  field (see [User config file](#user-config-file)); the file value wins.
195
195
 
196
- | Variable | Default | Purpose |
197
- | ---------------------------------- | ------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
198
- | `ZCODE_BIN` | _(auto-discovered)_ | Path to the ZCode CLI binary or its `.cjs` entry. Resolution order: this variable → `zcode` on `PATH` → the desktop-app bundle |
199
- | `ZCODE_NODE` | _(discovered)_ | Explicit Node binary to run `ZCODE_BIN` with (must support `node:sqlite`) |
200
- | `ZCODE_PROVIDER` | _(first enabled)_ | Pin the provider id from `config.json` (e.g. `builtin:zai-coding-plan`). Credentials are read from that provider only, and the model dropdown lists only its models. Unset keeps the default "first enabled provider wins". Set together with `ZCODE_MODEL` to pin the exact model: each new session is switched to `ZCODE_PROVIDER`/`ZCODE_MODEL` right after it is created, and session creation fails if the backend refuses that pair. |
201
- | `ZCODE_HOME` | `~/.zcode` | Directory that replaces `~/.zcode` as the ZCode data root. Everything the bridge reads from there follows it: credentials/provider config (`<dir>/v2/config.json`), the lazy-session alias store (`<dir>/v2/acp-lazy-sessions.json`), the desktop app's settings and tasks index (`<dir>/v2/setting.json`, `<dir>/v2/tasks-index.sqlite`), user skills (`<dir>/skills`), and the CLI config with plugin/skill/MCP enablement (`<dir>/cli/config.json` + plugin cache). Set it before starting the bridge — the credentials and tasks-index paths are resolved once at startup. |
202
- | `ZCODE_MODEL` | _(from config)_ | Override the active model id |
203
- | `ZCODE_DISALLOWED_TOOLS` | _(Cron tools)_ | Space- or comma-separated tool names removed from every session the bridge creates or resumes (sent as the backend's per-session `toolDenylist` — a registry-level removal, so the tools are neither offered to the model nor executable), merged with the built-in default that disallows `CronCreate CronList CronUpdate CronDelete` (the bridge cannot serve the backend's `automation/*` client requests, so those tools are advertised-but-broken — see #192). Rule forms like `Bash(git *)` remove the whole tool. A backend too old for `toolDenylist` gets one warning and runs without the policy. |
204
- | `ZCODE_ENABLE_AUTOMATION_TOOLS` | _(unset)_ | Set to `1` to stop disallowing the Cron* tools — only useful for a host that actually implements the backend's `automation/*` port. |
205
- | `ZCODE_ACP_AUTO_COMPACT_THRESHOLD` | _(unset)_ | Absolute token count that triggers automatic context compaction. After each successful turn (`end_turn`), if `contextUsed >= threshold`, the server invokes `session/compact` to free up context before the next prompt. Set to `0` or leave unset to disable (default). Example: `240000` triggers compaction at 240K tokens. The compaction target itself is decided by the ZCode backend. |
206
- | `ZCODE_ACP_QUOTA_AUTO_RESUME` | `1` | Wait out GLM usage-cap exhaustion (business codes 1308 …) and continue automatically: a quota-stopped `/auto` goal loop re-enters its rounds after the window reset, and quota-stopped workflow runs are resumed from the journal once the quota card reports the reset. Set `0` to restore pause-for-a-human. The bridge performs the wait (timers + a periodic journal scan), so it works while the editor/app is asleep or detached. |
207
- | `ZCODE_ACP_MODE` | `yolo` | Mode a newly created session starts in. Values are the backend's own modes — `plan`, `build`, `edit`, `yolo`, `auto`. Unset keeps `yolo` (unrestricted), the bridge's historical default. Only affects session creation; the mode dropdown still switches it per session. |
208
- | `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]`. |
209
- | `ZCODE_ACP_REMOTE` | _(unset)_ | Set to `1` to enable [remote access](#remote-access) — serve the same sessions to additional ACP clients over WebSocket. |
210
- | `ZCODE_ACP_REMOTE_TOKEN` | _(unset)_ | Auth token for remote access. **Mandatory** when `ZCODE_ACP_REMOTE=1`; remote stays disabled without it. |
211
- | `ZCODE_ACP_HUB_PORT` | `8377` | Port of the machine-level hub daemon. Map exactly this one port in your tunnel. |
212
- | `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)). |
213
- | `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. |
214
- | `ZCODE_ACP_SANDBOX` | _(unset)_ | Set to `1` to confine the agent's file writes with a macOS Seatbelt sandbox globally; per-project, set `"enabled": true` in `<workspace>/.zcode/acp/sandbox.json` instead (see [Sandbox](#sandbox)). |
215
- | `ZCODE_ACP_LANG` | _(inherited)_ | Language of the bridge's user-facing strings (popups, status/hint lines, command menu descriptions): `zh` or `en`. When unset, the bridge inherits the ZCode app's language (`localePreference`/`locale` in `~/.zcode/v2/setting.json`), then falls back to the `LC_ALL`/`LC_MESSAGES`/`LANG` locale, defaulting to English. |
216
- | `ZCODE_ACP_SHOW_COMPLETION_STATUS` | `1` | Emit successful turn-end status as assistant text. Set `0` or `false` to omit it; `session.showCompletionStatus` overrides the env. Error and cancellation warnings remain visible. |
196
+ | Variable | Default | Purpose |
197
+ | ---------------------------------- | ------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
198
+ | `ZCODE_BIN` | _(auto-discovered)_ | Path to the ZCode CLI binary or its `.cjs` entry. Resolution order: this variable → `zcode` on `PATH` → the desktop-app bundle |
199
+ | `ZCODE_NODE` | _(discovered)_ | Explicit Node binary to run `ZCODE_BIN` with (must support `node:sqlite`) |
200
+ | `ZCODE_PROVIDER` | _(first enabled)_ | Pin the provider id from `config.json` (e.g. `builtin:zai-coding-plan`). Credentials are read from that provider only, and the model dropdown lists only its models. Unset keeps the default "first enabled provider wins". Set together with `ZCODE_MODEL` to pin the exact model: each new session is switched to `ZCODE_PROVIDER`/`ZCODE_MODEL` right after it is created, and session creation fails if the backend refuses that pair. |
201
+ | `ZCODE_HOME` | `~/.zcode` | Directory that replaces `~/.zcode` as the ZCode data root. Everything the bridge reads from there follows it: credentials/provider config (`<dir>/v2/config.json`), the lazy-session alias store (`<dir>/v2/acp-lazy-sessions.json`), the desktop app's settings and tasks index (`<dir>/v2/setting.json`, `<dir>/v2/tasks-index.sqlite`), user skills (`<dir>/skills`), and the CLI config with plugin/skill/MCP enablement (`<dir>/cli/config.json` + plugin cache). Set it before starting the bridge — the credentials and tasks-index paths are resolved once at startup. |
202
+ | `ZCODE_MODEL` | _(from config)_ | Override the active model id |
203
+ | `ZCODE_DISALLOWED_TOOLS` | _(Cron tools)_ | Space- or comma-separated tool names removed from every session the bridge creates or resumes (sent as the backend's per-session `toolDenylist` — a registry-level removal, so the tools are neither offered to the model nor executable), merged with the built-in default that disallows `CronCreate CronList CronUpdate CronDelete` (the bridge cannot serve the backend's `automation/*` client requests, so those tools are advertised-but-broken — see #192). Rule forms like `Bash(git *)` remove the whole tool. Matching is by exact tool name — no wildcards (`mcp__*` matches nothing); list MCP tools by their full `mcp__<server>__<tool>` name. A backend too old for `toolDenylist` gets one warning and runs without the policy. |
204
+ | `ZCODE_ENABLE_AUTOMATION_TOOLS` | _(unset)_ | Set to `1` to stop disallowing the Cron* tools — only useful for a host that actually implements the backend's `automation/*` port. |
205
+ | `ZCODE_ACP_AUTO_COMPACT_THRESHOLD` | _(unset)_ | Absolute token count that triggers automatic context compaction. After each successful turn (`end_turn`), if `contextUsed >= threshold`, the server invokes `session/compact` to free up context before the next prompt. Set to `0` or leave unset to disable (default). Example: `240000` triggers compaction at 240K tokens. The compaction target itself is decided by the ZCode backend. |
206
+ | `ZCODE_ACP_QUOTA_AUTO_RESUME` | `1` | Wait out GLM usage-cap exhaustion (business codes 1308 …) and continue automatically: a quota-stopped `/auto` goal loop re-enters its rounds after the window reset, and quota-stopped workflow runs are resumed from the journal once the quota card reports the reset. Set `0` to restore pause-for-a-human. The bridge performs the wait (timers + a periodic journal scan), so it works while the editor/app is asleep or detached. |
207
+ | `ZCODE_ACP_MODE` | `yolo` | Mode a newly created session starts in. Values are the backend's own modes — `plan`, `build`, `edit`, `yolo`, `auto`. Unset keeps `yolo` (unrestricted), the bridge's historical default. Only affects session creation; the mode dropdown still switches it per session. |
208
+ | `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]`. |
209
+ | `ZCODE_ACP_REMOTE` | _(unset)_ | Set to `1` to enable [remote access](#remote-access) — serve the same sessions to additional ACP clients over WebSocket. |
210
+ | `ZCODE_ACP_REMOTE_TOKEN` | _(unset)_ | Auth token for remote access. **Mandatory** when `ZCODE_ACP_REMOTE=1`; remote stays disabled without it. |
211
+ | `ZCODE_ACP_HUB_PORT` | `8377` | Port of the machine-level hub daemon. Map exactly this one port in your tunnel. |
212
+ | `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)). |
213
+ | `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. |
214
+ | `ZCODE_ACP_SANDBOX` | _(unset)_ | Set to `1` to confine the agent's file writes with a macOS Seatbelt sandbox globally; per-project, set `"enabled": true` in `<workspace>/.zcode/acp/sandbox.json` instead (see [Sandbox](#sandbox)). |
215
+ | `ZCODE_ACP_LANG` | _(inherited)_ | Language of the bridge's user-facing strings (popups, status/hint lines, command menu descriptions): `zh` or `en`. When unset, the bridge inherits the ZCode app's language (`localePreference`/`locale` in `~/.zcode/v2/setting.json`), then falls back to the `LC_ALL`/`LC_MESSAGES`/`LANG` locale, defaulting to English. |
216
+ | `ZCODE_ACP_SHOW_COMPLETION_STATUS` | `1` | Emit successful turn-end status as assistant text. Set `0` or `false` to omit it; `session.showCompletionStatus` overrides the env. Error and cancellation warnings remain visible. |
217
217
 
218
218
  ## Sandbox
219
219
 
@@ -353,7 +353,7 @@ Thanks to everyone who has contributed (updated automatically from the
353
353
  <a href="https://github.com/william0wang/zcode-acp/graphs/contributors">
354
354
  <!-- release-please re-stamps &v= on every release: the new URL busts the
355
355
  contrib.rocks and GitHub camo image caches. -->
356
- <img src="https://contrib.rocks/image?repo=william0wang/zcode-acp&v=0.77.0" alt="Contributors" /> <!-- x-release-please-version -->
356
+ <img src="https://contrib.rocks/image?repo=william0wang/zcode-acp&v=0.79.0" alt="Contributors" /> <!-- x-release-please-version -->
357
357
  </a>
358
358
 
359
359
  ## Related Projects
package/README.zh-CN.md CHANGED
@@ -137,22 +137,22 @@ ZCode CLI 内置于桌面应用中,默认不会加到 `PATH`。用 `ZCODE_BIN`
137
137
  下表中所有 `ZCODE_ACP_*` 偏好类变量都有对应的配置文件字段(见
138
138
  [用户配置文件](#用户配置文件)),文件值优先。
139
139
 
140
- | 变量 | 默认值 | 用途 |
141
- | ---------------------------------- | ---------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
142
- | `ZCODE_BIN` | `zcode` | ZCode CLI 二进制文件路径或其 `.cjs` 入口 |
143
- | `ZCODE_NODE` | _(自动发现)_ | 显式指定运行 `ZCODE_BIN` 的 Node 二进制(必须支持 `node:sqlite`) |
144
- | `ZCODE_MODEL` | _(来自 config) | 覆盖当前使用的模型 id |
145
- | `ZCODE_DISALLOWED_TOOLS` | _(Cron 工具)_ | 以空格或逗号分隔的工具名,从桥接层创建或恢复的每个会话中移除(作为后端的会话级 `toolDenylist` 下发——注册表层面的移除,工具既不会提供给模型也无法执行),并与内置默认值合并——默认禁用 `CronCreate CronList CronUpdate CronDelete`(桥接层无法应答后端的 `automation/*` 客户端请求,这些工具对模型可见但必然失败,见 #192)。`Bash(git *)` 这类规则形式会移除整个工具。后端过旧不认 `toolDenylist` 时告警一次并在无策略下运行。 |
146
- | `ZCODE_ENABLE_AUTOMATION_TOOLS` | _(未设置) | 设为 `1` 取消对 Cron* 工具的默认禁用——仅对真正实现了后端 `automation/*` 端口的宿主有意义。 |
147
- | `ZCODE_ACP_AUTO_COMPACT_THRESHOLD` | _(未设置) | 触发自动压缩的绝对 token 阈值。每次回合成功完成后(`end_turn`),若 `contextUsed >= 阈值`,服务端会自动调用 `session/compact` 压缩上下文,为下一个 prompt 腾出空间。设为 `0` 或不设置则禁用(默认)。例如 `240000` 表示上下文达 24 万 token 时触发压缩。压缩目标由 ZCode 后端决定。 |
148
- | `ZCODE_ACP_DEBUG` | _(未设置) | 设为 `1` 可开启详细诊断日志(事件流、探测循环、状态更新)。默认安静——只输出警告类日志(后端管道错误、命令/权限失败、锁等待超时)。诊断桥接问题时开启;日志出现在 `Zed.log` 中,前缀为 `[zcode-acp]`。 |
149
- | `ZCODE_ACP_REMOTE` | _(未设置)_ | 设为 `1` 启用[远程访问](#远程访问)——通过 WebSocket 向更多 ACP 客户端提供相同会话。 |
150
- | `ZCODE_ACP_REMOTE_TOKEN` | _(未设置)_ | 远程访问的鉴权 token。启用 `ZCODE_ACP_REMOTE=1` 时**必填**;缺失则远程保持禁用。 |
151
- | `ZCODE_ACP_HUB_PORT` | `8377` | 机器级 `zcode-acp-hub` 的端口。隧道只映射这一个端口。 |
152
- | `ZCODE_ACP_HUB_HOST` | `127.0.0.1` | hub 绑定地址。`0.0.0.0` 会暴露仅 token 保护的明文面——只用于容器化隧道 agent 所在的私网接口(见[远程访问](#远程访问))。 |
153
- | `ZCODE_ACP_REMOTE_PORT` | `8378` | bridge ACP 端点的起始回环端口。每个 bridge(每个编辑器窗口)自动递增取下一个空闲端口。 |
154
- | `ZCODE_ACP_SANDBOX` | _(未设置)_ | 设为 `1` 全局启用 macOS Seatbelt 沙箱限制 Agent 的文件写入;项目级则在 `<工作区>/.zcode/acp/sandbox.json` 里设 `"enabled": true`(见[沙箱](#沙箱))。 |
155
- | `ZCODE_ACP_LANG` | _(继承)_ | 桥的用户可见文案(弹窗、状态/提示行、命令菜单描述)语言:`zh` 或 `en`。未设置时继承 ZCode APP 的语言设置(`~/.zcode/v2/setting.json` 的 `localePreference`/`locale`),再退回 `LC_ALL`/`LC_MESSAGES`/`LANG` 区域设置,默认英文。 |
140
+ | 变量 | 默认值 | 用途 |
141
+ | ---------------------------------- | ---------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
142
+ | `ZCODE_BIN` | `zcode` | ZCode CLI 二进制文件路径或其 `.cjs` 入口 |
143
+ | `ZCODE_NODE` | _(自动发现)_ | 显式指定运行 `ZCODE_BIN` 的 Node 二进制(必须支持 `node:sqlite`) |
144
+ | `ZCODE_MODEL` | _(来自 config) | 覆盖当前使用的模型 id |
145
+ | `ZCODE_DISALLOWED_TOOLS` | _(Cron 工具)_ | 以空格或逗号分隔的工具名,从桥接层创建或恢复的每个会话中移除(作为后端的会话级 `toolDenylist` 下发——注册表层面的移除,工具既不会提供给模型也无法执行),并与内置默认值合并——默认禁用 `CronCreate CronList CronUpdate CronDelete`(桥接层无法应答后端的 `automation/*` 客户端请求,这些工具对模型可见但必然失败,见 #192)。`Bash(git *)` 这类规则形式会移除整个工具。匹配按精确工具名进行,不支持通配符(`mcp__*` 不会命中任何工具);MCP 工具需写完整名称 `mcp__<server>__<tool>`。后端过旧不认 `toolDenylist` 时告警一次并在无策略下运行。 |
146
+ | `ZCODE_ENABLE_AUTOMATION_TOOLS` | _(未设置) | 设为 `1` 取消对 Cron* 工具的默认禁用——仅对真正实现了后端 `automation/*` 端口的宿主有意义。 |
147
+ | `ZCODE_ACP_AUTO_COMPACT_THRESHOLD` | _(未设置) | 触发自动压缩的绝对 token 阈值。每次回合成功完成后(`end_turn`),若 `contextUsed >= 阈值`,服务端会自动调用 `session/compact` 压缩上下文,为下一个 prompt 腾出空间。设为 `0` 或不设置则禁用(默认)。例如 `240000` 表示上下文达 24 万 token 时触发压缩。压缩目标由 ZCode 后端决定。 |
148
+ | `ZCODE_ACP_DEBUG` | _(未设置) | 设为 `1` 可开启详细诊断日志(事件流、探测循环、状态更新)。默认安静——只输出警告类日志(后端管道错误、命令/权限失败、锁等待超时)。诊断桥接问题时开启;日志出现在 `Zed.log` 中,前缀为 `[zcode-acp]`。 |
149
+ | `ZCODE_ACP_REMOTE` | _(未设置)_ | 设为 `1` 启用[远程访问](#远程访问)——通过 WebSocket 向更多 ACP 客户端提供相同会话。 |
150
+ | `ZCODE_ACP_REMOTE_TOKEN` | _(未设置)_ | 远程访问的鉴权 token。启用 `ZCODE_ACP_REMOTE=1` 时**必填**;缺失则远程保持禁用。 |
151
+ | `ZCODE_ACP_HUB_PORT` | `8377` | 机器级 `zcode-acp-hub` 的端口。隧道只映射这一个端口。 |
152
+ | `ZCODE_ACP_HUB_HOST` | `127.0.0.1` | hub 绑定地址。`0.0.0.0` 会暴露仅 token 保护的明文面——只用于容器化隧道 agent 所在的私网接口(见[远程访问](#远程访问))。 |
153
+ | `ZCODE_ACP_REMOTE_PORT` | `8378` | bridge ACP 端点的起始回环端口。每个 bridge(每个编辑器窗口)自动递增取下一个空闲端口。 |
154
+ | `ZCODE_ACP_SANDBOX` | _(未设置)_ | 设为 `1` 全局启用 macOS Seatbelt 沙箱限制 Agent 的文件写入;项目级则在 `<工作区>/.zcode/acp/sandbox.json` 里设 `"enabled": true`(见[沙箱](#沙箱))。 |
155
+ | `ZCODE_ACP_LANG` | _(继承)_ | 桥的用户可见文案(弹窗、状态/提示行、命令菜单描述)语言:`zh` 或 `en`。未设置时继承 ZCode APP 的语言设置(`~/.zcode/v2/setting.json` 的 `localePreference`/`locale`),再退回 `LC_ALL`/`LC_MESSAGES`/`LANG` 区域设置,默认英文。 |
156
156
 
157
157
  ## 沙箱
158
158
 
@@ -0,0 +1,44 @@
1
+ /**
2
+ * Bridge-side remote-close record (ADR-0006 persistence amendment).
3
+ *
4
+ * A remote close retires the conversation from discovery with an IN-MEMORY
5
+ * stamp (`server.discoveryRetiredAt`) — and a serve-origin bridge then
6
+ * EXITS itself (last conversation closed), so the stamp dies with the
7
+ * process. When the hub incubates the next serve bridge and a client's
8
+ * reconnect ladder session/loads the conversation, the fresh process has no
9
+ * retirement memory: the load re-arms discovery, the heartbeat re-advertises
10
+ * the sid, the hub clears its close tombstone, and the conversation the user
11
+ * closed resurrects on every surface (observed live 2026-10-28: closed
12
+ * 00:48, resurrected by an auto load at 01:07 with a real turn).
13
+ *
14
+ * This store is the durable half of the retirement: the close endpoint
15
+ * records EVERY id spelling of the conversation (editor aliases + backend
16
+ * sess_ id), the auto-load retirement gate reads it (`loadSession`), and
17
+ * genuine use (`markSessionActive` — a prompt, a user-intent load) clears
18
+ * it, preserving ADR-0006's self-heal: touching the session reopens it.
19
+ *
20
+ * Same durability rules as the lazy-alias store: a tiny JSON file under
21
+ * ~/.zcode/v2 shared by every bridge process, written only via temp+rename.
22
+ * Best-effort by design — read failures read as "nothing closed", write
23
+ * failures are logged and simply do not stick.
24
+ */
25
+ /**
26
+ * True when this id spelling belongs to a remotely closed conversation the
27
+ * user has not touched since (within the TTL). The auto-load retirement gate
28
+ * consults this; a USER-intent load or real use clears the record instead.
29
+ */
30
+ export declare function isClosedSessionId(sid: string | undefined): boolean;
31
+ /**
32
+ * Record every id spelling of a remotely closed conversation (the close
33
+ * endpoint's alias set plus the backend sess_ id). TTL-expired entries are
34
+ * pruned at write.
35
+ */
36
+ export declare function recordClosedSessionIds(sids: Array<string | undefined>): void;
37
+ /**
38
+ * Clear the close record for a conversation's id spellings — genuine use
39
+ * (`markSessionActive`): a prompt turn or a user-intent load reopens the
40
+ * conversation and the next serve-bridge respawn must not re-retire it.
41
+ * No-op (no write) when nothing is recorded.
42
+ */
43
+ export declare function clearClosedSessionIds(sids: Array<string | undefined>): void;
44
+ //# sourceMappingURL=closed-sessions.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"closed-sessions.d.ts","sourceRoot":"","sources":["../src/closed-sessions.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AA0DH;;;;GAIG;AACH,wBAAgB,iBAAiB,CAAC,GAAG,EAAE,MAAM,GAAG,SAAS,GAAG,OAAO,CAIlE;AAED;;;;GAIG;AACH,wBAAgB,sBAAsB,CAAC,IAAI,EAAE,KAAK,CAAC,MAAM,GAAG,SAAS,CAAC,GAAG,IAAI,CAU5E;AAED;;;;;GAKG;AACH,wBAAgB,qBAAqB,CAAC,IAAI,EAAE,KAAK,CAAC,MAAM,GAAG,SAAS,CAAC,GAAG,IAAI,CAY3E"}
@@ -0,0 +1,128 @@
1
+ /**
2
+ * Bridge-side remote-close record (ADR-0006 persistence amendment).
3
+ *
4
+ * A remote close retires the conversation from discovery with an IN-MEMORY
5
+ * stamp (`server.discoveryRetiredAt`) — and a serve-origin bridge then
6
+ * EXITS itself (last conversation closed), so the stamp dies with the
7
+ * process. When the hub incubates the next serve bridge and a client's
8
+ * reconnect ladder session/loads the conversation, the fresh process has no
9
+ * retirement memory: the load re-arms discovery, the heartbeat re-advertises
10
+ * the sid, the hub clears its close tombstone, and the conversation the user
11
+ * closed resurrects on every surface (observed live 2026-10-28: closed
12
+ * 00:48, resurrected by an auto load at 01:07 with a real turn).
13
+ *
14
+ * This store is the durable half of the retirement: the close endpoint
15
+ * records EVERY id spelling of the conversation (editor aliases + backend
16
+ * sess_ id), the auto-load retirement gate reads it (`loadSession`), and
17
+ * genuine use (`markSessionActive` — a prompt, a user-intent load) clears
18
+ * it, preserving ADR-0006's self-heal: touching the session reopens it.
19
+ *
20
+ * Same durability rules as the lazy-alias store: a tiny JSON file under
21
+ * ~/.zcode/v2 shared by every bridge process, written only via temp+rename.
22
+ * Best-effort by design — read failures read as "nothing closed", write
23
+ * failures are logged and simply do not stick.
24
+ */
25
+ import { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
26
+ import path from "node:path";
27
+ import process from "node:process";
28
+ import { warn, zcodeHomeDir } from "./utils.js";
29
+ /** Store file lives next to the lazy-alias store under ~/.zcode/v2/. */
30
+ const STORE_FILENAME = "acp-closed-sessions.json";
31
+ /**
32
+ * Entries older than this expire: a conversation closed this long ago
33
+ * without a single bridge-side touch stays closed in any practical sense,
34
+ * and the expiry bounds staleness mistakes (a desktop-side reopen that never
35
+ * passed through a bridge would otherwise keep the App's tab retired
36
+ * forever).
37
+ */
38
+ const TTL_MS = 30 * 24 * 60 * 60 * 1000;
39
+ /** Resolved at call time so tests can stub HOME/ZCODE_HOME without re-importing. */
40
+ function storePath() {
41
+ return path.join(zcodeHomeDir(), "v2", STORE_FILENAME);
42
+ }
43
+ function readTable() {
44
+ try {
45
+ const p = storePath();
46
+ if (!existsSync(p))
47
+ return {};
48
+ const raw = JSON.parse(readFileSync(p, "utf8"));
49
+ if (typeof raw !== "object" || raw === null)
50
+ return {};
51
+ const out = {};
52
+ for (const [sid, at] of Object.entries(raw)) {
53
+ if (typeof at === "number")
54
+ out[sid] = at;
55
+ }
56
+ return out;
57
+ }
58
+ catch {
59
+ // Corrupt or unreadable reads as nothing closed — never fatal.
60
+ return {};
61
+ }
62
+ }
63
+ function persist(table) {
64
+ try {
65
+ const p = storePath();
66
+ const dir = path.dirname(p);
67
+ mkdirSync(dir, { recursive: true });
68
+ const tmp = `${p}.tmp-${process.pid}`;
69
+ writeFileSync(tmp, JSON.stringify(table, null, 2));
70
+ renameSync(tmp, p);
71
+ }
72
+ catch (e) {
73
+ warn(`closed-sessions: store write failed ` +
74
+ `(${e instanceof Error ? e.message : String(e)}) — the close record will not stick`);
75
+ }
76
+ }
77
+ /**
78
+ * True when this id spelling belongs to a remotely closed conversation the
79
+ * user has not touched since (within the TTL). The auto-load retirement gate
80
+ * consults this; a USER-intent load or real use clears the record instead.
81
+ */
82
+ export function isClosedSessionId(sid) {
83
+ if (typeof sid !== "string" || sid.length === 0)
84
+ return false;
85
+ const at = readTable()[sid];
86
+ return at !== undefined && Date.now() - at <= TTL_MS;
87
+ }
88
+ /**
89
+ * Record every id spelling of a remotely closed conversation (the close
90
+ * endpoint's alias set plus the backend sess_ id). TTL-expired entries are
91
+ * pruned at write.
92
+ */
93
+ export function recordClosedSessionIds(sids) {
94
+ const ids = sids.filter((sid) => typeof sid === "string" && sid.length > 0);
95
+ if (ids.length === 0)
96
+ return;
97
+ const table = readTable();
98
+ const now = Date.now();
99
+ for (const [sid, at] of Object.entries(table)) {
100
+ if (now - at > TTL_MS)
101
+ delete table[sid];
102
+ }
103
+ for (const sid of new Set(ids))
104
+ table[sid] = now;
105
+ persist(table);
106
+ }
107
+ /**
108
+ * Clear the close record for a conversation's id spellings — genuine use
109
+ * (`markSessionActive`): a prompt turn or a user-intent load reopens the
110
+ * conversation and the next serve-bridge respawn must not re-retire it.
111
+ * No-op (no write) when nothing is recorded.
112
+ */
113
+ export function clearClosedSessionIds(sids) {
114
+ const ids = sids.filter((sid) => typeof sid === "string" && sid.length > 0);
115
+ if (ids.length === 0)
116
+ return;
117
+ const table = readTable();
118
+ let touched = false;
119
+ for (const sid of new Set(ids)) {
120
+ if (sid in table) {
121
+ delete table[sid];
122
+ touched = true;
123
+ }
124
+ }
125
+ if (touched)
126
+ persist(table);
127
+ }
128
+ //# sourceMappingURL=closed-sessions.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"closed-sessions.js","sourceRoot":"","sources":["../src/closed-sessions.ts"],"names":[],"mappings":"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"}
@@ -1,4 +1,4 @@
1
1
  {
2
- "fingerprint": "b40f0708d0cbc346cfb0d94ea40da1b34d8e713c99235728bcd0cb60ee81bd75",
3
- "fileCount": 129
2
+ "fingerprint": "4457cb5a58ba417e8bd6a451fd917fda4c8c570642a56f37ac3316656aa9be05",
3
+ "fileCount": 131
4
4
  }
@@ -36,6 +36,10 @@ export interface BuiltinProviderRule {
36
36
  mode?: string;
37
37
  accountType?: string;
38
38
  };
39
+ api?: {
40
+ type?: string;
41
+ baseUrl?: string;
42
+ };
39
43
  };
40
44
  }
41
45
  export interface BuiltinTable {
@@ -84,6 +88,15 @@ export declare function configProviderIdFor(providerId: string): string;
84
88
  export declare function codingPlanRequestAuthFor(accountProviderId: string | undefined): {
85
89
  apiKey: string;
86
90
  } | null;
91
+ /**
92
+ * Whether any usable ZCode credential exists — the `authenticate` method's
93
+ * readiness check (#344). True when a manual coding-plan key sits in
94
+ * provider_config.json, a bridge account is saved, the merged config.json
95
+ * view holds any provider key, or the desktop ladder found an entitlement.
96
+ * False means the editor user gets the actionable sign-in error instead of a
97
+ * silent success that dies on the first turn.
98
+ */
99
+ export declare function zcodeCredentialsPresent(): boolean;
87
100
  /** The `provider/updateAccountConfig` payload (schema-verified shape). */ export interface AccountProviderPayload {
88
101
  revision: string;
89
102
  basedOnZCodeBuiltinRevision: string;
@@ -1 +1 @@
1
- {"version":3,"file":"account-provider.d.ts","sourceRoot":"","sources":["../../src/config/account-provider.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AAQH,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,sBAAsB,CAAC;AAQzD,8CAA8C;AAC9C,MAAM,WAAW,mBAAmB;IAClC,UAAU,EAAE,MAAM,CAAC;IACnB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,MAAM,CAAC,EAAE;QACP,KAAK,CAAC,EAAE,MAAM,CAAC;QACf,eAAe,CAAC,EAAE,MAAM,EAAE,CAAC;QAC3B,MAAM,CAAC,EAAE;YAAE,IAAI,CAAC,EAAE,MAAM,CAAC;YAAC,IAAI,CAAC,EAAE,MAAM,CAAC;YAAC,WAAW,CAAC,EAAE,MAAM,CAAA;SAAE,CAAC;KACjE,CAAC;CACH;AAED,MAAM,WAAW,YAAY;IAC3B,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,MAAM,CAAC,EAAE;QAAE,mBAAmB,CAAC,EAAE;YAAE,aAAa,CAAC,EAAE,mBAAmB,EAAE,CAAA;SAAE,CAAA;KAAE,CAAC;CAC9E;AAqCD;;;;;GAKG;AACH,wBAAgB,gBAAgB,IAAI;IAAE,KAAK,EAAE,YAAY,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,GAAG,IAAI,CAiB/E;AAaD;;;;;;GAMG;AACH,wBAAgB,oBAAoB,CAAC,UAAU,EAAE,MAAM,GAAG,MAAM,CAiB/D;AAED,+EAA+E;AAC/E,wBAAgB,mBAAmB,CAAC,UAAU,EAAE,MAAM,GAAG,MAAM,CAuB9D;AAwID;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,wBAAwB,CACtC,iBAAiB,EAAE,MAAM,GAAG,SAAS,GACpC;IAAE,MAAM,EAAE,MAAM,CAAA;CAAE,GAAG,IAAI,CA0B3B;AAED,0EAA0E,CAAC,MAAM,WAAW,sBAAsB;IAChH,QAAQ,EAAE,MAAM,CAAC;IACjB,2BAA2B,EAAE,MAAM,CAAC;IACpC,SAAS,EAAE,MAAM,CACf,MAAM,EACN;QAAE,eAAe,CAAC,EAAE,MAAM,EAAE,CAAC;QAAC,MAAM,EAAE;YAAE,IAAI,EAAE,MAAM,CAAC;YAAC,QAAQ,EAAE,OAAO,CAAA;SAAE,CAAA;KAAE,CAC5E,CAAC;IACF,MAAM,EAAE,MAAM,CACZ,MAAM,EACN;QACE,YAAY,EAAE,WAAW,GAAG,SAAS,GAAG,aAAa,GAAG,SAAS,CAAC;QAClE,QAAQ,EAAE,OAAO,CAAC;QAClB,OAAO,EAAE,OAAO,CAAC;KAClB,CACF,CAAC;CACH;AAED;;;;GAIG;AACH,wBAAgB,0BAA0B,IAAI,sBAAsB,GAAG,IAAI,CA6B1E;AAED;;;;;GAKG;AACH,wBAAsB,yBAAyB,CAC7C,OAAO,EAAE,YAAY,EACrB,MAAM,EAAE,MAAM,MAAM,GACnB,OAAO,CAAC,OAAO,CAAC,CA2BlB"}
1
+ {"version":3,"file":"account-provider.d.ts","sourceRoot":"","sources":["../../src/config/account-provider.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AAQH,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,sBAAsB,CAAC;AAQzD,8CAA8C;AAC9C,MAAM,WAAW,mBAAmB;IAClC,UAAU,EAAE,MAAM,CAAC;IACnB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,MAAM,CAAC,EAAE;QACP,KAAK,CAAC,EAAE,MAAM,CAAC;QACf,eAAe,CAAC,EAAE,MAAM,EAAE,CAAC;QAC3B,MAAM,CAAC,EAAE;YAAE,IAAI,CAAC,EAAE,MAAM,CAAC;YAAC,IAAI,CAAC,EAAE,MAAM,CAAC;YAAC,WAAW,CAAC,EAAE,MAAM,CAAA;SAAE,CAAC;QAChE,GAAG,CAAC,EAAE;YAAE,IAAI,CAAC,EAAE,MAAM,CAAC;YAAC,OAAO,CAAC,EAAE,MAAM,CAAA;SAAE,CAAC;KAC3C,CAAC;CACH;AAED,MAAM,WAAW,YAAY;IAC3B,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,MAAM,CAAC,EAAE;QAAE,mBAAmB,CAAC,EAAE;YAAE,aAAa,CAAC,EAAE,mBAAmB,EAAE,CAAA;SAAE,CAAA;KAAE,CAAC;CAC9E;AAqCD;;;;;GAKG;AACH,wBAAgB,gBAAgB,IAAI;IAAE,KAAK,EAAE,YAAY,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,GAAG,IAAI,CAiB/E;AAaD;;;;;;GAMG;AACH,wBAAgB,oBAAoB,CAAC,UAAU,EAAE,MAAM,GAAG,MAAM,CAiB/D;AAED,+EAA+E;AAC/E,wBAAgB,mBAAmB,CAAC,UAAU,EAAE,MAAM,GAAG,MAAM,CAuB9D;AAsOD;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,wBAAwB,CACtC,iBAAiB,EAAE,MAAM,GAAG,SAAS,GACpC;IAAE,MAAM,EAAE,MAAM,CAAA;CAAE,GAAG,IAAI,CA+B3B;AAED;;;;;;;GAOG;AACH,wBAAgB,uBAAuB,IAAI,OAAO,CAcjD;AAED,0EAA0E,CAAC,MAAM,WAAW,sBAAsB;IAChH,QAAQ,EAAE,MAAM,CAAC;IACjB,2BAA2B,EAAE,MAAM,CAAC;IACpC,SAAS,EAAE,MAAM,CACf,MAAM,EACN;QAAE,eAAe,CAAC,EAAE,MAAM,EAAE,CAAC;QAAC,MAAM,EAAE;YAAE,IAAI,EAAE,MAAM,CAAC;YAAC,QAAQ,EAAE,OAAO,CAAA;SAAE,CAAA;KAAE,CAC5E,CAAC;IACF,MAAM,EAAE,MAAM,CACZ,MAAM,EACN;QACE,YAAY,EAAE,WAAW,GAAG,SAAS,GAAG,aAAa,GAAG,SAAS,CAAC;QAClE,QAAQ,EAAE,OAAO,CAAC;QAClB,OAAO,EAAE,OAAO,CAAC;KAClB,CACF,CAAC;CACH;AAED;;;;GAIG;AACH,wBAAgB,0BAA0B,IAAI,sBAAsB,GAAG,IAAI,CA6B1E;AAED;;;;;GAKG;AACH,wBAAsB,yBAAyB,CAC7C,OAAO,EAAE,YAAY,EACrB,MAAM,EAAE,MAAM,MAAM,GACnB,OAAO,CAAC,OAAO,CAAC,CA2BlB"}
@@ -29,7 +29,7 @@ import os from "node:os";
29
29
  import path from "node:path";
30
30
  import { readCredentialStoreSync } from "../backend/credential-store.js";
31
31
  import { BUILTIN_PROVIDER_ENV, builtinProviderEnv } from "../backend/resolve.js";
32
- import { log, warn, ZCODE_CREDS_PATH } from "../utils.js";
32
+ import { log, warn, zcodePersonalProviderPath, ZCODE_CREDS_PATH } from "../utils.js";
33
33
  import { bridgeOverlays, readCredentialConfig } from "./bridge-accounts.js";
34
34
  /** The bundled provider-table file the CLI boots from. */
35
35
  const PROVIDER_CONFIG_NAME = "zcode-builtin.json";
@@ -195,15 +195,101 @@ function storeIndividualCodingPlanKey(accountProviderId) {
195
195
  * installed catalog is unusable gains nothing. They never count as a
196
196
  * verdict, so an account for one family leaves source 3 open for the other.
197
197
  *
198
+ * Manual plan keys from provider_config.json (`zhipu-coding-plan-api-key`
199
+ * rules, #344) sit at the source-2 tier — see manualCodingPlanKeys below.
200
+ *
198
201
  * Verified 2026-09 against the desktop's own pushed account snapshot: sources
199
202
  * 1+2 produced exactly its entitlement set (bigmodel individual true, every
200
203
  * zai/start/team plan false); source 3 restores the same outcome on fresh
201
204
  * desktop-only layouts (verified 2026-10 against a real store).
202
205
  */
206
+ /**
207
+ * Manual coding-plan api keys from provider_config.json (#344).
208
+ *
209
+ * The desktop's Settings → Model Provider → "Use API key" flow writes the plan
210
+ * key as a personal rule with access type `zhipu-coding-plan-api-key`, while
211
+ * an OAuth login writes NO legacy config.json key at all — so a 3.12+
212
+ * desktop-only machine had no entitlement source the ladder above could read:
213
+ * every account model switch failed with "Provider Registry 中不存在 Model"
214
+ * and the reporter had to copy the key into config.json by hand. These rules
215
+ * are the same tier as config.json's builtin entries — an explicit user key,
216
+ * not a verdict — so they ADD to the ladder without arming sawVerdict, and a
217
+ * coding-plan-cache `unavailable` verdict (about the ACCOUNT plan, not manual
218
+ * keys) does not suppress them.
219
+ *
220
+ * The family comes from the rule's api.baseUrl (the official coding-plan
221
+ * endpoints; the rule id itself — e.g. `zai-api` — is user-chosen and carries
222
+ * no family promise). Upstream plans to fold this access type back into
223
+ * plain `api-key` (todo-157, domain-gated) — matching then moves to the
224
+ * baseUrl alone, which this already reads.
225
+ */
226
+ function manualCodingPlanKeys() {
227
+ const families = codingPlanFamiliesByBaseUrl();
228
+ const out = new Map();
229
+ for (const rule of readPersonalProviderRules()) {
230
+ if (rule.enabled === false)
231
+ continue;
232
+ if (rule.config?.access?.type !== "zhipu-coding-plan-api-key")
233
+ continue;
234
+ const apiKey = rule.config.access.apiKey?.trim();
235
+ const baseUrl = rule.config.api?.baseUrl?.replace(/\/+$/, "");
236
+ const family = baseUrl ? families.get(baseUrl) : undefined;
237
+ if (!apiKey || !family)
238
+ continue;
239
+ out.set(`builtin:${family}-coding-plan`, apiKey);
240
+ }
241
+ return out;
242
+ }
243
+ let personalRulesCache = null;
244
+ /**
245
+ * Personal provider rules, memoized by the same stat key as the builtin table
246
+ * — the runtime-headers path re-reads these before every model request.
247
+ */
248
+ function readPersonalProviderRules() {
249
+ const file = zcodePersonalProviderPath();
250
+ try {
251
+ const { ino, size, mtimeMs, ctimeMs } = statSync(file);
252
+ const key = `${file}\0${ino}:${size}:${mtimeMs}:${ctimeMs}`;
253
+ if (personalRulesCache?.key !== key) {
254
+ const parsed = JSON.parse(readFileSync(file, "utf8"));
255
+ personalRulesCache = { key, rules: parsed.config?.providerConfigRules?.providerRules ?? [] };
256
+ }
257
+ return personalRulesCache.rules;
258
+ }
259
+ catch {
260
+ return []; // absent (pre-3.12 desktop / hermetic HOME) — nothing to add
261
+ }
262
+ }
263
+ /**
264
+ * baseUrl → family for the OFFICIAL individual coding-plan endpoints. Derived
265
+ * from the installed table (a zhipu-account rule's api.baseUrl IS its family's
266
+ * coding-plan endpoint), with the frozen official pair as the table-absent
267
+ * fallback (headless Linux): api.z.ai → zai, open.bigmodel.cn → bigmodel —
268
+ * the same endpoints the shipped table carries.
269
+ */
270
+ function codingPlanFamiliesByBaseUrl() {
271
+ const out = new Map([
272
+ ["https://api.z.ai/api/anthropic", "zai"],
273
+ ["https://open.bigmodel.cn/api/anthropic", "bigmodel"],
274
+ ]);
275
+ for (const rule of readBuiltinTable()?.table.config?.providerConfigRules?.providerRules ?? []) {
276
+ if (rule.config?.access?.type !== "zhipu-account")
277
+ continue;
278
+ if (rule.config?.access?.mode !== "individual-coding-plan")
279
+ continue;
280
+ const family = rule.config?.access?.accountType;
281
+ const baseUrl = rule.config?.api?.baseUrl?.replace(/\/+$/, "");
282
+ if (family && baseUrl)
283
+ out.set(baseUrl, family);
284
+ }
285
+ return out;
286
+ }
203
287
  function entitledBuiltinProviders() {
204
288
  const out = desktopEntitledBuiltinProviders();
205
289
  for (const family of Object.keys(bridgeOverlays()))
206
290
  out.add(`builtin:${family}-coding-plan`);
291
+ for (const legacyId of manualCodingPlanKeys().keys())
292
+ out.add(legacyId);
207
293
  return out;
208
294
  }
209
295
  /** Sources 1-4 above, read from the raw desktop files only. */
@@ -315,11 +401,42 @@ export function codingPlanRequestAuthFor(accountProviderId) {
315
401
  return { apiKey };
316
402
  }
317
403
  catch {
318
- // unreadable/absent config — the store decides
404
+ // unreadable/absent config — the manual key or the store decides
319
405
  }
406
+ // The desktop's own manual plan key (#344): 3.12+ writes it to
407
+ // provider_config.json, never config.json — rank it above the encrypted
408
+ // store (a fresh-install account key) but below the bridge's own setups.
409
+ const manual = manualCodingPlanKeys().get(legacyId);
410
+ if (manual)
411
+ return { apiKey: manual };
320
412
  const stored = storeIndividualCodingPlanKey(accountProviderId);
321
413
  return stored ? { apiKey: stored } : null;
322
414
  }
415
+ /**
416
+ * Whether any usable ZCode credential exists — the `authenticate` method's
417
+ * readiness check (#344). True when a manual coding-plan key sits in
418
+ * provider_config.json, a bridge account is saved, the merged config.json
419
+ * view holds any provider key, or the desktop ladder found an entitlement.
420
+ * False means the editor user gets the actionable sign-in error instead of a
421
+ * silent success that dies on the first turn.
422
+ */
423
+ export function zcodeCredentialsPresent() {
424
+ if (manualCodingPlanKeys().size > 0)
425
+ return true;
426
+ if (Object.keys(bridgeOverlays()).length > 0)
427
+ return true;
428
+ try {
429
+ const cfg = readCredentialConfig(ZCODE_CREDS_PATH);
430
+ for (const entry of Object.values(cfg.provider ?? {})) {
431
+ if (entry?.options?.apiKey?.trim())
432
+ return true;
433
+ }
434
+ }
435
+ catch {
436
+ // unreadable desktop config — the ladder below still speaks
437
+ }
438
+ return desktopEntitledBuiltinProviders().size > 0;
439
+ }
323
440
  /**
324
441
  * Build the account snapshot: every coding-plan provider the bundled table
325
442
  * declares, with entitlement taken from config.json's enabled builtin plans.