@co0ontty/wand 4.93.0 → 4.94.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 (172) hide show
  1. package/README.md +25 -30
  2. package/dist/ai-text-runner.d.ts +2 -10
  3. package/dist/ai-text-runner.js +13 -74
  4. package/dist/build-info.json +3 -3
  5. package/dist/cli.js +47 -9
  6. package/dist/config.d.ts +2 -3
  7. package/dist/config.js +26 -5
  8. package/dist/core-status-cli.d.ts +1 -1
  9. package/dist/core-status-cli.js +3 -1
  10. package/dist/daemon-connection.d.ts +10 -0
  11. package/dist/daemon-connection.js +28 -0
  12. package/dist/diagnostic-logs.d.ts +10 -0
  13. package/dist/diagnostic-logs.js +121 -0
  14. package/dist/employee-text.js +1 -3
  15. package/dist/git-quick-commit.d.ts +1 -1
  16. package/dist/git-quick-commit.js +2 -9
  17. package/dist/layout-tree.d.ts +3 -1
  18. package/dist/layout-tree.js +8 -0
  19. package/dist/local-model-process.js +20 -0
  20. package/dist/local-model-setup.js +5 -3
  21. package/dist/managed-temp.d.ts +32 -0
  22. package/dist/managed-temp.js +176 -0
  23. package/dist/middleware/api-scope.js +1 -0
  24. package/dist/models/shared.d.ts +0 -6
  25. package/dist/models/shared.js +0 -12
  26. package/dist/models/thinking.d.ts +0 -2
  27. package/dist/models/thinking.js +0 -2
  28. package/dist/models.d.ts +0 -1
  29. package/dist/models.js +3 -11
  30. package/dist/native-session-title.d.ts +1 -1
  31. package/dist/native-session-title.js +1 -1
  32. package/dist/pi-model-discovery.js +5 -17
  33. package/dist/pi-resource-recommendation.d.ts +1 -1
  34. package/dist/pi-resource-recommendation.js +129 -42
  35. package/dist/process-manager-native-history.js +1 -6
  36. package/dist/process-manager.d.ts +0 -1
  37. package/dist/process-manager.js +5 -18
  38. package/dist/provider-catalog.d.ts +1 -2
  39. package/dist/provider-catalog.js +1 -6
  40. package/dist/provider-cli-updater.js +1 -9
  41. package/dist/provider-history-scanner.d.ts +0 -3
  42. package/dist/provider-history-scanner.js +0 -26
  43. package/dist/render-daemon-client.js +6 -29
  44. package/dist/render-structured-client.js +2 -7
  45. package/dist/retention.js +1 -6
  46. package/dist/run-command.d.ts +5 -0
  47. package/dist/run-command.js +14 -1
  48. package/dist/server-session-lifecycle-routes.js +1 -1
  49. package/dist/server-storage-cleanup-routes.d.ts +3 -0
  50. package/dist/server-storage-cleanup-routes.js +19 -0
  51. package/dist/server-workspace-routes.d.ts +4 -1
  52. package/dist/server-workspace-routes.js +75 -9
  53. package/dist/server.js +21 -16
  54. package/dist/service-command-privilege.d.ts +17 -0
  55. package/dist/service-command-privilege.js +55 -0
  56. package/dist/service-manager/index.d.ts +3 -0
  57. package/dist/service-manager/index.js +3 -0
  58. package/dist/service-manager/launchd.d.ts +1 -0
  59. package/dist/service-manager/launchd.js +15 -0
  60. package/dist/service-manager/systemd.d.ts +1 -0
  61. package/dist/service-manager/systemd.js +11 -0
  62. package/dist/session-ai-context.js +17 -6
  63. package/dist/session-completion-state.d.ts +2 -0
  64. package/dist/session-completion-state.js +6 -2
  65. package/dist/session-logger.js +7 -1
  66. package/dist/session-model.d.ts +4 -0
  67. package/dist/session-model.js +13 -4
  68. package/dist/session-registry.d.ts +1 -1
  69. package/dist/session-registry.js +0 -3
  70. package/dist/silicon-employee-draft.js +0 -1
  71. package/dist/speech-service.js +18 -8
  72. package/dist/storage/mappers.js +6 -10
  73. package/dist/storage/migrations.js +14 -0
  74. package/dist/storage/uploads-repository.d.ts +36 -0
  75. package/dist/storage/uploads-repository.js +98 -0
  76. package/dist/storage-cleanup.d.ts +50 -0
  77. package/dist/storage-cleanup.js +344 -0
  78. package/dist/storage.d.ts +3 -1
  79. package/dist/storage.js +4 -0
  80. package/dist/structured-failure.d.ts +2 -0
  81. package/dist/structured-failure.js +9 -0
  82. package/dist/structured-grok-adapter.js +4 -4
  83. package/dist/structured-opencode-adapter.js +4 -9
  84. package/dist/structured-provider-common.d.ts +1 -2
  85. package/dist/structured-provider-common.js +3 -6
  86. package/dist/structured-session-manager-replay.js +2 -6
  87. package/dist/structured-session-manager.d.ts +1 -2
  88. package/dist/structured-session-manager.js +32 -25
  89. package/dist/task-organize-contract.d.ts +41 -0
  90. package/dist/task-organize-contract.js +4 -0
  91. package/dist/task-title-organize-service.d.ts +21 -0
  92. package/dist/task-title-organize-service.js +168 -0
  93. package/dist/task-title-organize.d.ts +77 -0
  94. package/dist/task-title-organize.js +346 -0
  95. package/dist/terminal-daemon-client.js +10 -48
  96. package/dist/time-utils.d.ts +2 -0
  97. package/dist/time-utils.js +7 -0
  98. package/dist/tui/attach.js +6 -5
  99. package/dist/tui/commands.d.ts +4 -1
  100. package/dist/tui/commands.js +36 -25
  101. package/dist/tui/index.js +2 -2
  102. package/dist/tui/service-actions.js +5 -4
  103. package/dist/tui/service-command.d.ts +6 -0
  104. package/dist/tui/service-command.js +40 -0
  105. package/dist/tui/service-panel.js +9 -8
  106. package/dist/types.d.ts +4 -4
  107. package/dist/upload-reference-guard.d.ts +4 -0
  108. package/dist/upload-reference-guard.js +29 -0
  109. package/dist/upload-routes.d.ts +4 -0
  110. package/dist/upload-routes.js +121 -21
  111. package/dist/user-profile.d.ts +1 -1
  112. package/dist/user-profile.js +8 -3
  113. package/dist/web-ui/content/ai-teams.js +9 -9
  114. package/dist/web-ui/content/module-manifest.json +1 -1
  115. package/dist/web-ui/content/modules/{2548dbe0d0783577/chat-render-EODYMQF6.js → 0051bd3d55f10ab7/chat-render-RY2I2CF3.js} +1 -1
  116. package/dist/web-ui/content/modules/{2548dbe0d0783577/chunk-ZTEQSXPA.js → 0051bd3d55f10ab7/chunk-2F6J2MKX.js} +1 -1
  117. package/dist/web-ui/content/modules/{2548dbe0d0783577/chunk-A33YPCY6.js → 0051bd3d55f10ab7/chunk-7AFM245W.js} +1 -1
  118. package/dist/web-ui/content/modules/{2548dbe0d0783577/chunk-QNUWUMKW.js → 0051bd3d55f10ab7/chunk-MHX2QXIS.js} +1 -1
  119. package/dist/web-ui/content/modules/{2548dbe0d0783577/chunk-WGADUEIT.js → 0051bd3d55f10ab7/chunk-RAAPETNH.js} +1 -1
  120. package/dist/web-ui/content/modules/{2548dbe0d0783577/chunk-I3UYDQLS.js → 0051bd3d55f10ab7/chunk-RXKBHYUR.js} +2 -2
  121. package/dist/web-ui/content/modules/{2548dbe0d0783577/chunk-3TCUG766.js → 0051bd3d55f10ab7/chunk-U4EDP4TI.js} +1 -1
  122. package/dist/web-ui/content/modules/{2548dbe0d0783577/chunk-HK7RBU3M.js → 0051bd3d55f10ab7/chunk-WHXQR7JT.js} +1 -1
  123. package/dist/web-ui/content/modules/0051bd3d55f10ab7/employee-card-NV34TZIH.js +1 -0
  124. package/dist/web-ui/content/modules/{2548dbe0d0783577/host-2PU6TV2I.js → 0051bd3d55f10ab7/host-2IVIZ2YC.js} +1 -1
  125. package/dist/web-ui/content/modules/{2548dbe0d0783577/host-BPYNMKFC.js → 0051bd3d55f10ab7/host-AZXNFCRW.js} +1 -1
  126. package/dist/web-ui/content/modules/{2548dbe0d0783577/host-HZIKPWYY.js → 0051bd3d55f10ab7/host-EPEQX22R.js} +1 -1
  127. package/dist/web-ui/content/modules/{2548dbe0d0783577/host-YELXZID4.js → 0051bd3d55f10ab7/host-FZYIMOLC.js} +1 -1
  128. package/dist/web-ui/content/modules/{2548dbe0d0783577/host-OGD6AWHL.js → 0051bd3d55f10ab7/host-G6GL4SNL.js} +1 -1
  129. package/dist/web-ui/content/modules/{2548dbe0d0783577/host-E3SCPOIU.js → 0051bd3d55f10ab7/host-PEILC5NU.js} +1 -1
  130. package/dist/web-ui/content/modules/{2548dbe0d0783577/host-OWD32FZ3.js → 0051bd3d55f10ab7/host-PHHNGVNL.js} +2 -2
  131. package/dist/web-ui/content/modules/{2548dbe0d0783577/host-CXDAFMXX.js → 0051bd3d55f10ab7/host-Q524TMSR.js} +1 -1
  132. package/dist/web-ui/content/modules/0051bd3d55f10ab7/host-RO5MB6RB.js +69 -0
  133. package/dist/web-ui/content/modules/{2548dbe0d0783577/host-J5XRAQFP.js → 0051bd3d55f10ab7/host-WOQRK6NG.js} +1 -1
  134. package/dist/web-ui/content/modules/{2548dbe0d0783577/host-HKXE2DOT.js → 0051bd3d55f10ab7/host-XNIR3NTJ.js} +1 -1
  135. package/dist/web-ui/content/modules/{2548dbe0d0783577/host-4E4H4JZJ.js → 0051bd3d55f10ab7/host-XRVOIEP6.js} +1 -1
  136. package/dist/web-ui/content/modules/0051bd3d55f10ab7/host-ZDKOQEMS.js +1 -0
  137. package/dist/web-ui/content/modules/0051bd3d55f10ab7/host-bridge-PMQZRG5G.js +1 -0
  138. package/dist/web-ui/content/modules/{2548dbe0d0783577/library-bridge-Y3Y24CPQ.js → 0051bd3d55f10ab7/library-bridge-DTMQIHVJ.js} +1 -1
  139. package/dist/web-ui/content/modules/{2548dbe0d0783577/page-GQBUU7T7.js → 0051bd3d55f10ab7/page-PY4JUL4W.js} +1 -1
  140. package/dist/web-ui/content/modules/{2548dbe0d0783577/panel-PG662OC5.js → 0051bd3d55f10ab7/panel-BBQFC55Q.js} +1 -1
  141. package/dist/web-ui/content/modules/0051bd3d55f10ab7/runtime.js +974 -0
  142. package/dist/web-ui/content/modules/0051bd3d55f10ab7/task-board-host-PXZSWBBG.js +2 -0
  143. package/dist/web-ui/content/modules/0051bd3d55f10ab7/workspace-agent-picker-DJSECSY6.js +1 -0
  144. package/dist/web-ui/content/modules/{2548dbe0d0783577/workspace-window-HV4T3WKR.js → 0051bd3d55f10ab7/workspace-window-MNMKSOBG.js} +1 -1
  145. package/dist/web-ui/content/styles.css +1 -1
  146. package/dist/web-ui/embedded-assets.d.ts +27 -27
  147. package/dist/web-ui/embedded-assets.js +4 -4
  148. package/dist/web-ui/provider-identity.d.ts +4 -5
  149. package/dist/web-ui/provider-identity.js +4 -10
  150. package/dist/web-ui/react/workspaces/sidebar-manage.d.ts +39 -0
  151. package/dist/web-ui/react/workspaces/sidebar-manage.js +149 -0
  152. package/dist/web-ui/react/workspaces/types.d.ts +408 -0
  153. package/dist/web-ui/react/workspaces/types.js +16 -0
  154. package/dist/web-ui/session-activity.js +1 -1
  155. package/package.json +2 -2
  156. package/dist/structured-gemini-adapter.d.ts +0 -40
  157. package/dist/structured-gemini-adapter.js +0 -208
  158. package/dist/web-ui/content/modules/2548dbe0d0783577/chunk-XGVT4ZWT.js +0 -1
  159. package/dist/web-ui/content/modules/2548dbe0d0783577/employee-card-LJ3TX72B.js +0 -1
  160. package/dist/web-ui/content/modules/2548dbe0d0783577/host-ITV2QZ4A.js +0 -1
  161. package/dist/web-ui/content/modules/2548dbe0d0783577/host-XNWC7KPR.js +0 -69
  162. package/dist/web-ui/content/modules/2548dbe0d0783577/host-bridge-G5HEVADT.js +0 -1
  163. package/dist/web-ui/content/modules/2548dbe0d0783577/runtime.js +0 -946
  164. package/dist/web-ui/content/modules/2548dbe0d0783577/task-board-host-RAN2Z2KR.js +0 -2
  165. package/dist/web-ui/content/modules/2548dbe0d0783577/workspace-agent-picker-FXLH6N5K.js +0 -1
  166. /package/dist/web-ui/content/modules/{2548dbe0d0783577/chunk-YX475LXO.js → 0051bd3d55f10ab7/chunk-6C6A2RHG.js} +0 -0
  167. /package/dist/web-ui/content/modules/{2548dbe0d0783577/chunk-NP35GDQ3.js → 0051bd3d55f10ab7/chunk-ECWYA3U5.js} +0 -0
  168. /package/dist/web-ui/content/modules/{2548dbe0d0783577/chunk-47EVOSJS.js → 0051bd3d55f10ab7/chunk-T35D57FW.js} +0 -0
  169. /package/dist/web-ui/content/modules/{2548dbe0d0783577 → 0051bd3d55f10ab7}/chunk-YOXXNXOX.js +0 -0
  170. /package/dist/web-ui/content/modules/{2548dbe0d0783577/chunk-C3K34WJD.js → 0051bd3d55f10ab7/chunk-YQJ6XYV7.js} +0 -0
  171. /package/dist/web-ui/content/modules/{2548dbe0d0783577/host-J6HBDK4A.js → 0051bd3d55f10ab7/host-2AQAIUNO.js} +0 -0
  172. /package/dist/web-ui/content/modules/{2548dbe0d0783577 → 0051bd3d55f10ab7}/sha256-ANXV72UC.js +0 -0
