@ai-zen/cli 1.0.0-alpha.9 → 1.0.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 (336) hide show
  1. package/CHANGELOG.md +1334 -275
  2. package/LICENSE +21 -0
  3. package/README.md +136 -31
  4. package/README.zh.md +136 -31
  5. package/dist/agent-creator.d.ts +11 -0
  6. package/dist/agent-creator.d.ts.map +1 -1
  7. package/dist/agent-creator.js +50 -20
  8. package/dist/agent-creator.js.map +1 -1
  9. package/dist/agent-switch.d.ts +61 -0
  10. package/dist/agent-switch.d.ts.map +1 -0
  11. package/dist/agent-switch.js +83 -0
  12. package/dist/agent-switch.js.map +1 -0
  13. package/dist/agents-store.d.ts +6 -1
  14. package/dist/agents-store.d.ts.map +1 -1
  15. package/dist/agents-store.js +46 -4
  16. package/dist/agents-store.js.map +1 -1
  17. package/dist/auto-migrate-confirm.d.ts +43 -0
  18. package/dist/auto-migrate-confirm.d.ts.map +1 -0
  19. package/dist/auto-migrate-confirm.js +47 -0
  20. package/dist/auto-migrate-confirm.js.map +1 -0
  21. package/dist/clipboard.d.ts +68 -0
  22. package/dist/clipboard.d.ts.map +1 -0
  23. package/dist/clipboard.js +177 -0
  24. package/dist/clipboard.js.map +1 -0
  25. package/dist/config-editor.d.ts +42 -5
  26. package/dist/config-editor.d.ts.map +1 -1
  27. package/dist/config-editor.js +80 -27
  28. package/dist/config-editor.js.map +1 -1
  29. package/dist/config.d.ts +76 -17
  30. package/dist/config.d.ts.map +1 -1
  31. package/dist/config.js +143 -39
  32. package/dist/config.js.map +1 -1
  33. package/dist/conversation-commands/registry.d.ts +127 -4
  34. package/dist/conversation-commands/registry.d.ts.map +1 -1
  35. package/dist/conversation-commands/registry.js +93 -14
  36. package/dist/conversation-commands/registry.js.map +1 -1
  37. package/dist/conversation-persist-plugin.d.ts +0 -1
  38. package/dist/conversation-persist-plugin.d.ts.map +1 -1
  39. package/dist/conversation-persist-plugin.js +7 -16
  40. package/dist/conversation-persist-plugin.js.map +1 -1
  41. package/dist/conversation-repository.d.ts +79 -18
  42. package/dist/conversation-repository.d.ts.map +1 -1
  43. package/dist/conversation-repository.js +96 -30
  44. package/dist/conversation-repository.js.map +1 -1
  45. package/dist/cwd-tracker-plugin.d.ts.map +1 -1
  46. package/dist/cwd-tracker-plugin.js +22 -7
  47. package/dist/cwd-tracker-plugin.js.map +1 -1
  48. package/dist/format-time.d.ts +17 -6
  49. package/dist/format-time.d.ts.map +1 -1
  50. package/dist/format-time.js +32 -14
  51. package/dist/format-time.js.map +1 -1
  52. package/dist/handoff-service.d.ts +19 -0
  53. package/dist/handoff-service.d.ts.map +1 -0
  54. package/dist/handoff-service.js +85 -0
  55. package/dist/handoff-service.js.map +1 -0
  56. package/dist/hook.d.ts.map +1 -1
  57. package/dist/hook.js +69 -15
  58. package/dist/hook.js.map +1 -1
  59. package/dist/i18n/en-wizard.d.ts +8 -0
  60. package/dist/i18n/en-wizard.d.ts.map +1 -0
  61. package/dist/i18n/en-wizard.js +303 -0
  62. package/dist/i18n/en-wizard.js.map +1 -0
  63. package/dist/i18n/en.d.ts +7 -0
  64. package/dist/i18n/en.d.ts.map +1 -0
  65. package/dist/i18n/en.js +245 -0
  66. package/dist/i18n/en.js.map +1 -0
  67. package/dist/i18n/index.d.ts +57 -0
  68. package/dist/i18n/index.d.ts.map +1 -0
  69. package/dist/i18n/index.js +109 -0
  70. package/dist/i18n/index.js.map +1 -0
  71. package/dist/i18n/preferences.d.ts +37 -0
  72. package/dist/i18n/preferences.d.ts.map +1 -0
  73. package/dist/i18n/preferences.js +73 -0
  74. package/dist/i18n/preferences.js.map +1 -0
  75. package/dist/i18n/zh-wizard.d.ts +11 -0
  76. package/dist/i18n/zh-wizard.d.ts.map +1 -0
  77. package/dist/i18n/zh-wizard.js +306 -0
  78. package/dist/i18n/zh-wizard.js.map +1 -0
  79. package/dist/i18n/zh.d.ts +10 -0
  80. package/dist/i18n/zh.d.ts.map +1 -0
  81. package/dist/i18n/zh.js +249 -0
  82. package/dist/i18n/zh.js.map +1 -0
  83. package/dist/index.js +52 -23
  84. package/dist/index.js.map +1 -1
  85. package/dist/mcp-command.d.ts +32 -0
  86. package/dist/mcp-command.d.ts.map +1 -0
  87. package/dist/mcp-command.js +37 -0
  88. package/dist/mcp-command.js.map +1 -0
  89. package/dist/mode.d.ts +31 -4
  90. package/dist/mode.d.ts.map +1 -1
  91. package/dist/mode.js +42 -7
  92. package/dist/mode.js.map +1 -1
  93. package/dist/model-switch.d.ts +58 -0
  94. package/dist/model-switch.d.ts.map +1 -0
  95. package/dist/model-switch.js +62 -0
  96. package/dist/model-switch.js.map +1 -0
  97. package/dist/reasoning-effort.d.ts +96 -0
  98. package/dist/reasoning-effort.d.ts.map +1 -0
  99. package/dist/reasoning-effort.js +164 -0
  100. package/dist/reasoning-effort.js.map +1 -0
  101. package/dist/reasoning-presets.d.ts +34 -0
  102. package/dist/reasoning-presets.d.ts.map +1 -0
  103. package/dist/reasoning-presets.js +85 -0
  104. package/dist/reasoning-presets.js.map +1 -0
  105. package/dist/sdk-log.d.ts +84 -0
  106. package/dist/sdk-log.d.ts.map +1 -0
  107. package/dist/sdk-log.js +167 -0
  108. package/dist/sdk-log.js.map +1 -0
  109. package/dist/session-pointer.d.ts.map +1 -1
  110. package/dist/session-pointer.js +10 -4
  111. package/dist/session-pointer.js.map +1 -1
  112. package/dist/stdio-runner.d.ts.map +1 -1
  113. package/dist/stdio-runner.js +32 -30
  114. package/dist/stdio-runner.js.map +1 -1
  115. package/dist/sub-agent-guard-plugin.d.ts +10 -7
  116. package/dist/sub-agent-guard-plugin.d.ts.map +1 -1
  117. package/dist/sub-agent-guard-plugin.js +12 -9
  118. package/dist/sub-agent-guard-plugin.js.map +1 -1
  119. package/dist/tui/chat-history.d.ts +29 -0
  120. package/dist/tui/chat-history.d.ts.map +1 -0
  121. package/dist/tui/chat-history.js +55 -0
  122. package/dist/tui/chat-history.js.map +1 -0
  123. package/dist/tui/chat-session.d.ts +144 -13
  124. package/dist/tui/chat-session.d.ts.map +1 -1
  125. package/dist/tui/chat-session.js +312 -73
  126. package/dist/tui/chat-session.js.map +1 -1
  127. package/dist/tui/components.d.ts +109 -6
  128. package/dist/tui/components.d.ts.map +1 -1
  129. package/dist/tui/components.js +177 -12
  130. package/dist/tui/components.js.map +1 -1
  131. package/dist/tui/config-wizard/detail.d.ts +18 -10
  132. package/dist/tui/config-wizard/detail.d.ts.map +1 -1
  133. package/dist/tui/config-wizard/detail.js +53 -46
  134. package/dist/tui/config-wizard/detail.js.map +1 -1
  135. package/dist/tui/config-wizard/engine.d.ts +2 -2
  136. package/dist/tui/config-wizard/engine.d.ts.map +1 -1
  137. package/dist/tui/config-wizard/engine.js +48 -26
  138. package/dist/tui/config-wizard/engine.js.map +1 -1
  139. package/dist/tui/config-wizard/fields.d.ts +3 -1
  140. package/dist/tui/config-wizard/fields.d.ts.map +1 -1
  141. package/dist/tui/config-wizard/fields.js +202 -167
  142. package/dist/tui/config-wizard/fields.js.map +1 -1
  143. package/dist/tui/config-wizard/frame.js +1 -1
  144. package/dist/tui/config-wizard/kv-editor.d.ts.map +1 -1
  145. package/dist/tui/config-wizard/kv-editor.js +13 -5
  146. package/dist/tui/config-wizard/kv-editor.js.map +1 -1
  147. package/dist/tui/config-wizard/list.d.ts +3 -1
  148. package/dist/tui/config-wizard/list.d.ts.map +1 -1
  149. package/dist/tui/config-wizard/list.js +4 -4
  150. package/dist/tui/config-wizard/list.js.map +1 -1
  151. package/dist/tui/config-wizard/mcp-helpers.d.ts.map +1 -1
  152. package/dist/tui/config-wizard/mcp-helpers.js +4 -3
  153. package/dist/tui/config-wizard/mcp-helpers.js.map +1 -1
  154. package/dist/tui/config-wizard/menu.d.ts.map +1 -1
  155. package/dist/tui/config-wizard/menu.js +33 -21
  156. package/dist/tui/config-wizard/menu.js.map +1 -1
  157. package/dist/tui/config-wizard/prompt-editor.d.ts +8 -1
  158. package/dist/tui/config-wizard/prompt-editor.d.ts.map +1 -1
  159. package/dist/tui/config-wizard/prompt-editor.js +11 -7
  160. package/dist/tui/config-wizard/prompt-editor.js.map +1 -1
  161. package/dist/tui/config-wizard/prompt.d.ts.map +1 -1
  162. package/dist/tui/config-wizard/prompt.js +8 -7
  163. package/dist/tui/config-wizard/prompt.js.map +1 -1
  164. package/dist/tui/config-wizard/step-agents.d.ts.map +1 -1
  165. package/dist/tui/config-wizard/step-agents.js +50 -29
  166. package/dist/tui/config-wizard/step-agents.js.map +1 -1
  167. package/dist/tui/config-wizard/step-defaults.d.ts.map +1 -1
  168. package/dist/tui/config-wizard/step-defaults.js +77 -27
  169. package/dist/tui/config-wizard/step-defaults.js.map +1 -1
  170. package/dist/tui/config-wizard/step-endpoints.d.ts.map +1 -1
  171. package/dist/tui/config-wizard/step-endpoints.js +27 -14
  172. package/dist/tui/config-wizard/step-endpoints.js.map +1 -1
  173. package/dist/tui/config-wizard/step-mcp.d.ts.map +1 -1
  174. package/dist/tui/config-wizard/step-mcp.js +29 -25
  175. package/dist/tui/config-wizard/step-mcp.js.map +1 -1
  176. package/dist/tui/config-wizard/step-models.d.ts.map +1 -1
  177. package/dist/tui/config-wizard/step-models.js +98 -43
  178. package/dist/tui/config-wizard/step-models.js.map +1 -1
  179. package/dist/tui/config-wizard/step-new-endpoint.d.ts.map +1 -1
  180. package/dist/tui/config-wizard/step-new-endpoint.js +16 -12
  181. package/dist/tui/config-wizard/step-new-endpoint.js.map +1 -1
  182. package/dist/tui/config-wizard/step-simple.d.ts.map +1 -1
  183. package/dist/tui/config-wizard/step-simple.js +18 -17
  184. package/dist/tui/config-wizard/step-simple.js.map +1 -1
  185. package/dist/tui/config-wizard/types.d.ts +31 -3
  186. package/dist/tui/config-wizard/types.d.ts.map +1 -1
  187. package/dist/tui/config-wizard/types.js.map +1 -1
  188. package/dist/tui/file-index.d.ts +38 -0
  189. package/dist/tui/file-index.d.ts.map +1 -0
  190. package/dist/tui/file-index.js +117 -0
  191. package/dist/tui/file-index.js.map +1 -0
  192. package/dist/tui/index.d.ts +17 -1
  193. package/dist/tui/index.d.ts.map +1 -1
  194. package/dist/tui/index.js +105 -49
  195. package/dist/tui/index.js.map +1 -1
  196. package/dist/tui/input-editing.d.ts +52 -0
  197. package/dist/tui/input-editing.d.ts.map +1 -0
  198. package/dist/tui/input-editing.js +91 -0
  199. package/dist/tui/input-editing.js.map +1 -0
  200. package/dist/tui/key-debug.d.ts +62 -0
  201. package/dist/tui/key-debug.d.ts.map +1 -0
  202. package/dist/tui/key-debug.js +92 -0
  203. package/dist/tui/key-debug.js.map +1 -0
  204. package/dist/tui/mention.d.ts +47 -0
  205. package/dist/tui/mention.d.ts.map +1 -0
  206. package/dist/tui/mention.js +152 -0
  207. package/dist/tui/mention.js.map +1 -0
  208. package/dist/tui/mouse.d.ts +55 -0
  209. package/dist/tui/mouse.d.ts.map +1 -0
  210. package/dist/tui/mouse.js +74 -0
  211. package/dist/tui/mouse.js.map +1 -0
  212. package/dist/tui/phase.d.ts +64 -0
  213. package/dist/tui/phase.d.ts.map +1 -0
  214. package/dist/tui/phase.js +112 -0
  215. package/dist/tui/phase.js.map +1 -0
  216. package/dist/tui/screens/agent-tabs.d.ts +53 -0
  217. package/dist/tui/screens/agent-tabs.d.ts.map +1 -0
  218. package/dist/tui/screens/agent-tabs.js +81 -0
  219. package/dist/tui/screens/agent-tabs.js.map +1 -0
  220. package/dist/tui/screens/chat-commands.d.ts +78 -1
  221. package/dist/tui/screens/chat-commands.d.ts.map +1 -1
  222. package/dist/tui/screens/chat-commands.js +688 -157
  223. package/dist/tui/screens/chat-commands.js.map +1 -1
  224. package/dist/tui/screens/chat.d.ts +57 -1
  225. package/dist/tui/screens/chat.d.ts.map +1 -1
  226. package/dist/tui/screens/chat.js +708 -261
  227. package/dist/tui/screens/chat.js.map +1 -1
  228. package/dist/tui/screens/common-keys.d.ts +36 -0
  229. package/dist/tui/screens/common-keys.d.ts.map +1 -0
  230. package/dist/tui/screens/common-keys.js +67 -0
  231. package/dist/tui/screens/common-keys.js.map +1 -0
  232. package/dist/tui/screens/frame-buffer.d.ts.map +1 -1
  233. package/dist/tui/screens/frame-buffer.js.map +1 -1
  234. package/dist/tui/screens/handoff-choice.d.ts +37 -0
  235. package/dist/tui/screens/handoff-choice.d.ts.map +1 -0
  236. package/dist/tui/screens/handoff-choice.js +42 -0
  237. package/dist/tui/screens/handoff-choice.js.map +1 -0
  238. package/dist/tui/screens/index.d.ts +7 -5
  239. package/dist/tui/screens/index.d.ts.map +1 -1
  240. package/dist/tui/screens/index.js +6 -5
  241. package/dist/tui/screens/index.js.map +1 -1
  242. package/dist/tui/screens/input-row.d.ts +64 -0
  243. package/dist/tui/screens/input-row.d.ts.map +1 -0
  244. package/dist/tui/screens/input-row.js +67 -0
  245. package/dist/tui/screens/input-row.js.map +1 -0
  246. package/dist/tui/screens/input.d.ts +77 -1
  247. package/dist/tui/screens/input.d.ts.map +1 -1
  248. package/dist/tui/screens/input.js +125 -8
  249. package/dist/tui/screens/input.js.map +1 -1
  250. package/dist/tui/screens/layout.d.ts +49 -0
  251. package/dist/tui/screens/layout.d.ts.map +1 -1
  252. package/dist/tui/screens/layout.js +45 -7
  253. package/dist/tui/screens/layout.js.map +1 -1
  254. package/dist/tui/screens/lines.d.ts +7 -0
  255. package/dist/tui/screens/lines.d.ts.map +1 -1
  256. package/dist/tui/screens/lines.js +6 -6
  257. package/dist/tui/screens/lines.js.map +1 -1
  258. package/dist/tui/screens/messages-view.d.ts +20 -1
  259. package/dist/tui/screens/messages-view.d.ts.map +1 -1
  260. package/dist/tui/screens/messages-view.js +55 -8
  261. package/dist/tui/screens/messages-view.js.map +1 -1
  262. package/dist/tui/screens/notice-bar.d.ts +5 -0
  263. package/dist/tui/screens/notice-bar.d.ts.map +1 -0
  264. package/dist/tui/screens/notice-bar.js +21 -0
  265. package/dist/tui/screens/notice-bar.js.map +1 -0
  266. package/dist/tui/screens/notice.d.ts +44 -10
  267. package/dist/tui/screens/notice.d.ts.map +1 -1
  268. package/dist/tui/screens/notice.js +75 -15
  269. package/dist/tui/screens/notice.js.map +1 -1
  270. package/dist/tui/screens/overlays.d.ts.map +1 -1
  271. package/dist/tui/screens/overlays.js +4 -3
  272. package/dist/tui/screens/overlays.js.map +1 -1
  273. package/dist/tui/screens/render.d.ts +1 -1
  274. package/dist/tui/screens/render.d.ts.map +1 -1
  275. package/dist/tui/screens/render.js +18 -11
  276. package/dist/tui/screens/render.js.map +1 -1
  277. package/dist/tui/screens/use-chat-viewport.d.ts +22 -0
  278. package/dist/tui/screens/use-chat-viewport.d.ts.map +1 -0
  279. package/dist/tui/screens/use-chat-viewport.js +50 -0
  280. package/dist/tui/screens/use-chat-viewport.js.map +1 -0
  281. package/dist/tui/screens/use-input-editing.d.ts +69 -0
  282. package/dist/tui/screens/use-input-editing.d.ts.map +1 -0
  283. package/dist/tui/screens/use-input-editing.js +220 -0
  284. package/dist/tui/screens/use-input-editing.js.map +1 -0
  285. package/dist/tui/screens/use-overlays.d.ts +25 -0
  286. package/dist/tui/screens/use-overlays.d.ts.map +1 -0
  287. package/dist/tui/screens/use-overlays.js +32 -0
  288. package/dist/tui/screens/use-overlays.js.map +1 -0
  289. package/dist/tui/screens/use-wizard-host.d.ts +46 -0
  290. package/dist/tui/screens/use-wizard-host.d.ts.map +1 -0
  291. package/dist/tui/screens/use-wizard-host.js +123 -0
  292. package/dist/tui/screens/use-wizard-host.js.map +1 -0
  293. package/dist/tui/screens/welcome.d.ts +12 -0
  294. package/dist/tui/screens/welcome.d.ts.map +1 -0
  295. package/dist/tui/screens/welcome.js +36 -0
  296. package/dist/tui/screens/welcome.js.map +1 -0
  297. package/dist/tui/status-segments.d.ts +37 -0
  298. package/dist/tui/status-segments.d.ts.map +1 -0
  299. package/dist/tui/status-segments.js +166 -0
  300. package/dist/tui/status-segments.js.map +1 -0
  301. package/dist/tui/text.d.ts +22 -7
  302. package/dist/tui/text.d.ts.map +1 -1
  303. package/dist/tui/text.js +110 -39
  304. package/dist/tui/text.js.map +1 -1
  305. package/dist/tui/theme.d.ts +2 -0
  306. package/dist/tui/theme.d.ts.map +1 -1
  307. package/dist/tui/theme.js +2 -0
  308. package/dist/tui/theme.js.map +1 -1
  309. package/dist/tui/token-rate.d.ts +83 -0
  310. package/dist/tui/token-rate.d.ts.map +1 -0
  311. package/dist/tui/token-rate.js +195 -0
  312. package/dist/tui/token-rate.js.map +1 -0
  313. package/dist/types.d.ts +7 -8
  314. package/dist/types.d.ts.map +1 -1
  315. package/dist/user-content.d.ts +41 -0
  316. package/dist/user-content.d.ts.map +1 -0
  317. package/dist/user-content.js +37 -0
  318. package/dist/user-content.js.map +1 -0
  319. package/dist/version-compare.d.ts +37 -0
  320. package/dist/version-compare.d.ts.map +1 -0
  321. package/dist/version-compare.js +105 -0
  322. package/dist/version-compare.js.map +1 -0
  323. package/dist/version.js.map +1 -1
  324. package/package.json +7 -5
  325. package/dist/auto-migrate-confirm-plugin.d.ts +0 -58
  326. package/dist/auto-migrate-confirm-plugin.d.ts.map +0 -1
  327. package/dist/auto-migrate-confirm-plugin.js +0 -61
  328. package/dist/auto-migrate-confirm-plugin.js.map +0 -1
  329. package/dist/migration-service.d.ts +0 -15
  330. package/dist/migration-service.d.ts.map +0 -1
  331. package/dist/migration-service.js +0 -66
  332. package/dist/migration-service.js.map +0 -1
  333. package/dist/tui/screens/splash.d.ts +0 -5
  334. package/dist/tui/screens/splash.d.ts.map +0 -1
  335. package/dist/tui/screens/splash.js +0 -51
  336. package/dist/tui/screens/splash.js.map +0 -1