package/README.md CHANGED
@@ -11,7 +11,7 @@
11
11
 
12
12
  ### Overview
13
13
 
14
- Wand is a web console for remotely accessing and managing local CLI tools from a browser. It supports [Claude Code](https://docs.anthropic.com/en/docs/claude-code), [Codex](https://github.com/openai/codex), [OpenCode](https://opencode.ai/), [Grok Build](https://grok.com/), [Qoder CLI](https://docs.qoder.com/en/cli/quick-start), and [Pi](https://pi.dev/), as well as [Gemini CLI](https://github.com/google-gemini/gemini-cli), with terminal and structured conversation views, persistent resumable sessions, permission controls, file browsing, and native clients for multiple platforms.
14
+ Wand is a web console for remotely accessing and managing local CLI tools from a browser. It supports [Claude Code](https://docs.anthropic.com/en/docs/claude-code), [Codex](https://github.com/openai/codex), [OpenCode](https://opencode.ai/), [Grok Build](https://grok.com/), [Qoder CLI](https://docs.qoder.com/en/cli/quick-start), and [Pi](https://pi.dev/), with terminal and structured conversation views, persistent resumable sessions, permission controls, file browsing, and native clients for multiple platforms.
15
15
 
16
16
  The browser password manager extension source lives in `browser-extension/`.
17
17
 
@@ -48,7 +48,7 @@ wand config:password
48
48
  #### Core
49
49
 
50
50
  - **Dual view modes** — switch between raw terminal output and a structured conversation view for the same session
51
- - **Multiple providers** — create PTY or structured sessions for Claude Code, Codex, OpenCode, Grok, Qoder CLI, Pi, and Gemini CLI
51
+ - **Multiple providers** — create PTY or structured sessions for Claude Code, Codex, OpenCode, Grok, Qoder CLI, and Pi
52
52
  - **Session management** — create, archive, and resume Claude, Codex, OpenCode, Grok, and Qoder sessions with their provider-native context; restore Claude/Codex native history; show summaries in the session list
53
53
  - **Permission control** — visual permission prompts with one-time approval, per-turn memory, and related policies
54
54
 
@@ -66,7 +66,7 @@ wand config:password
66
66
  - **Native clients** — Android, iOS, and macOS clients with encrypted connection codes and update checks
67
67
  - **HTTPS** — optional self-signed certificates for remote or mobile access
68
68
  - **Update channels** — built-in stable/beta update checks and upgrade prompts
69
- - **CLI updates** — check and quickly update Claude Code, Codex, OpenCode, Qoder CLI, Pi, and Gemini CLI from server settings, with optional automatic updates
69
+ - **CLI updates** — check and quickly update Claude Code, Codex, OpenCode, Qoder CLI, and Pi from server settings, with optional automatic updates
70
70
 
71
71
  Stable subscribers receive npm `latest`; Beta subscribers receive the newer of `latest`
72
72
  and `beta`, including a stable release that supersedes its prerelease. Installing stable
@@ -113,16 +113,16 @@ Web and Android show inference calls as standalone local-decision cards, outside
113
113
 
114
114
  ### System Service
115
115
 
116
- The default service mode is system-wide: Linux writes `/etc/systemd/system/wand.service`, and macOS writes `/Library/LaunchDaemons/com.wand.web.plist`. Installing or removing the system service requires sudo.
116
+ The default service mode is system-wide: Linux writes `/etc/systemd/system/wand.service`, and macOS writes `/Library/LaunchDaemons/com.wand.web.plist`. System service changes require administrator permission. Wand automatically invokes sudo and lets you enter the password in the terminal; it never reads or stores the password. Without an interactive terminal, only already-authorized noninteractive sudo is allowed. Restart waits for active Core turns before gracefully restarting Server, leaving Render/terminald running (`--timeout <seconds>` cancels on timeout).
117
117
 
118
118
  ```bash
119
- sudo wand service:install # Register and start the service
120
- wand service:status # Show status
121
- sudo wand service:start # Start
122
- sudo wand service:stop # Stop
123
- sudo wand service:restart # Restart
124
- wand service:logs # Show recent logs
125
- sudo wand service:uninstall # Stop and remove the service
119
+ wand service:install # Register and start; automatically request sudo
120
+ wand service:status # Show status
121
+ wand service:start # Start
122
+ wand service:stop # Stop
123
+ wand service:restart # Safely restart; automatically request sudo
124
+ wand service:logs # Show recent logs
125
+ wand service:uninstall # Stop and remove the service
126
126
  ```
127
127
 
128
128
  Use `--user` for a user-level service:
@@ -180,17 +180,13 @@ npm uninstall -g opencode
180
180
  npm install -g opencode-ai@latest
181
181
  ```
182
182
 
183
- Gemini structured sessions require `@google/gemini-cli` 0.11 or newer (the release that added headless `--output-format stream-json`):
184
-
185
- ```bash
186
- npm install -g @google/gemini-cli@latest
187
183
  ```
188
184
 
189
185
  ## 中文
190
186
 
191
187
  ### 概览
192
188
 
193
- 通过浏览器远程访问和管理本地 CLI 工具的 Web 控制台。支持 [Claude Code](https://docs.anthropic.com/en/docs/claude-code)、[Codex](https://github.com/openai/codex)、[OpenCode](https://opencode.ai/)、[Grok Build](https://grok.com/)、[Qoder CLI](https://docs.qoder.com/zh/cli/quick-start)、[Pi](https://pi.dev/) 和 [Gemini CLI](https://github.com/google-gemini/gemini-cli),提供终端和结构化对话双视图、会话持久化与恢复、权限管控、文件浏览和多平台客户端。
189
+ 通过浏览器远程访问和管理本地 CLI 工具的 Web 控制台。支持 [Claude Code](https://docs.anthropic.com/en/docs/claude-code)、[Codex](https://github.com/openai/codex)、[OpenCode](https://opencode.ai/)、[Grok Build](https://grok.com/)、[Qoder CLI](https://docs.qoder.com/zh/cli/quick-start)、[Pi](https://pi.dev/),提供终端和结构化对话双视图、会话持久化与恢复、权限管控、文件浏览和多平台客户端。
194
190
 
195
191
  ### 安装
196
192
 
@@ -227,10 +223,6 @@ npm uninstall -g opencode
227
223
  npm install -g opencode-ai@latest
228
224
  ```
229
225
 
230
- Gemini 结构化会话需要 `@google/gemini-cli` 0.11 或更高版本(该版本才加入 headless 的 `--output-format stream-json`):
231
-
232
- ```bash
233
- npm install -g @google/gemini-cli@latest
234
226
  ```
235
227
 
236
228
  ### 功能
@@ -238,7 +230,7 @@ npm install -g @google/gemini-cli@latest
238
230
  #### 核心
239
231
 
240
232
  - **双视图模式** — 终端原始输出和结构化对话视图可随时切换,同一会话两种呈现
241
- - **多 Provider 支持** — Claude Code、Codex、OpenCode、Grok、Qoder CLI、Pi 和 Gemini CLI 均可创建 PTY 或结构化会话
233
+ - **多 Provider 支持** — Claude Code、Codex、OpenCode、Grok、Qoder CLI 和 Pi 均可创建 PTY 或结构化会话
242
234
  - **会话管理** — 创建、归档并携带原生上下文恢复 Claude、Codex、OpenCode、Grok、Qoder 会话;支持恢复 Claude/Codex 原生历史记录;会话列表显示摘要
243
235
  - **权限控制** — 可视化权限提示,支持逐次确认、单次批准、本轮记忆等策略
244
236
 
@@ -282,7 +274,7 @@ master 的 Beta 发布只写入 `beta`,版本为下一补丁号加
282
274
  版本基数同时检查正式 Git tags 和 npm latest,兼容未推送 Git 的本地正式发布。
283
275
  Beta 标签缺失或无效时,Beta 订阅仍可接收有效正式版;正式通道始终不选择 beta。
284
276
  两个候选都无效或 npm 不可达时不安装。
285
- - **CLI 更新** — 在服务端设置中检查并快速更新 Claude Code、Codex、OpenCode、Qoder CLI、Pi 和 Gemini CLI,也可开启自动更新
277
+ - **CLI 更新** — 在服务端设置中检查并快速更新 Claude Code、Codex、OpenCode、Qoder CLI 和 Pi,也可开启自动更新
286
278
 
287
279
  ### 配置
288
280
 
@@ -334,16 +326,16 @@ Web/Android 的推理调用独立显示为「本地决策」卡,不混入普
334
326
 
335
327
  ### 系统服务
336
328
 
337
- 默认走 **system-wide**:Linux 写 `/etc/systemd/system/wand.service`,macOS 写 `/Library/LaunchDaemons/com.wand.web.plist`。开机自启、不依赖 login session、`service wand` / `systemctl status wand` 这些老命令都能用。装/卸需要 sudo。
329
+ 默认走 **system-wide**:Linux 写 `/etc/systemd/system/wand.service`,macOS 写 `/Library/LaunchDaemons/com.wand.web.plist`。开机自启、不依赖 login session、`service wand` / `systemctl status wand` 这些老命令都能用。系统服务变更需要管理员权限,Wand 会自动申请 sudo,让你在终端输入密码(不显示、不读取、不保存)。无交互终端时仅允许已经授权的非交互 sudo。重启会先等待 Core 回合排空,再优雅重启 Server,保留 Render/terminald;可用 `--timeout <秒数>` 限制等待时间,超时不停止服务。
338
330
 
339
331
  ```bash
340
- sudo wand service:install # 注册并启动(首次安装走这里)
341
- wand service:status # 查状态(active / inactive / failed) — 读取不要 sudo
342
- sudo wand service:start # 启动
343
- sudo wand service:stop # 停止
344
- sudo wand service:restart # 重启
345
- wand service:logs # 看最近日志(--lines N 调整行数)
346
- sudo wand service:uninstall # 卸载(停服 + 删 unit)
332
+ wand service:install # 注册并启动(自动申请 sudo)
333
+ wand service:status # 查状态(active / inactive / failed) — 读取不要 sudo
334
+ wand service:start # 启动
335
+ wand service:stop # 停止
336
+ wand service:restart # 安全重启(自动申请 sudo)
337
+ wand service:logs # 看最近日志(--lines N 调整行数)
338
+ wand service:uninstall # 卸载(停服 + 删 unit)
347
339
  ```
348
340
 
349
341
  不想用 sudo?传 `--user` 切到 user-level(写 `~/.config/systemd/user/wand.service` 或 `~/Library/LaunchAgents/`):
@@ -414,6 +406,9 @@ git submodule update --init -- render-bin
414
406
  macOS 每日清理历史归档编译中间物(保留7天)用 `npm run clean:build-cache:install -- --install`;
415
407
  范围、并发保护、日志和停用方式见[自动清理说明](docs/build-cache-cleanup.md)。不修改Git hook,不自动删除现用原生构建树。
416
408
 
409
+ 通过 npm 安装后,运行存储维护默认在后台自动执行:回收过期临时残留与无引用的上传文件,限制诊断日志大小并清理过期轮转日志。无需源码仓库或日常手动操作,保留会话历史和正在使用的文件。
410
+ 归属判断、保留时间与可选管理员诊断入口见[运行存储自动维护](docs/storage-cleanup.md)。
411
+
417
412
  数据存储在 `~/.wand/` 下:`config.json`(配置)、`wand.db`(SQLite)、`sessions/`(日志)。
418
413
 
419
414
  ### License
@@ -27,15 +27,6 @@ type CliAiTextOptions = QuickCommitAiOptions & {
27
27
  export declare function normalizeAiText(raw: string): string;
28
28
  export declare function buildPiTextArgs(request: AiTextRequest, opts: QuickCommitAiOptions, allowTools?: boolean): string[];
29
29
  export declare function buildGrokTextArgs(request: AiTextRequest, opts: QuickCommitAiOptions, allowTools?: boolean): string[];
30
- /**
31
- * Gemini CLI 没有 `--append-system-prompt`,规则走 `contentWithSystemPrompt` 并进内容;
32
- * headless 下必须带 `--skip-trust`,否则未信任的工作目录会直接以 55 退出。
33
- *
34
- * `-p ""` 只把 CLI 钉在 headless 模式,真正的 prompt 走 stdin:commit diff 可能很大,
35
- * 塞进 argv 会撞 ARG_MAX。Gemini 没有 Codex `--ephemeral` 那样的临时会话开关,
36
- * 一次性调用会留在它自己的聊天记录里(Wand 不扫描 Gemini 历史,不会误当可恢复会话)。
37
- */
38
- export declare function buildGeminiTextArgs(opts: QuickCommitAiOptions, allowTools?: boolean): string[];
39
30
  export declare function buildQoderTextArgs(request: AiTextRequest, opts: QuickCommitAiOptions, allowTools?: boolean): string[];
40
31
  /** provider 自己的系统提示开关 + 规则文本;没有开关的 provider 返回空。 */
41
32
  export declare function systemPromptArgsFrom(provider: SessionProvider, system: string): string[];
@@ -47,8 +38,9 @@ export declare function runCliText(command: string, args: string[], prompt: stri
47
38
  inheritEnv?: boolean;
48
39
  signal?: AbortSignal;
49
40
  env?: (base: NodeJS.ProcessEnv) => NodeJS.ProcessEnv;
41
+ temporaryDirectory?: string;
42
+ onChildClose?: () => void;
50
43
  }): Promise<string>;
51
- export declare function assertGeminiTextCapability(): void;
52
44
  export declare function callCliAiText(request: AiTextRequest, cwd: string, language: string, opts: CliAiTextOptions): Promise<string>;
53
45
  /** 角色设定走系统提示前缀;任务自己的输出格式与约束必须排在后面。 */
54
46
  export declare function withOpsPersona(request: AiTextRequest, persona?: string): AiTextRequest;
@@ -1,11 +1,9 @@
1
1
  import { spawn } from "node:child_process";
2
2
  import { randomUUID } from "node:crypto";
3
- import { existsSync, mkdtempSync, readdirSync, rmSync, writeFileSync } from "node:fs";
4
- import { tmpdir } from "node:os";
5
- import { join } from "node:path";
6
3
  import { StringDecoder } from "node:string_decoder";
7
4
  import { appendDiagnosticTail, MAX_AI_TEXT_BYTES } from "./bounded-process-output.js";
8
5
  import { QuickCommitError } from "./command-error.js";
6
+ import { registerTemporaryChild } from "./managed-temp.js";
9
7
  import { abortableEmployeeText, prepareEmployeeTextCandidate } from "./employee-text.js";
10
8
  import { buildChildEnv } from "./env-utils.js";
11
9
  import { getGitErrorMessage } from "./git-utils.js";
@@ -112,12 +110,6 @@ function assertCliProtocolSucceeded(stdout, provider) {
112
110
  ? failed(message.errorMessage) : null;
113
111
  }
114
112
  }
115
- else if (provider === "gemini") {
116
- if (event.type === "error" && event.severity === "error")
117
- failure = failed(event);
118
- if (event.type === "result")
119
- failure = event.status === "error" ? failed(event) : null;
120
- }
121
113
  else if ((provider === "grok" || provider === "opencode") && event.type === "error") {
122
114
  failure = failed(event);
123
115
  }
@@ -237,24 +229,6 @@ function extractPiText(stdout) {
237
229
  }
238
230
  return text.trim();
239
231
  }
240
- /** Gemini `--output-format stream-json`:assistant message 是增量 chunk,拼接即可。 */
241
- function extractGeminiText(stdout) {
242
- assertCliProtocolSucceeded(stdout, "gemini");
243
- let text = "";
244
- for (const line of stdout.split(/\r?\n/)) {
245
- const trimmed = line.trim();
246
- if (!trimmed.startsWith("{"))
247
- continue;
248
- try {
249
- const event = JSON.parse(trimmed);
250
- if (event.type === "message" && event.role === "assistant" && typeof event.content === "string") {
251
- text += event.content;
252
- }
253
- }
254
- catch { /* ignore non-protocol stdout */ }
255
- }
256
- return text.trim();
257
- }
258
232
  export function buildPiTextArgs(request, opts, allowTools = false) {
259
233
  const args = ["--mode", "json", "--print", "--no-session"];
260
234
  if (!allowTools)
@@ -284,23 +258,6 @@ export function buildGrokTextArgs(request, opts, allowTools = false) {
284
258
  args.push("--tools", "", "--disallowed-tools", "run_terminal_cmd,read_file,search_replace,grep,list_dir,web_search,web_fetch,todo_write,task,Agent", "--deny", "MCPTool", "--disable-web-search", "--no-subagents");
285
259
  return args;
286
260
  }
287
- /**
288
- * Gemini CLI 没有 `--append-system-prompt`,规则走 `contentWithSystemPrompt` 并进内容;
289
- * headless 下必须带 `--skip-trust`,否则未信任的工作目录会直接以 55 退出。
290
- *
291
- * `-p ""` 只把 CLI 钉在 headless 模式,真正的 prompt 走 stdin:commit diff 可能很大,
292
- * 塞进 argv 会撞 ARG_MAX。Gemini 没有 Codex `--ephemeral` 那样的临时会话开关,
293
- * 一次性调用会留在它自己的聊天记录里(Wand 不扫描 Gemini 历史,不会误当可恢复会话)。
294
- */
295
- export function buildGeminiTextArgs(opts, allowTools = false) {
296
- const args = ["-p", "", "--output-format", "stream-json", "--skip-trust"];
297
- const model = opts.model?.trim();
298
- if (model && model !== "default")
299
- args.push("--model", model);
300
- if (allowTools)
301
- args.push("--approval-mode", "yolo");
302
- return args;
303
- }
304
261
  export function buildQoderTextArgs(request, opts, allowTools = false) {
305
262
  const args = ["-p", request.prompt, "--output-format", "stream-json", "--no-session-persistence"];
306
263
  const model = opts.model?.trim();
@@ -337,6 +294,17 @@ export function runCliText(command, args, prompt, opts) {
337
294
  env: opts.env ? opts.env(baseEnv) : baseEnv,
338
295
  stdio: ["pipe", "pipe", "pipe"],
339
296
  });
297
+ if (opts.temporaryDirectory) {
298
+ try {
299
+ registerTemporaryChild(opts.temporaryDirectory, child.pid);
300
+ }
301
+ catch {
302
+ child.on("error", () => { });
303
+ child.once("close", () => { reject(new Error("AI text temporary lease unavailable")); });
304
+ child.kill("SIGKILL");
305
+ return;
306
+ }
307
+ }
340
308
  let stdout = "";
341
309
  let stdoutBytes = 0;
342
310
  const stdoutDecoder = new StringDecoder("utf8");
@@ -386,6 +354,7 @@ export function runCliText(command, args, prompt, opts) {
386
354
  });