@@ -1,175 +1,706 @@
1
1
  import { AgentNS } from "@ai-zen/agents-core";
2
- import { spawnSync } from "node:child_process";
2
+ import { HandoffAbortedError } from "@ai-zen/agents-sdk";
3
+ import { spawn, spawnSync } from "node:child_process";
3
4
  import { readFileSync, unlinkSync, writeFileSync } from "node:fs";
4
5
  import { tmpdir } from "node:os";
5
6
  import { join } from "node:path";
6
7
  import { stripCwdNote } from "../chat-session.js";
7
- import { matchCommandHints } from "../../conversation-commands/registry.js";
8
- import { conversationRepository, listConversations } from "../../conversation-repository.js";
9
- import { formatShortTime } from "../../format-time.js";
8
+ import { canonicalCommandName, matchCommandHints, parseCommandArgs, } from "../../conversation-commands/registry.js";
9
+ import { conversationExists, conversationRepository, listConversations } from "../../conversation-repository.js";
10
+ import { mcpConfigOrEmpty, readConfig, readMcpConfig, readReasoningPresets, saveConfig } from "../../config.js";
11
+ import { resetScope } from "../../agent-creator.js";
12
+ import { switchModel } from "../../model-switch.js";
13
+ import { listAgentChoices, switchAgent } from "../../agent-switch.js";
14
+ import { parseMcpScope, pickStartScope } from "../../mcp-command.js";
15
+ import { effortChoices, effortLabel, describeEffort, describeEffortView, setAgentEffort, } from "../../reasoning-effort.js";
16
+ import { matchLevel, presetFor } from "../../reasoning-presets.js";
17
+ import { getLocale, t } from "../../i18n/index.js";
18
+ import { persistLocale } from "../../i18n/preferences.js";
19
+ import { readAgentStore } from "../../agents-store.js";
20
+ import { CLI_VERSION } from "../../version.js";
21
+ import { compareVersions, pickNewest } from "../../version-compare.js";
10
22
  import { truncate } from "./render.js";