387
355
  child.on("close", (code) => {
388
356
  cleanup();
357
+ opts.onChildClose?.();
389
358
  if (settled)
390
359
  return;
391
360
  settled = true;
@@ -502,34 +471,6 @@ async function callPiText(request, cwd, opts) {
502
471
  throw new QuickCommitError("Pi 返回了空的 commit message。", "EMPTY_AI_MESSAGE");
503
472
  return text;
504
473
  }
505
- export function assertGeminiTextCapability() {
506
- // Gemini ignores supplemental admin policy when a central policy exists.
507
- // Refuse that unsupported text-only path rather than launching without deny.
508
- const systemPolicies = process.platform === "darwin" ? "/Library/Application Support/GeminiCli/policies"
509
- : process.platform === "win32" ? join(process.env.ProgramData ?? "C:\\ProgramData", "gemini-cli", "policies") : "/etc/gemini-cli/policies";
510
- if (existsSync(systemPolicies) && readdirSync(systemPolicies).some(file => file.endsWith(".toml"))) {
511
- throw new QuickCommitError("Gemini 中央策略阻止注入纯文本工具边界,请配置可用的文本候选。", "AI_FALLBACK_FAILED");
512
- }
513
- }
514
- async function callGeminiText(request, cwd, opts) {
515
- assertGeminiTextCapability();
516
- const root = mkdtempSync(join(tmpdir(), "wand-gemini-text-"));
517
- try {
518
- const policy = join(root, "text-only.toml");
519
- writeFileSync(policy, '[[rule]]\ntoolName = "*"\ndecision = "deny"\npriority = 999\n', { mode: 0o600 });
520
- const args = [...buildGeminiTextArgs(opts), "--admin-policy", policy, "--extensions", "none", "--allowed-mcp-server-names", ""];
521
- const stdout = await runCliText("gemini", args, contentWithSystemPrompt("gemini", request), {
522
- cwd, timeoutMs: cliTextTimeoutMs(CODEX_MESSAGE_TIMEOUT_MS, opts), inheritEnv: opts.inheritEnv, signal: opts.signal,
523
- });
524
- const text = extractGeminiText(stdout);
525
- if (!text)
526
- throw new QuickCommitError("Gemini 返回了空的 commit message。", "EMPTY_AI_MESSAGE");
527
- return text;
528
- }
529
- finally {
530
- rmSync(root, { recursive: true, force: true });
531
- }
532
- }
533
474
  export async function callCliAiText(request, cwd, language, opts) {
534
475
  const provider = defaultProvider(opts.provider);
535
476
  if (provider === "codex") {
@@ -543,8 +484,6 @@ export async function callCliAiText(request, cwd, language, opts) {
543
484
  return callQoderText(request, cwd, opts);
544
485
  if (provider === "pi")
545
486
  return callPiText(request, cwd, opts);
546
- if (provider === "gemini")
547
- return callGeminiText(request, cwd, opts);
548
487
  return callClaudeText(request, cwd, language, opts);
549
488
  }
550
489
  /**
@@ -1,6 +1,6 @@
1
1
  {
2
- "commit": "6d9e09030eeb98d41403c42abe59226c730af9f0",
3
- "builtAt": "2026-10-15T00:06:39.228Z",
4
- "version": "4.93.0",
2
+ "commit": "4323ba1516bf4465bd3e239e67508a972f01a3e4",
3
+ "builtAt": "2026-10-15T00:50:47.298Z",
4
+ "version": "4.94.0",
5
5
  "channel": "stable"
6
6
  }
package/dist/cli.js CHANGED
@@ -29,6 +29,9 @@ async function main() {
29
29
  await runAttach(live, configPath, useTui);
30
30
  break;
31
31
  }
32
+ const { installServiceDiagnosticRotation } = await import("./diagnostic-logs.js");
33
+ const { dirname } = await import("node:path");
34
+ installServiceDiagnosticRotation(dirname(configPath), "web");
32
35
  const { ensureNodePtyHelperExecutable } = await import("./ensure-node-pty-helper.js");
33
36
  ensureNodePtyHelperExecutable();
34
37
  const { isPortInUseError, startServer } = await import("./server.js");
@@ -93,6 +96,9 @@ async function main() {
93
96
  break;
94
97
  }
95
98
  case "terminald": {
99
+ const { installServiceDiagnosticRotation } = await import("./diagnostic-logs.js");
100
+ const { dirname } = await import("node:path");
101
+ installServiceDiagnosticRotation(dirname(configPath), "terminald");
96
102
  const { ensureNodePtyHelperExecutable } = await import("./ensure-node-pty-helper.js");
97
103
  ensureNodePtyHelperExecutable();
98
104
  const { runTerminalDaemon } = await import("./terminal-daemon-server.js");
@@ -103,6 +109,31 @@ async function main() {
103
109
  process.stdout.write(`${configPath}\n`);
104
110
  break;
105
111
  }
112
+ case "storage:cleanup": {
113
+ const options = args.slice(1);
114
+ for (let i = 0; i < options.length; i++) {
115
+ if (options[i] === "-c" || options[i] === "--config") {
116
+ if (!options[++i] || options[i].startsWith("-"))
117
+ throw new Error("Missing config path.");
118
+ }
119
+ else if (options[i] !== "--apply")
120
+ throw new Error("Usage: wand storage:cleanup [--apply] [-c <config>]");
121
+ }
122
+ const { createStorageCleanup } = await import("./storage-cleanup.js");
123
+ const { dirname } = await import("node:path");
124
+ const { openUploadCleanupStore } = await import("./storage/uploads-repository.js");
125
+ const { resolveDatabasePath } = await import("./storage.js");
126
+ const database = openUploadCleanupStore(resolveDatabasePath(configPath), options.includes("--apply"));
127
+ const cleanup = createStorageCleanup(dirname(configPath), { uploads: database?.uploads });
128
+ try {
129
+ process.stdout.write(`${JSON.stringify(await cleanup.run(options.includes("--apply")), null, 2)}\n`);
130
+ }
131
+ finally {
132
+ await cleanup.stop();
133
+ database?.close();
134
+ }
135
+ break;
136
+ }
106
137
  case "config:show": {
107
138
  // 展示合并后的视图(JSON 部署字段 + DB 偏好字段),但绝不打印运行时密钥。
108
139
  const { ensureDatabaseFile, resolveDatabasePath, WandStorage } = await import("./storage.js");
@@ -279,6 +310,7 @@ Commands:
279
310
  wand config:show Print current config
280
311
  wand config:password Print current login password
281
312
  wand config:set Update a simple config value
313
+ wand storage:cleanup [--apply] Inspect/reclaim abandoned Wand runtime storage
282
314
 
283
315
  Agent runtime:
284
316
  wand session:list List sessions as JSON
@@ -821,12 +853,13 @@ function setConfigValue(config, key, value) {
821
853
  port: Number(value)
822
854
  };
823
855
  case "https":
856
+ case "autoCleanupTemporaryFiles":
824
857
  if (value !== "true" && value !== "false") {
825
- throw new Error("https must be 'true' or 'false'");
858
+ throw new Error(`${key} must be 'true' or 'false'`);
826
859
  }
827
860
  return {
828
861
  ...config,
829
- https: value === "true"
862
+ [key]: value === "true"
830
863
  };
831
864
  case "publicOrigin": {
832
865
  if (!value)
@@ -861,13 +894,17 @@ function setConfigValue(config, key, value) {
861
894
  * 都在 tui/commands.ts 内部处理。
862
895
  */
863
896
  async function runServiceCommand(command, args, configPath) {
897
+ const { elevateServiceCommand, resolveServiceCommandScope } = await import("./service-command-privilege.js");
898
+ const scope = resolveServiceCommandScope(command, args);
899
+ if (command === "service:restart" && !readFlagValue(args, "-c") && !readFlagValue(args, "--config")) {
900
+ const { readServiceConfigPath } = await import("./service-manager/index.js");
901
+ configPath = readServiceConfigPath(scope) ?? configPath;
902
+ }
903
+ const elevated = await elevateServiceCommand(command, args, configPath, scope);
904
+ if (elevated !== null)
905
+ return elevated;
864
906
  const { installService, uninstallService, serviceStart, serviceStop, serviceRestart, serviceStatus, serviceLogs, } = await import("./tui/commands.js");
865
907
  const verbose = args.includes("--verbose");
866
- // --user / --system 决定 scope;不传走库里 default(= system)。
867
- // 同时传 --user 和 --system 时 --user 胜(更"友好"那一个不需要 root)。
868
- const wantUser = args.includes("--user");
869
- const wantSystem = args.includes("--system");
870
- const scope = wantUser ? "user" : (wantSystem ? "system" : undefined);
871
908
  switch (command) {
872
909
  case "service:install": {
873
910
  const result = installService({ configPath, scope });
@@ -897,7 +934,8 @@ async function runServiceCommand(command, args, configPath) {
897
934
  return result.ok ? 0 : 1;
898
935
  }
899
936
  case "service:restart": {
900
- const result = serviceRestart(scope ? { scope } : undefined);
937
+ const timeoutSeconds = Number(readFlagValue(args, "--timeout") ?? 0);
938
+ const result = await serviceRestart({ scope, configPath, timeoutSeconds });
901
939
  printServiceResult(result, verbose);
902
940
  return result.ok ? 0 : 1;
903
941
  }
@@ -929,7 +967,7 @@ async function runServiceCommand(command, args, configPath) {
929
967
  function printServiceResult(result, verbose) {
930
968
  const prefix = result.ok ? "[wand]" : "[wand] ✗";
931
969
  process.stdout.write(`${prefix} ${result.message}\n`);
932
- if (verbose && result.detail) {
970
+ if ((verbose || !result.ok) && result.detail) {
933
971
  process.stdout.write(result.detail + "\n");
934
972
  }
935
973
  }
package/dist/config.d.ts CHANGED
@@ -7,7 +7,7 @@ import type { WandStorage } from "./storage.js";
7
7
  * 升级路径:老 JSON 里仍存有这些字段时,首次启动会被搬到 DB(见 migrateLegacyPreferencesToDb),
8
8
  * 然后下一次 saveConfig 写回 JSON 时它们会被剥离(见 stripPreferenceFields)。
9
9
  */
10
- export type PreferenceKey = "defaultProvider" | "defaultSessionKind" | "defaultEngine" | "defaultTaskWorktree" | "defaultMode" | "defaultCwd" | "defaultModel" | "defaultCodexModel" | "defaultOpenCodeModel" | "defaultGrokModel" | "defaultQoderModel" | "defaultPiModel" | "defaultGeminiModel" | "modelGroups" | "commitCli" | "commitModel" | "systemAiCli" | "systemAiModel" | "defaultThinkingEffort" | "language" | "cardDefaults" | "inheritEnv" | "taskRetention" | "userProfile" | "localDecision";
10
+ export type PreferenceKey = "defaultProvider" | "defaultSessionKind" | "defaultEngine" | "defaultTaskWorktree" | "defaultMode" | "defaultCwd" | "defaultModel" | "defaultCodexModel" | "defaultOpenCodeModel" | "defaultGrokModel" | "defaultQoderModel" | "defaultPiModel" | "modelGroups" | "commitCli" | "commitModel" | "systemAiCli" | "systemAiModel" | "defaultThinkingEffort" | "language" | "cardDefaults" | "inheritEnv" | "taskRetention" | "userProfile" | "localDecision";
11
11
  /**
12
12
  * 每个 provider 的默认模型偏好字段。新增 provider 时只改这一处:
13
13
  * 偏好读写、环境归一、默认模型投影都从这张表派生。
@@ -19,7 +19,6 @@ export declare const PROVIDER_MODEL_PREFERENCE_FIELDS: {
19
19
  readonly grok: "defaultGrokModel";
20
20
  readonly qoder: "defaultQoderModel";
21
21
  readonly pi: "defaultPiModel";
22
- readonly gemini: "defaultGeminiModel";
23
22
  };
24
23
  export type ProviderModelField = (typeof PROVIDER_MODEL_PREFERENCE_FIELDS)[SessionProvider];
25
24
  export declare function isPreferenceKey(key: string): key is PreferenceKey;
@@ -74,7 +73,7 @@ export interface PreferenceSpec<T> {
74
73
  };
75
74
  readonly hot: true;
76
75
  }
77
- declare const TRIMMED_STRING_PREFERENCE_KEYS: readonly ["defaultModel", "defaultCodexModel", "defaultOpenCodeModel", "defaultGrokModel", "defaultQoderModel", "defaultPiModel", "defaultGeminiModel", "commitModel", "systemAiModel", "language"];
76
+ declare const TRIMMED_STRING_PREFERENCE_KEYS: readonly ["defaultModel", "defaultCodexModel", "defaultOpenCodeModel", "defaultGrokModel", "defaultQoderModel", "defaultPiModel", "commitModel", "systemAiModel", "language"];
78
77
  export type TrimmedStringPreferenceKey = (typeof TRIMMED_STRING_PREFERENCE_KEYS)[number];
79
78
  /** Every preference key, in schema order. */
80
79
  export declare const PREFERENCE_KEYS: PreferenceKey[];
package/dist/config.js CHANGED
@@ -24,7 +24,6 @@ export const PROVIDER_MODEL_PREFERENCE_FIELDS = {
24
24
  grok: "defaultGrokModel",
25
25
  qoder: "defaultQoderModel",
26
26
  pi: "defaultPiModel",
27
- gemini: "defaultGeminiModel",
28
27
  };
29
28
  const PROVIDER_MODEL_FIELDS = Object.values(PROVIDER_MODEL_PREFERENCE_FIELDS);
30
29
  function preferenceStorageKey(key) {
@@ -59,6 +58,7 @@ export const defaultConfig = () => ({
59
58
  host: "127.0.0.1",
60
59
  port: 8443,
61
60
  https: false,
61
+ autoCleanupTemporaryFiles: true,
62
62
  password: "change-me",
63
63
  shell: resolveDefaultShell(),
64
64
  startupCommands: [],
@@ -169,7 +169,14 @@ export async function loadConfigWithStorage(configPath, storage) {
169
169
  }
170
170
  migrateLegacyPreferencesToDb(rawInput, storage);
171
171
  migrateLegacyPasswordToDb(rawInput, storage);
172
- const config = mergeWithDefaults(rawInput);
172
+ // DB preferences are authoritative, including a user correction of an old
173
+ // JSON tool selection. Do not validate the shadowed legacy JSON value.
174
+ const mergeInput = { ...rawInput };
175
+ for (const key of EXECUTION_TOOL_PREFERENCES) {
176
+ if (storage.hasPreference(preferenceStorageKey(key)))
177
+ delete mergeInput[key];
178
+ }
179
+ const config = mergeWithDefaults(mergeInput);
173
180
  applyStoragePreferences(config, storage);
174
181
  // appSecret: DB 是权威源。
175
182
  // - DB 有 → 直接覆盖 runtime config(即使 mergeWithDefaults 临时生成了新的,也以 DB 为准)
@@ -306,7 +313,7 @@ function enumSpec(key, options) {
306
313
  }
307
314
  const TRIMMED_STRING_PREFERENCE_KEYS = [
308
315
  "defaultModel", "defaultCodexModel", "defaultOpenCodeModel", "defaultGrokModel", "defaultQoderModel",
309
- "defaultPiModel", "defaultGeminiModel", "commitModel", "systemAiModel", "language",
316
+ "defaultPiModel", "commitModel", "systemAiModel", "language",
310
317
  ];
311
318
  function isOneOf(options) {
312
319
  return (value) => typeof value === "string" && options.includes(value);
@@ -364,7 +371,6 @@ const PREFERENCE_SPECS = {
364
371
  defaultGrokModel: trimmedStringSpec("defaultGrokModel", () => ""),
365
372
  defaultQoderModel: trimmedStringSpec("defaultQoderModel", () => ""),
366
373
  defaultPiModel: trimmedStringSpec("defaultPiModel", () => ""),
367
- defaultGeminiModel: trimmedStringSpec("defaultGeminiModel", () => ""),
368
374
  modelGroups: {
369
375
  storageKey: preferenceStorageKey("modelGroups"),
370
376
  default: () => [],
@@ -472,6 +478,13 @@ export function preferenceSpec(key) {
472
478
  }
473
479
  /** Keys applied to the live process config without restart (all of them today). */
474
480
  export const HOT_PREFERENCE_KEYS = PREFERENCE_KEYS.filter((key) => PREFERENCE_SPECS[key].hot);
481
+ const EXECUTION_TOOL_PREFERENCES = ["defaultProvider", "systemAiCli", "commitCli"];
482
+ /** Removed/unknown explicit tools must never silently become another executable. */
483
+ function assertSupportedToolPreference(key, value) {
484
+ if (typeof value === "string" && value.trim() && !PREFERENCE_SPECS[key].read(value)) {
485
+ throw new Error(`不支持的执行工具偏好 ${key}: ${value}。请重新选择受支持的工具;原配置与历史已保留。`);
486
+ }
487
+ }
475
488
  /** 用 DB 里的偏好覆盖 config 对象(in-place),返回同一个引用。DB 里没有的字段不动,保留默认值。 */
476
489
  export function applyStoragePreferences(config, storage) {
477
490
  const target = config;
@@ -479,7 +492,11 @@ export function applyStoragePreferences(config, storage) {
479
492
  const spec = PREFERENCE_SPECS[key];
480
493
  if (!storage.hasPreference(spec.storageKey))
481
494
  continue;
482
- const result = spec.read(storage.getPreference(spec.storageKey, spec.default()));
495
+ const raw = storage.getPreference(spec.storageKey, spec.default());
496
+ if (EXECUTION_TOOL_PREFERENCES.includes(key)) {
497
+ assertSupportedToolPreference(key, raw);
498
+ }
499
+ const result = spec.read(raw);
483
500
  if (result)
484
501
  target[key] = result.value;
485
502
  }
@@ -649,12 +666,16 @@ function normalizeStructuredChatPersona(input) {
649
666
  return { user, assistant };
650
667
  }
651
668
  function mergeWithDefaults(input) {
669
+ for (const key of EXECUTION_TOOL_PREFERENCES)
670
+ assertSupportedToolPreference(key, input[key]);
652
671
  const defaults = defaultConfig();
653
672
  return {
654
673
  ...defaults,
655
674
  ...input,
656
675
  // Ensure https is boolean
657
676
  https: typeof input.https === "boolean" ? input.https : defaults.https,
677
+ autoCleanupTemporaryFiles: typeof input.autoCleanupTemporaryFiles === "boolean"
678
+ ? input.autoCleanupTemporaryFiles : defaults.autoCleanupTemporaryFiles,
658
679
  browserExtensionIds: Array.isArray(input.browserExtensionIds)
659
680
  ? input.browserExtensionIds.filter((id) => typeof id === "string").map((id) => id.trim()).filter(Boolean)
660
681
  : undefined,
@@ -8,4 +8,4 @@ export interface CoreStatus {
8
8
  export declare function remainingCoreRequestBudget(deadline: number): number;
9
9
  export declare function checkCoreStatus(configPath: string): Promise<void>;
10
10
  /** Zero means wait indefinitely. A timeout/error NEVER runs the stop command. */
11
- export declare function waitForCoreTurnsComplete(configPath: string, timeoutSeconds?: number, stopCommand?: string[], allowStopped?: boolean): Promise<number>;
11
+ export declare function waitForCoreTurnsComplete(configPath: string, timeoutSeconds?: number, stopCommand?: string[] | (() => Promise<number>), allowStopped?: boolean): Promise<number>;
@@ -181,7 +181,7 @@ export async function checkCoreStatus(configPath) {
181
181
  export async function waitForCoreTurnsComplete(configPath, timeoutSeconds = 0, stopCommand, allowStopped = false) {
182
182
  if (!Number.isFinite(timeoutSeconds) || timeoutSeconds < 0)
183
183
  throw new Error("--timeout 必须是非负秒数(0 表示一直等待)。");
184
- if (stopCommand && !stopCommand.length)
184
+ if (Array.isArray(stopCommand) && !stopCommand.length)
185
185
  throw new Error("缺少等待完成后要执行的停止命令。");
186
186
  const deadline = timeoutSeconds > 0 ? Date.now() + timeoutSeconds * 1000 : Infinity;
187
187
  const control = await openControl(configPath, deadline);
@@ -240,6 +240,8 @@ export async function waitForCoreTurnsComplete(configPath, timeoutSeconds = 0, s
240
240
  console.log("[wand] 原生 Core 回合已全部完成,可以停止服务。");
241
241
  if (!stopCommand)
242
242
  return 0;
243
+ if (typeof stopCommand === "function")
244
+ return await stopCommand();
243
245
  return await new Promise((resolve, reject) => {
244
246
  const child = spawn(stopCommand[0], stopCommand.slice(1), { stdio: "inherit" });
245
247
  child.once("error", reject);
@@ -4,6 +4,16 @@ export declare function rejectPendingRequests<T extends {
4
4
  timer: NodeJS.Timeout;
5
5
  reject(error: Error): void;
6
6
  }>(pending: Map<number, T>, error: Error): void;
7
+ /** Replay a retained terminal event asynchronously; disposal cancels queued delivery. */
8
+ export declare function subscribeWithPendingEvent<Event>(listeners: Set<(event: Event) => void>, listener: (event: Event) => void, pending: Event | null): {
9
+ dispose(): void;
10
+ };
11
+ /** A queued batch belongs to this registration; the owner has already taken it from its buffer. */
12
+ export declare function subscribeWithBufferedEvents<Event>(listeners: Set<(event: Event) => void>, listener: (event: Event) => void, pending: readonly Event[]): {
13
+ dispose(): void;
14
+ };
15
+ /** Snapshot listeners once so re-entrant subscription changes cannot alter the current delivery. */
16
+ export declare function emitToSubscribedListeners<Event>(listeners: Set<(event: Event) => void>, event: Event): boolean;
7
17
  /** Bound connect attempts, including sockets that close before the handshake. */
8
18
  export declare function waitForDaemonSocket(socket: net.Socket): Promise<void>;
9
19
  /** Probe the application, not just the fd: a half-open socket may still be writable. */
@@ -10,6 +10,34 @@ export function rejectPendingRequests(pending, error) {
10
10
  }
11
11
  pending.clear();
12
12
  }
13
+ /** Replay a retained terminal event asynchronously; disposal cancels queued delivery. */
14
+ export function subscribeWithPendingEvent(listeners, listener, pending) {
15
+ listeners.add(listener);
16
+ if (pending)
17
+ queueMicrotask(() => { if (listeners.has(listener))
18
+ listener(pending); });
19
+ return { dispose: () => listeners.delete(listener) };
20
+ }
21
+ /** A queued batch belongs to this registration; the owner has already taken it from its buffer. */
22
+ export function subscribeWithBufferedEvents(listeners, listener, pending) {
23
+ const subscription = subscribeWithPendingEvent(listeners, listener, null);
24
+ if (pending.length > 0)
25
+ queueMicrotask(() => {
26
+ if (!listeners.has(listener))
27
+ return;
28
+ for (const event of pending)
29
+ listener(event);
30
+ });
31
+ return subscription;
32
+ }
33
+ /** Snapshot listeners once so re-entrant subscription changes cannot alter the current delivery. */
34
+ export function emitToSubscribedListeners(listeners, event) {
35
+ if (listeners.size === 0)
36
+ return false;
37
+ for (const listener of Array.from(listeners))
38
+ listener(event);
39
+ return true;
40
+ }
13
41
  /** Bound connect attempts, including sockets that close before the handshake. */
14
42
  export function waitForDaemonSocket(socket) {
15
43
  return new Promise((resolve, reject) => {
@@ -0,0 +1,10 @@
1
+ export declare const DIAGNOSTIC_LOG_MAX_BYTES: number;
2
+ export declare const DIAGNOSTIC_LOG_ROTATIONS = 3;
3
+ export declare const DIAGNOSTIC_LOG_RETENTION_DAYS = 14;
4
+ export declare const DIAGNOSTIC_RECEIPT_SUFFIX = ".wand-log-receipt.json";
5
+ export declare function diagnosticReceiptMatches(file: string): boolean;
6
+ /** Only redundant diagnostics: PTY history and native protocol sources never enter this writer. */
7
+ export declare function appendBoundedDiagnostic(file: string, data: string | Buffer, maxBytes?: number, rotations?: number): void;
8
+ /** Attach only to launchd/file redirections matching this instance; keep TTYs and journald untouched. */
9
+ export declare function installServiceDiagnosticRotation(configDir: string, kind: "web" | "terminald"): () => void;
10
+ export declare function isDiagnosticArchive(name: string): boolean;