11
- export async function runCommand(name, raw, host) {
12
- const { session, notice, clearTranscript, rows, exit, suspendTerminal, openWizard, openKeyEditor, doExit, setPickerState, setConfirmState, setInputText, } = host;
13
- const push = (text, tone = "info") => {
14
- notice(text, tone);
15
- };
16
- switch (name) {
17
- case "config":
18
- await openWizard({ kind: "menu" });
19
- break;
20
- case "key":
21
- await openKeyEditor();
22
- break;
23
- case "help": {
24
- const lines = ["可用命令:"];
25
- for (const hint of matchCommandHints("")) {
26
- lines.push(` ${hint.label} ${hint.description}`);
23
+ import { STATUS_BAR_LINES, pickerLines } from "./layout.js";
24
+ import { usableFrameRows } from "../text.js";
25
+ import { chooseHandoffMode } from "./handoff-choice.js";
26
+ /**
27
+ * 解析命令后的执行者参数(`/migrate self`、`/handoff summary`)。
28
+ * 缺省返回 undefined(走服务默认值);非法值也返回 undefined,由调用方报错。
29
+ */
30
+ export function parseHandoffMode(raw) {
31
+ const value = raw?.trim().toLowerCase();
32
+ return value === "self" || value === "summary" ? value : undefined;
33
+ }
34
+ /**
35
+ * 解析 `/handoff` `/migrate` 的执行者参数。
36
+ *
37
+ * 单独成函数而不是挂在 `CommandContext` 上:16 个命令里只有这两个用得到,
38
+ * 挂在上下文中会让另外 14 个命令(`/save` `/help` `/clear` …)每次也白算一遍。
39
+ */
40
+ export function parseHandoffSelection(args) {
41
+ const raw = args[0]?.toLowerCase();
42
+ const mode = parseHandoffMode(raw);
43
+ return { mode, invalid: raw !== undefined && mode === undefined ? raw : undefined };
44
+ }
45
+ // ==================== 各命令 ====================
46
+ /**
47
+ * `/save [名字]`
48
+ *
49
+ * 无参 → 时间戳 id(维持原行为)
50
+ * 带名 → 另存为该文件名;重名且不是当前会话时**先二次确认**(静默覆盖会让另一份
51
+ * 对话凭空消失,而文件名是用户唯一能看到的身份)
52
+ *
53
+ * 名字的合法性(路径穿越等)不在这里校验:会话仓储的 `path()` 是唯一落盘口,
54
+ * L2 修好后 `/save ../x` 会被那一层拦住,此处不另写一套。
55
+ */
56
+ const cmdSave = async ({ session, push, args, confirm }) => {
57
+ const name = args[0]?.trim();
58
+ try {
59
+ if (!name) {
60
+ const id = await session.save();
61
+ push(t("cmd.save.done", { id }));
62
+ return;
63
+ }
64
+ if (name !== session.currentId && (await conversationExists(name))) {
65
+ if (!(await confirm(t("cmd.save.overwrite", { id: name })))) {
66
+ push(t("cmd.save.cancelled"));
67
+ return;
27
68
  }
28
- push(lines.join("\n"));
29
- break;
30
69
  }
31
- case "clear":
32
- // 清屏但保留上下文:仅把转录显示起点推到最新
33
- clearTranscript();
34
- break;
35
- case "new": {
36
- // reset 会先归档当前对话(有内容才存),返回存档 id(无内容为 null)
37
- const archivedId = await session.reset();
38
- clearTranscript();
39
- push(archivedId ? `旧对话已存档:${archivedId}\n已开始新对话` : "已开始新对话");
40
- break;
70
+ const id = await session.saveAs(name);
71
+ push(t("cmd.save.done", { id }));
72
+ }
73
+ catch (error) {
74
+ push(t("cmd.save.failed", { error: error?.message ?? error }), "error");
75
+ }
76
+ };
77
+ const cmdLoad = async ({ push, rows, exit, pick }) => {
78
+ // 条数上限:用布局自己的公式算,不留魔数 —— 命令层拿到的是**原始终端行数**,而
79
+ // 覆盖层能用的是 `usableFrameRows(rows) = rows - 1`;picker 自身占
80
+ // `pickerLines(n) + STATUS_BAR_LINES`。
81
+ const maxItems = Math.max(1, usableFrameRows(rows ?? 24) - (pickerLines(0) + STATUS_BAR_LINES));
82
+ const list = await listConversations();
83
+ if (list.length === 0) {
84
+ push(t("cmd.load.none"), "error");
85
+ return;
86
+ }
87
+ // 列表只显示会话文件名(id):不再解析首条消息当摘要(读盘开销已从 /load 消失)
88
+ const allItems = list.map((c) => ({ label: c.id, value: c.id }));
89
+ // 覆盖层不能高于终端:仅展示最近的有限条
90
+ const items = allItems.slice(0, maxItems);
91
+ const title = allItems.length > maxItems ? t("cmd.load.tooMany", { n: maxItems }) : t("cmd.load.titleReplacing");
92
+ const choice = await pick(title, items);
93
+ if (choice == null) {
94
+ push(t("cmd.load.cancelled"));
95
+ return;
96
+ }
97
+ const conversation = await conversationRepository().read(choice);
98
+ if (!conversation) {
99
+ push(t("cmd.load.readFailed", { id: choice }), "error");
100
+ return;
101
+ }
102
+ // 整体切换会话(含模型 / Agent)——交由上层重挂载对话屏
103
+ exit({
104
+ load: {
105
+ agentId: conversation.agentId,
106
+ messages: conversation.messages,
107
+ conversationId: conversation.id,
108
+ conversationName: conversation.id,
109
+ },
110
+ });
111
+ };
112
+ const cmdNew = async ({ session, clearTranscript, push }) => {
113
+ // reset 会先归档当前对话(有内容才存),返回存档 id(无内容为 null)
114
+ const archivedId = await session.reset();
115
+ clearTranscript();
116
+ push(archivedId ? t("cmd.new.archived", { id: archivedId }) : t("cmd.new.started"));
117
+ };
118
+ const cmdBack = async ({ session, push, rows, exit, pick }) => {
119
+ const allItems = [];
120
+ session.messages.forEach((message, index) => {
121
+ if (message.role !== AgentNS.Role.User)
122
+ return;
123
+ const content = typeof message.content === "string" ? message.content : "";
124
+ allItems.push({ label: truncate(stripCwdNote(content), 60), value: String(index) });
125
+ });
126
+ if (allItems.length === 0) {
127
+ push(t("cmd.back.empty"), "error");
128
+ return;
129
+ }
130
+ // 最近的在最前;仅展示最近的有限条,避免覆盖层高于终端(上界同 /load)
131
+ const maxItems = Math.max(1, usableFrameRows(rows ?? 24) - (pickerLines(0) + STATUS_BAR_LINES));
132
+ const items = allItems.reverse().slice(0, maxItems);
133
+ const title = allItems.length > maxItems ? t("cmd.back.tooMany", { n: maxItems }) : t("cmd.back.title");
134
+ const choice = await pick(title, items);
135
+ if (choice == null) {
136
+ push(t("cmd.back.cancelled"));
137
+ return;
138
+ }
139
+ const text = session.rollbackTo(Number(choice));
140
+ // 撤回也要落盘:否则存档里仍留着被撤回的消息,下次 `/load` 或重启就「复活」。
141
+ // `save()` 会顺带更新 last-session 指针 —— 刚撤回的会话就是本目录的上一轮,符合预期。
142
+ await session.save();
143
+ exit({ restart: true, messages: session.messages, initialInput: text });
144
+ };
145
+ const cmdEditor = async ({ push, suspendTerminal, setInputText }) => {
146
+ const file = join(tmpdir(), `aizen-input-${Date.now()}.md`);
147
+ try {
148
+ writeFileSync(file, "", "utf-8");
149
+ await suspendTerminal(() => {
150
+ const editor = process.env.EDITOR || process.env.VISUAL || (process.platform === "win32" ? "notepad" : "vi");
151
+ const result = spawnSync(editor, [file], { stdio: "inherit" });
152
+ if (result.error)
153
+ throw result.error;
154
+ });
155
+ const text = readFileSync(file, "utf-8").trim();
156
+ if (text)
157
+ setInputText(text);
158
+ }
159
+ catch (error) {
160
+ push(t("cmd.editor.failed", { error: error?.message ?? error }), "error");
161
+ }
162
+ finally {
163
+ try {
164
+ unlinkSync(file);
41
165
  }
42
- case "save": {
43
- try {
44
- const id = await session.save();
45
- push(`对话已保存:${id}`);
46
- }
47
- catch (error) {
48
- push(`保存失败:${error?.message ?? error}`, "error");
49
- }
50
- break;
166
+ catch {
167
+ /* 忽略 */
51
168
  }
52
- case "load": {
53
- const list = await listConversations();
54
- if (list.length === 0) {
55
- push("还没有已保存的对话", "error");
56
- break;
57
- }
58
- const allItems = list.map((c) => ({
59
- label: c.name,
60
- value: c.id,
61
- hint: formatShortTime(c.updatedAt),
62
- }));
63
- // 覆盖层不能高于终端:仅展示最近的有限条
64
- const maxItems = Math.max(1, (rows ?? 24) - 8);
65
- const items = allItems.slice(0, maxItems);
66
- const title = allItems.length > maxItems
67
- ? `选择要加载的对话(仅显示最近 ${maxItems} 条)`
68
- : "选择要加载的对话(当前会话将被替换)";
69
- const choice = await new Promise((resolve) => setPickerState({ title, items, resolve }));
70
- if (choice == null) {
71
- push("已取消加载");
72
- break;
73
- }
74
- const conversation = await conversationRepository.read(choice);
75
- if (!conversation) {
76
- push(`对话不存在或读取失败:${choice}`, "error");
77
- break;
78
- }
79
- // 整体切换会话(含模型 / Agent)——交由上层重挂载对话屏
80
- exit({
81
- load: {
82
- modelId: conversation.modelId,
83
- agentId: conversation.agentId,
84
- messages: conversation.messages,
85
- conversationId: conversation.id,
86
- conversationName: conversation.id,
87
- },
88
- });
89
- break;
169
+ }
170
+ };
171
+ const cmdClear = async ({ clearTranscript }) => {
172
+ // 清屏但保留上下文:仅把转录显示起点推到最新
173
+ clearTranscript();
174
+ };
175
+ const cmdConfig = async ({ openWizard }) => {
176
+ await openWizard({ kind: "menu" });
177
+ };
178
+ const cmdModel = async ({ session, push, args, exit, pick }) => {
179
+ const config = await readConfig();
180
+ const models = config.models ?? [];
181
+ if (models.length === 0) {
182
+ push(t("cmd.model.noModels"), "error");
183
+ return;
184
+ }
185
+ // 支持 `/model <id>` 直切,无参时弹选择器
186
+ let chosen = args[0];
187
+ if (chosen && !models.some((model) => model.id === chosen)) {
188
+ push(t("cmd.model.unknown", { id: chosen, options: models.map((m) => m.id).join(" / ") }), "error");
189
+ return;
190
+ }
191
+ if (!chosen) {
192
+ const items = models.map((model) => ({
193
+ label: model.id === session.modelId ? `${model.name}${t("common.current")}` : model.name,
194
+ value: model.id,
195
+ hint: model.id,
196
+ }));
197
+ const picked = await pick(t("cmd.model.title"), items);
198
+ if (picked == null) {
199
+ push(t("cmd.model.cancelled"));
200
+ return;
90
201
  }
91
- case "migrate": {
92
- if (!session.messages.some((m) => m.role === AgentNS.Role.User)) {
93
- push("当前对话还没有可迁移的内容", "error");
94
- break;
95
- }
96
- const ok = await new Promise((resolve) => setConfirmState({ question: "确定对当前对话进行任务迁移吗?", resolve }));
97
- if (!ok) {
98
- push("已取消迁移");
99
- break;
100
- }
101
- try {
102
- await session.migrate();
103
- push("任务迁移完成,已开启新会话");
104
- }
105
- catch (error) {
106
- push(`任务迁移失败:${error?.message ?? error}`, "error");
107
- }
108
- break;
202
+ chosen = picked;
203
+ }
204
+ if (chosen === session.modelId) {
205
+ push(t("cmd.model.already", { id: chosen }));
206
+ return;
207
+ }
208
+ // 先确认目标模型可用:重挂载走的是 ChatSession.create → createModel,
209
+ // 端点缺 Key 会直接抛错落到启动错误屏,用户连 /key 都进不去
210
+ const targetModel = models.find((model) => model.id === chosen);
211
+ const targetEndpoint = (config.endpoints ?? []).find((endpoint) => endpoint.id === targetModel.endpointId);
212
+ if (!targetEndpoint) {
213
+ push(t("cmd.model.endpointMissing", { id: chosen, endpoint: targetModel.endpointId }), "error");
214
+ return;
215
+ }
216
+ if (!targetEndpoint.apiKey) {
217
+ push(t("cmd.model.noApiKey", { name: targetEndpoint.name }), "error");
218
+ return;
219
+ }
220
+ try {
221
+ // 只写 Agent 定义(模型唯一真相源)
222
+ const result = await switchModel({ config, agentId: session.agentId, modelId: chosen });
223
+ // Scope 持有配置快照:不重建的话 getScope() 会把旧模型又交给新 Agent
224
+ await resetScope();
225
+ // 重挂载对话屏:模型变了要重算上下文阈值、重建 Agent;消息与上下文保留
226
+ exit({ restart: true, agentId: session.agentId, messages: session.messages, notice: result.summary });
227
+ }
228
+ catch (error) {
229
+ push(t("cmd.model.failed", { error: error?.message ?? error }), "error");
230
+ }
231
+ };
232
+ const cmdAgent = async ({ session, push, args, exit, pick }) => {
233
+ const config = await readConfig();
234
+ const store = await readAgentStore();
235
+ const choices = listAgentChoices(config, store.agents);
236
+ if (choices.length === 0) {
237
+ push(t("cmd.agent.noAgents"), "error");
238
+ return;
239
+ }
240
+ // 支持 `/agent <id>` 直切,无参时弹选择器(与 /model 同一套做法)
241
+ let chosen = args[0];
242
+ if (chosen && !choices.some((choice) => choice.id === chosen)) {
243
+ push(t("cmd.agent.unknown", { id: chosen, options: choices.map((choice) => choice.id).join(" / ") }), "error");
244
+ return;
245
+ }
246
+ if (!chosen) {
247
+ const items = choices.map((choice) => {
248
+ const current = choice.id === session.agentId;
249
+ return {
250
+ label: `${choice.name}${current ? t("common.current") : ""}${choice.selectable ? "" : t("cmd.agent.unavailable")}`,
251
+ value: choice.id,
252
+ // 可用时给模型名(切换等于连模型一起换,得让用户看见换成哪个);
253
+ // 不可用时给原因,否则用户只能干看着选不动
254
+ hint: choice.selectable ? (choice.modelName ?? choice.id) : (choice.reason ?? ""),
255
+ };
256
+ });
257
+ const picked = await pick(t("cmd.agent.title"), items);
258
+ if (picked == null) {
259
+ push(t("cmd.agent.cancelled"));
260
+ return;
109
261
  }
110
- case "back": {
111
- const allItems = [];
112
- session.messages.forEach((message, index) => {
113
- if (message.role !== AgentNS.Role.User)
114
- return;
115
- const content = typeof message.content === "string" ? message.content : "";
116
- allItems.push({ label: truncate(stripCwdNote(content), 60), value: String(index) });
117
- });
118
- if (allItems.length === 0) {
119
- push("还没有可撤回的消息", "error");
120
- break;
121
- }
122
- // 最近的在最前;仅展示最近的有限条,避免覆盖层高于终端
123
- const maxItems = Math.max(1, (rows ?? 24) - 8);
124
- const items = allItems.reverse().slice(0, maxItems);
125
- const title = allItems.length > maxItems
126
- ? `选择要撤回到的消息(仅显示最近 ${maxItems} 条)`
127
- : "选择要撤回到的消息(其后内容将被移除)";
128
- const choice = await new Promise((resolve) => setPickerState({ title, items, resolve }));
129
- if (choice == null) {
130
- push("已取消撤回");
131
- break;
132
- }
133
- const text = session.rollbackTo(Number(choice));
134
- exit({ restart: true, messages: session.messages, initialInput: text });
135
- break;
262
+ chosen = picked;
263
+ }
264
+ const target = choices.find((choice) => choice.id === chosen);
265
+ // 切过去跑不起来(未绑模型 / 端点缺 Key)就别切:重挂载后用户会卡在启动错误屏
266
+ if (!target.selectable) {
267
+ push(t("cmd.agent.unavailableReason", { name: target.name, reason: target.reason ?? "" }), "error");
268
+ return;
269
+ }
270
+ try {
271
+ const result = switchAgent({ config, agents: store.agents, targetAgentId: chosen });
272
+ await saveConfig(result.config);
273
+ if (chosen === session.agentId) {
274
+ // 会话已经在这个 Agent 上:只需把「默认 Agent」记号写对,不必重建
275
+ push(t("cmd.agent.alreadyOn", { summary: result.summary ?? "" }));
276
+ return;
136
277
  }
137
- case "editor": {
138
- const file = join(tmpdir(), `aizen-input-${Date.now()}.md`);
139
- try {
140
- writeFileSync(file, "", "utf-8");
141
- await suspendTerminal(() => {
142
- const editor = process.env.EDITOR ||
143
- process.env.VISUAL ||
144
- (process.platform === "win32" ? "notepad" : "vi");
145
- const result = spawnSync(editor, [file], { stdio: "inherit" });
146
- if (result.error)
147
- throw result.error;
148
- });
149
- const text = readFileSync(file, "utf-8").trim();
150
- if (text)
151
- setInputText(text);
152
- }
153
- catch (error) {
154
- push(`编辑器打开失败:${error?.message ?? error}`, "error");
155
- }
156
- finally {
157
- try {
158
- unlinkSync(file);
159
- }
160
- catch {
161
- /* 忽略 */
162
- }
163
- }
164
- break;
278
+ // Scope 持有 Agent / 配置快照:不重建的话下一轮请求还是旧 Agent
279
+ await resetScope();
280
+ // 重挂载对话屏:提示词、工具集、模型都跟着 Agent 变;消息与上下文保留
281
+ exit({ restart: true, agentId: chosen, messages: session.messages, notice: result.summary });
282
+ }
283
+ catch (error) {
284
+ push(t("cmd.agent.failed", { error: error?.message ?? error }), "error");
285
+ }
286
+ };
287
+ const cmdReload = async ({ session, push }) => {
288
+ // 手改配置文件(mcp.json / skills / tools / config.json)后不用重启 CLI:
289
+ // `session.reload()` 内含 resetScope()(断开旧 MCP 连接)→ 重建 Agent → 重新发现。
290
+ // 消息与上下文保留,所以「刷新一下就看到新东西」不需要新开对话。
291
+ try {
292
+ await session.reload();
293
+ push(t("cmd.reload.done"));
294
+ }
295
+ catch (error) {
296
+ push(t("cmd.reload.failed", { error: error?.message ?? error }), "error");
297
+ }
298
+ };
299
+ // ==================== /update ====================
300
+ /** `/update` 的安装目标包 */
301
+ const UPDATE_PACKAGE = "@ai-zen/cli";
302
+ /** npm 查询类调用的超时:断网 / registry 无响应时不能无限期挂着 */
303
+ const UPDATE_QUERY_TIMEOUT_MS = 20_000;
304
+ /**
305
+ * 跑一次 npm 并收集 stdout(**异步**)。
306
+ *
307
+ * 为什么不能用 `spawnSync`:`npm view` 走网络要 1–3 秒,同步执行会把事件循环钉死,
308
+ * Ink 的动画定时器跟着停摆 —— 用户看到的是一个**冻住的转圈**,既没有进度也不知道
309
+ * 在等什么。超时主动 kill,避免断网时命令永远不返回。
310
+ */
311
+ function runNpmCapture(args, spawnImpl = spawn) {
312
+ return new Promise((resolve) => {
313
+ let settled = false;
314
+ let timer;
315
+ const child = spawnImpl("npm", args, { stdio: ["ignore", "pipe", "ignore"] });
316
+ let stdout = "";
317
+ const done = (value) => {
318
+ if (settled)
319
+ return;
320
+ settled = true;
321
+ if (timer)
322
+ clearTimeout(timer);
323
+ resolve(value);
324
+ };
325
+ child.stdout?.on("data", (chunk) => {
326
+ stdout += chunk;
327
+ });
328
+ // `error` 与 `close` 可能都触发(`spawn` 失败时先 error 再 close),settled 保证只结算一次
329
+ child.on("error", (err) => done({ code: null, stdout, error: err.message }));
330
+ child.on("close", (code) => done({ code, stdout }));
331
+ timer = setTimeout(() => {
332
+ child.kill("SIGKILL");
333
+ done({ code: null, stdout, error: `timeout ${UPDATE_QUERY_TIMEOUT_MS}ms` });
334
+ }, UPDATE_QUERY_TIMEOUT_MS);
335
+ });
336
+ }
337
+ /**
338
+ * 读取包在 registry 上的全部 dist-tag(`{ latest, alpha, … }`)。
339
+ *
340
+ * 只拿 `latest` **不够**:本项目的实际发布状态就是「alpha 比正式版新」
341
+ * (曾经 `latest = 0.9.0`、`alpha = 1.0.0-alpha.23`),只认 `latest` 会把正在跑
342
+ * alpha 的用户**降级**回去。所以取全部 tag、再挑 semver 最大的那个。
343
+ *
344
+ * `--prefer-online`:npm 会缓存 packument,刚发版时 `npm view` 可能仍拿到旧 tag,
345
+ * 把「有新版」误判成「已是最新」(发布当天踩过这个坑)。
346
+ *
347
+ * 失败(npm 不存在 / 网络不通 / 输出不是 JSON)返回 `undefined`,由调用方报错。
348
+ */
349
+ async function readDistTags(pkg, spawnImpl) {
350
+ const result = await runNpmCapture(["view", pkg, "dist-tags", "--json", "--prefer-online"], spawnImpl);
351
+ if (result.code !== 0)
352
+ return undefined;
353
+ try {
354
+ const parsed = JSON.parse(result.stdout);
355
+ const tags = {};
356
+ for (const [tag, version] of Object.entries(parsed)) {
357
+ if (typeof version === "string")
358
+ tags[tag] = version;
359
+ }
360
+ return tags;
361
+ }
362
+ catch {
363
+ return undefined;
364
+ }
365
+ }
366
+ /** 全局安装目录(`npm root -g`);读不到返回 `undefined` */
367
+ async function readGlobalRoot(spawnImpl) {
368
+ const result = await runNpmCapture(["root", "-g"], spawnImpl);
369
+ const dir = result.stdout.trim();
370
+ return result.code === 0 && dir ? dir : undefined;
371
+ }
372
+ /**
373
+ * 读取**全局安装目录**里某个包的实际版本,读不到返回 `undefined`。
374
+ *
375
+ * 走 `npm root -g` + 读 `package.json`,而不是 `npm view`:后者查的是 registry,
376
+ * 刚装完的版本在 registry 上通常要几分钟才可见(发布经验,见交接文档),
377
+ * 拿它报「装好了没」会把成功误报成失败。
378
+ */
379
+ async function readGlobalInstalledVersion(pkg, spawnImpl) {
380
+ try {
381
+ const root = await readGlobalRoot(spawnImpl);
382
+ if (!root)
383
+ return undefined;
384
+ const parsed = JSON.parse(readFileSync(join(root, pkg, "package.json"), "utf-8"));
385
+ return typeof parsed.version === "string" ? parsed.version : undefined;
386
+ }
387
+ catch {
388
+ return undefined;
389
+ }
390
+ }
391
+ /**
392
+ * 安装(`stdio: inherit`,npm 的输出直接打在真实终端上)。成功返回 `undefined`,失败返回原因。
393
+ *
394
+ * `--prefer-online` 治 npm 的元数据缓存(否则刚发版的包可能解析失败 / 装到旧版本);
395
+ * `--no-fund` 只是减噪 —— 募捐提示对正在等结果的人没有信息量。
396
+ */
397
+ function installGlobal(spec, spawnImpl = spawn) {
398
+ return new Promise((resolve) => {
399
+ const child = spawnImpl("npm", ["install", "-g", spec, "--prefer-online", "--no-fund"], {
400
+ stdio: "inherit",
401
+ });
402
+ child.on("error", (err) => resolve(err.message));
403
+ child.on("close", (code) => resolve(code === 0 ? undefined : `exit ${code ?? "unknown"}`));
404
+ });
405
+ }
406
+ const cmdUpdate = async ({ push, suspendTerminal, confirm, restartProcess, session, spawn: spawnImpl, }) => {
407
+ // 每一步都说清「在干什么、看到什么」:只给一个转圈,无法区分「在查」和「在装」
408
+ push(t("cmd.update.checking", { pkg: UPDATE_PACKAGE }));
409
+ const tags = await readDistTags(UPDATE_PACKAGE, spawnImpl);
410
+ if (!tags) {
411
+ push(t("cmd.update.tagsFailed", { pkg: UPDATE_PACKAGE }), "error");
412
+ return;
413
+ }
414
+ // 全部 dist-tag 里 semver 最大的那个——不管是 alpha 还是正式版
415
+ const target = pickNewest(Object.values(tags));
416
+ if (!target) {
417
+ push(t("cmd.update.noVersion", { pkg: UPDATE_PACKAGE }), "error");
418
+ return;
419
+ }
420
+ // 不比当前进程新就不装:本地跑的开发版(或超前于 registry 的构建)不能被「升级」降级
421
+ const order = compareVersions(target, CLI_VERSION);
422
+ if (order !== null && order <= 0) {
423
+ push(t("cmd.update.same", { version: CLI_VERSION }));
424
+ return;
425
+ }
426
+ const spec = `${UPDATE_PACKAGE}@${target}`;
427
+ push(t("cmd.update.found", { target, current: CLI_VERSION }));
428
+ // 安装过程交给**真实终端**(`suspendTerminal` 期间 Ink 暂停渲染):npm 的进度条与
429
+ // 报错直接打在 TTY 上,比捕获后回放可读。移交之前先在 TTY 上打一段头部 ——
430
+ // 否则用户只看到 npm 自己的滚动输出,不知道装的是什么、从哪来、装到哪。
431
+ const root = await readGlobalRoot(spawnImpl);
432
+ let error;
433
+ await suspendTerminal(async () => {
434
+ process.stdout.write(t("cmd.update.installHeader", {
435
+ spec,
436
+ current: CLI_VERSION,
437
+ root: root ?? t("cmd.update.rootUnknown"),
438
+ }) + "\n");
439
+ error = await installGlobal(spec, spawnImpl);
440
+ });
441
+ if (error) {
442
+ push(t("cmd.update.failed", { spec, error }), "error");
443
+ return;
444
+ }
445
+ // 装完读全局目录里的版本:读不到(自建安装路径 / 别的包管理器)就按目标版本报
446
+ const installed = (await readGlobalInstalledVersion(UPDATE_PACKAGE, spawnImpl)) ?? target;
447
+ if (installed === CLI_VERSION) {
448
+ push(t("cmd.update.same", { version: installed }));
449
+ return;
450
+ }
451
+ push(t("cmd.update.done", { spec, current: CLI_VERSION, version: installed }));
452
+ // 新代码只在**新进程**里生效:本进程的模块图已经加载完了,连 `exit({ restart: true })`
453
+ // 都只是重挂载界面、跑的还是旧模块,所以「重启」必须是真的重新 exec 自己。
454
+ // 用二次确认而不是装完直接重启:否则「装好了什么」这句结果还没看清,画面就换掉了;
455
+ // 确认框默认停在「是」——敲 `/update` 的人本来就是要升级,一个回车就走完。
456
+ const ok = await confirm(t("cmd.update.confirmRestart", { current: CLI_VERSION, version: installed }));
457
+ if (!ok) {
458
+ push(t("cmd.update.restart", { current: CLI_VERSION }));
459
+ return;
460
+ }
461
+ // 重启前把当前对话落盘:新进程靠 `last-session.json` 指针续接,而指针正是落盘时更新的。
462
+ // 没有用户消息(刚开的新会话)就不落盘,免得留下一份空会话文件。
463
+ if (session.messages.some((m) => m.role === AgentNS.Role.User)) {
464
+ try {
465
+ await session.save();
466
+ }
467
+ catch {
468
+ // 落盘失败不阻断重启:ConversationPersistPlugin 每轮已落过一次,这里只是补最后一手
469
+ }
470
+ }
471
+ await restartProcess();
472
+ };
473
+ const cmdMcp = async ({ push, args, openWizard }) => {
474
+ // 不另建一套 MCP 编辑界面:直接打开配置中心里已有的 MCP 屏。
475
+ // 增 / 删 / 改 / 启停 / 切换作用域都在那一屏里;关闭后宿主会因 `mcpChanged`
476
+ // 自动 `session.reload()`,新服务器当场生效(与 /config 走同一条路径)。
477
+ const rawScope = args[0];
478
+ const explicit = parseMcpScope(rawScope);
479
+ if (rawScope && !explicit) {
480
+ push(t("cmd.mcp.unknownScope", { scope: rawScope }), "error");
481
+ return;
482
+ }
483
+ const scope = explicit ??
484
+ pickStartScope({
485
+ // 这里读只是为了「默认进哪个作用域」:坏文件按空配置算即可,
486
+ // 「已损坏」的提示由编辑屏负责(宿主那一轮会重新读并带上原因)
487
+ global: mcpConfigOrEmpty(await readMcpConfig("global")),
488
+ project: mcpConfigOrEmpty(await readMcpConfig("project")),
489
+ });
490
+ await openWizard({ kind: "mcp", scope });
491
+ };
492
+ const cmdLang = async ({ push, args, refreshLocale }) => {
493
+ // `/lang` 无参在中文 / 英文之间切;`/lang zh`、`/lang en` 直接指定。
494
+ // 状态栏、输入行、通知等界面文案都走 `t()`,但它是模块级函数 —— 语言变了
495
+ // 已渲染的内容不会自己更新,必须让 React 重跑一遍渲染函数(refreshLocale)。
496
+ const wanted = args[0]?.toLowerCase();
497
+ if (wanted && wanted !== "zh" && wanted !== "en") {
498
+ push(t("cmd.lang.unknown", { lang: wanted }), "error");
499
+ return;
500
+ }
501
+ const target = wanted ?? (getLocale() === "zh" ? "en" : "zh");
502
+ await persistLocale(target);
503
+ refreshLocale();
504
+ push(t("notice.languageChanged"));
505
+ };
506
+ const cmdThink = async ({ session, push, args, setEffortView, pick }) => {
507
+ const config = await readConfig();
508
+ const presets = await readReasoningPresets();
509
+ const store = await readAgentStore();
510
+ const definition = store.agents.find((item) => item.id === session.agentId) ?? null;
511
+ const modelId = definition?.modelId;
512
+ if (!modelId) {
513
+ push(t("cmd.think.noModel", { id: session.agentId }), "error");
514
+ return;
515
+ }
516
+ const model = (config.models ?? []).find((item) => item.id === modelId);
517
+ if (!model) {
518
+ push(t("cmd.think.modelMissing", { id: modelId }), "error");
519
+ return;
520
+ }
521
+ const choices = effortChoices(model, presets);
522
+ if (choices.length === 0) {
523
+ push(t("cmd.think.notAdjustable", { id: model.id }), "error");
524
+ return;
525
+ }
526
+ // 支持 `/think high` 直设、`/think default` 恢复跟随模型默认,无参时弹选择器
527
+ let picked = args[0];
528
+ if (picked !== undefined && picked !== "default" && !choices.includes(picked)) {
529
+ push(t("cmd.think.unknown", { level: picked, options: choices.join(" / ") }), "error");
530
+ return;
531
+ }
532
+ if (!picked) {
533
+ // 「当前」指 Agent 定义上真正写着的那一档(模型默认参数不算,那是跟随状态)
534
+ const current = matchLevel(presetFor(model.id, model.reasoningPreset, presets), definition?.modelParams);
535
+ const items = [
536
+ {
537
+ label: `${t("cmd.think.followDefault")}${current ? "" : t("common.current")}`,
538
+ value: "default",
539
+ hint: t("cmd.think.clearHint"),
540
+ },
541
+ ...choices.map((value) => ({
542
+ label: `${effortLabel(value)}${value === current ? t("common.current") : ""}`,
543
+ value,
544
+ hint: value,
545
+ })),
546
+ ];
547
+ const choice = await pick(t("cmd.think.title", { level: describeEffort(model, presets, definition) }), items);
548
+ if (choice == null) {
549
+ push(t("cmd.think.cancelled"));
550
+ return;
551
+ }
552
+ picked = choice;
553
+ }
554
+ const effort = picked === "default" ? null : picked;
555
+ const agentLevel = matchLevel(presetFor(model.id, model.reasoningPreset, presets), definition?.modelParams);
556
+ const unchanged = effort === null ? agentLevel === undefined : agentLevel === effort;
557
+ if (unchanged) {
558
+ push(t("cmd.think.unchanged"));
559
+ return;
560
+ }
561
+ try {
562
+ const result = await setAgentEffort({
563
+ config,
564
+ presets,
565
+ agentId: session.agentId,
566
+ effort,
567
+ // 已读过定义,避免命令层与写入层各读一次盘
568
+ readAgent: async () => definition,
569
+ });
570
+ // 状态栏先跟上;档位要通过重建 Agent 才会进请求参数
571
+ setEffortView(describeEffortView(result.model, presets, result.definition));
572
+ await session.reload();
573
+ push(result.summary);
574
+ }
575
+ catch (error) {
576
+ push(t("cmd.think.failed", { error: error?.message ?? error }), "error");
577
+ }
578
+ };
579
+ const cmdKey = async ({ openKeyEditor }) => {
580
+ await openKeyEditor();
581
+ };
582
+ const cmdHandoff = async ({ session, push, args, setPickerState }) => {
583
+ const handoff = parseHandoffSelection(args);
584
+ if (handoff.invalid !== undefined) {
585
+ push(t("cmd.handoff.unknownMode", { who: handoff.invalid }), "error");
586
+ return;
587
+ }
588
+ if (!session.messages.some((m) => m.role === AgentNS.Role.User)) {
589
+ push(t("cmd.handoff.empty"), "error");
590
+ return;
591
+ }
592
+ // 未显式指定执行者时三选一(与自动迁移统一);显式 `/handoff self|summary` 则直接采用
593
+ const chosen = handoff.mode ?? (await chooseHandoffMode(setPickerState, t("cmd.handoff.title")));
594
+ if (chosen == null) {
595
+ push(t("cmd.handoff.cancelled"));
596
+ return;
597
+ }
598
+ try {
599
+ const path = await session.handoff(chosen);
600
+ push(t("cmd.handoff.done", { path }));
601
+ }
602
+ catch (error) {
603
+ // 用户按 Ctrl+C 取消:不是失败,也不该报错堆栈
604
+ if (error instanceof HandoffAbortedError) {
605
+ push(t("cmd.handoff.cancelled"));
606
+ return;
165
607
  }
166
- case "exit":
167
- case "quit": {
168
- await doExit();
169
- break;
608
+ push(t("cmd.handoff.failed", { error: error?.message ?? error }), "error");
609
+ }
610
+ };
611
+ const cmdMigrate = async ({ session, push, args, setPickerState, clearTranscript }) => {
612
+ const handoff = parseHandoffSelection(args);
613
+ if (handoff.invalid !== undefined) {
614
+ push(t("cmd.handoff.unknownMode", { who: handoff.invalid }), "error");
615
+ return;
616
+ }
617
+ if (!session.messages.some((m) => m.role === AgentNS.Role.User)) {
618
+ push(t("cmd.migrate.empty"), "error");
619
+ return;
620
+ }
621
+ // 与原 yes/no 确认统一为三选一:选「不交接」即取消本次迁移
622
+ const chosen = handoff.mode ?? (await chooseHandoffMode(setPickerState, t("cmd.migrate.title")));
623
+ if (chosen == null) {
624
+ push(t("cmd.migrate.cancelled"));
625
+ return;
626
+ }
627
+ try {
628
+ const path = await session.migrate(chosen);
629
+ // 迁移用 `prune` 策略把 messages 剔除到「系统提示 + 交接断点」(仅 1~2 条),
630
+ // 转录显示起点必须跟着归零,否则 `messages.slice(transcriptFrom)` 恒为空——
631
+ // 迁移成功、上下文有内容,但屏幕上什么都没有(同 `/new` 分支的做法)。
632
+ clearTranscript();
633
+ push(t("cmd.migrate.done", { path }));
634
+ }
635
+ catch (error) {
636
+ // 用户按 Ctrl+C 取消:交接已停下且未改动会话,不是失败
637
+ if (error instanceof HandoffAbortedError) {
638
+ push(t("cmd.migrate.cancelled"));
639
+ return;
170
640
  }
171
- default:
172
- push(`未知命令:${raw}(输入 /help 查看可用命令)`, "error");
641
+ push(t("cmd.migrate.failed", { error: error?.message ?? error }), "error");
642
+ }
643
+ };
644
+ const cmdHelp = async ({ push }) => {
645
+ const lines = [t("cmd.help.title")];
646
+ for (const hint of matchCommandHints("")) {
647
+ lines.push(` ${hint.label} ${hint.description}`);
648
+ }
649
+ push(lines.join("\n"));
650
+ };
651
+ const cmdExit = async ({ doExit }) => {
652
+ await doExit();
653
+ };
654
+ /**
655
+ * 命令处理器表 —— 键是**主名**,类型保证与 `COMMAND_REGISTRY` 一一对应。
656
+ * 别名(`/effort`、`/quit`)由 `canonicalCommandName()` 归一后落到同一处理器。
657
+ */
658
+ const HANDLERS = {
659
+ save: cmdSave,
660
+ load: cmdLoad,
661
+ new: cmdNew,
662
+ back: cmdBack,
663
+ editor: cmdEditor,
664
+ clear: cmdClear,
665
+ config: cmdConfig,
666
+ model: cmdModel,
667
+ agent: cmdAgent,
668
+ reload: cmdReload,
669
+ update: cmdUpdate,
670
+ mcp: cmdMcp,
671
+ lang: cmdLang,
672
+ think: cmdThink,
673
+ key: cmdKey,
674
+ handoff: cmdHandoff,
675
+ migrate: cmdMigrate,
676
+ help: cmdHelp,
677
+ exit: cmdExit,
678
+ };
679
+ /** 组装执行上下文:参数解析、通知与选择器的统一入口 */
680
+ function buildContext(name, raw, host) {
681
+ return {
682
+ ...host,
683
+ name,
684
+ raw,
685
+ // 参数切分与命令名解析同源(registry 的 splitCommandInput),命令层不再自己切一遍
686
+ args: parseCommandArgs(raw),
687
+ push: (text, tone = "info") => host.notice(text, tone),
688
+ pick: (title, items) => new Promise((resolve) => host.setPickerState({ title, items, resolve })),
689
+ confirm: (question) => new Promise((resolve) => host.setConfirmState({ question, resolve })),
690
+ };
691
+ }
692
+ /**
693
+ * 执行一条斜杠命令。
694
+ *
695
+ * @param name 命令名(含别名,大小写不敏感;由 `parseCommandInput()` 从输入里取出)
696
+ * @param raw 原始输入(含 `/`),用于参数解析与错误回显
697
+ */
698
+ export async function runCommand(name, raw, host) {
699
+ const canonical = canonicalCommandName(name);
700
+ if (!canonical) {
701
+ host.notice(t("cmd.unknown", { cmd: raw }), "error");
702
+ return;
173
703
  }
704
+ await HANDLERS[canonical](buildContext(canonical, raw, host));
174
705
  }
175
706
  //# sourceMappingURL=chat-commands.js.map