synabun 2.0.5 → 2.0.7

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 (233) hide show
  1. package/CHANGELOG.md +40 -0
  2. package/README.md +2 -1
  3. package/THIRD-PARTY-LICENSES.md +13 -0
  4. package/docs/whatsapp.md +151 -16
  5. package/hooks/claude-code/shared.mjs +6 -3
  6. package/lib/child-path.js +303 -0
  7. package/lib/claude-hooks.js +2 -1
  8. package/lib/data-home-migration.js +2 -0
  9. package/lib/desktop-app.js +265 -0
  10. package/lib/external-tools.js +7 -3
  11. package/lib/log-redact.js +102 -0
  12. package/lib/packaged-runtime.js +138 -11
  13. package/mcp-server/dist/http.d.ts +3 -1
  14. package/mcp-server/dist/http.d.ts.map +1 -1
  15. package/mcp-server/dist/http.js +15 -4
  16. package/mcp-server/dist/http.js.map +1 -1
  17. package/mcp-server/dist/index.d.ts.map +1 -1
  18. package/mcp-server/dist/index.js +9 -6
  19. package/mcp-server/dist/index.js.map +1 -1
  20. package/mcp-server/dist/services/budget-judgments.d.ts +108 -0
  21. package/mcp-server/dist/services/budget-judgments.d.ts.map +1 -0
  22. package/mcp-server/dist/services/budget-judgments.js +153 -0
  23. package/mcp-server/dist/services/budget-judgments.js.map +1 -0
  24. package/mcp-server/dist/services/codex-config-heal.d.ts +5 -1
  25. package/mcp-server/dist/services/codex-config-heal.d.ts.map +1 -1
  26. package/mcp-server/dist/services/codex-config-heal.js +41 -4
  27. package/mcp-server/dist/services/codex-config-heal.js.map +1 -1
  28. package/mcp-server/dist/services/identity.d.ts +11 -0
  29. package/mcp-server/dist/services/identity.d.ts.map +1 -1
  30. package/mcp-server/dist/services/identity.js +24 -0
  31. package/mcp-server/dist/services/identity.js.map +1 -1
  32. package/mcp-server/dist/services/neural-interface.d.ts +13 -0
  33. package/mcp-server/dist/services/neural-interface.d.ts.map +1 -1
  34. package/mcp-server/dist/services/neural-interface.js +34 -2
  35. package/mcp-server/dist/services/neural-interface.js.map +1 -1
  36. package/mcp-server/dist/services/typesafe-config.d.ts +1 -1
  37. package/mcp-server/dist/services/typesafe-config.d.ts.map +1 -1
  38. package/mcp-server/dist/services/typesafe-config.js +4 -1
  39. package/mcp-server/dist/services/typesafe-config.js.map +1 -1
  40. package/mcp-server/dist/tools/agents.d.ts +10 -7
  41. package/mcp-server/dist/tools/agents.d.ts.map +1 -1
  42. package/mcp-server/dist/tools/agents.js +34 -13
  43. package/mcp-server/dist/tools/agents.js.map +1 -1
  44. package/mcp-server/dist/tools/whatsapp-send-file.d.ts +17 -0
  45. package/mcp-server/dist/tools/whatsapp-send-file.d.ts.map +1 -0
  46. package/mcp-server/dist/tools/whatsapp-send-file.js +44 -0
  47. package/mcp-server/dist/tools/whatsapp-send-file.js.map +1 -0
  48. package/mcp-server/dist/tui.js +2 -1
  49. package/mcp-server/dist/tui.js.map +1 -1
  50. package/mcp-server/dist/version.d.ts +5 -0
  51. package/mcp-server/dist/version.d.ts.map +1 -0
  52. package/mcp-server/dist/version.js +21 -0
  53. package/mcp-server/dist/version.js.map +1 -0
  54. package/mcp-server/package-lock.json +2 -2
  55. package/mcp-server/package.json +1 -1
  56. package/mcp-server/run.mjs +15 -9
  57. package/mcp-server/src/http.ts +18 -3
  58. package/mcp-server/src/index.ts +9 -6
  59. package/mcp-server/src/services/budget-judgments.ts +183 -0
  60. package/mcp-server/src/services/codex-config-heal.ts +35 -4
  61. package/mcp-server/src/services/identity.ts +27 -0
  62. package/mcp-server/src/services/neural-interface.ts +34 -2
  63. package/mcp-server/src/services/typesafe-config.ts +4 -1
  64. package/mcp-server/src/tools/agents.ts +33 -14
  65. package/mcp-server/src/tools/whatsapp-send-file.ts +51 -0
  66. package/mcp-server/src/tui.ts +2 -1
  67. package/mcp-server/src/version.ts +22 -0
  68. package/neural-interface/i18n/en.json +627 -57
  69. package/neural-interface/i18n/pt-BR.json +626 -56
  70. package/neural-interface/lib/account-usage.js +163 -0
  71. package/neural-interface/lib/assistant-api.js +255 -52
  72. package/neural-interface/lib/assistant-brains/claude.js +15 -2
  73. package/neural-interface/lib/assistant-brains/codex.js +3 -0
  74. package/neural-interface/lib/assistant-brains/opencode.js +17 -4
  75. package/neural-interface/lib/assistant-budget-planner.js +390 -0
  76. package/neural-interface/lib/assistant-budget-split.js +408 -0
  77. package/neural-interface/lib/assistant-budget.js +94 -95
  78. package/neural-interface/lib/assistant-config.js +62 -14
  79. package/neural-interface/lib/assistant-dispatch.js +432 -125
  80. package/neural-interface/lib/assistant-jev.js +26 -3
  81. package/neural-interface/lib/assistant-persona.js +68 -11
  82. package/neural-interface/lib/assistant-plan-permissions.js +8 -1
  83. package/neural-interface/lib/assistant-route-accounts.js +14 -0
  84. package/neural-interface/lib/assistant-route-gate.js +19 -3
  85. package/neural-interface/lib/assistant-router.js +373 -42
  86. package/neural-interface/lib/assistant-runtime.js +307 -47
  87. package/neural-interface/lib/assistant-user-writer.js +4 -2
  88. package/neural-interface/lib/augmented-path.js +317 -53
  89. package/neural-interface/lib/claude-accounts.js +342 -31
  90. package/neural-interface/lib/claude-agent-bridge.js +90 -3
  91. package/neural-interface/lib/claude-executable.js +6 -1
  92. package/neural-interface/lib/claude-model-catalog.js +3 -3
  93. package/neural-interface/lib/claude-panel-auth.js +221 -0
  94. package/neural-interface/lib/claude-profile-api-key.js +62 -0
  95. package/neural-interface/lib/cli-install-api.js +137 -0
  96. package/neural-interface/lib/cli-install.js +797 -0
  97. package/neural-interface/lib/cli-path.js +130 -0
  98. package/neural-interface/lib/cli-resolver.js +744 -0
  99. package/neural-interface/lib/cli-update-planner.js +143 -7
  100. package/neural-interface/lib/codex-account-auth.js +178 -0
  101. package/neural-interface/lib/codex-accounts.js +489 -0
  102. package/neural-interface/lib/codex-capabilities.js +2 -1
  103. package/neural-interface/lib/codex-compat-rpc.js +8 -2
  104. package/neural-interface/lib/codex-mcp-approvals.js +5 -1
  105. package/neural-interface/lib/codex-panel-auth.js +62 -0
  106. package/neural-interface/lib/codex-session-settings.js +1 -1
  107. package/neural-interface/lib/codex-settings-schema.js +120 -0
  108. package/neural-interface/lib/desktop/config.js +3 -0
  109. package/neural-interface/lib/git-bash/bzip2.js +339 -0
  110. package/neural-interface/lib/git-bash/claude-settings.js +73 -0
  111. package/neural-interface/lib/git-bash/extract-worker.js +34 -0
  112. package/neural-interface/lib/git-bash/install.js +529 -0
  113. package/neural-interface/lib/git-bash/licenses/COPYING +360 -0
  114. package/neural-interface/lib/git-bash/licenses/LGPL-2.1 +511 -0
  115. package/neural-interface/lib/git-bash/manager.js +268 -0
  116. package/neural-interface/lib/git-bash/pins.js +206 -0
  117. package/neural-interface/lib/git-bash/pins.json +34 -0
  118. package/neural-interface/lib/git-bash/resolver.js +194 -0
  119. package/neural-interface/lib/git-read.js +34 -2
  120. package/neural-interface/lib/install-details.js +145 -0
  121. package/neural-interface/lib/mcp-installer.js +14 -2
  122. package/neural-interface/lib/native-loop-providers.js +16 -3
  123. package/neural-interface/lib/native-loop-runtime.js +9 -4
  124. package/neural-interface/lib/os-path.js +319 -0
  125. package/neural-interface/lib/provider-panel-api-key-auth.js +92 -0
  126. package/neural-interface/lib/provider-panel-terminal-auth.js +95 -0
  127. package/neural-interface/lib/pty-host/core.js +2 -1
  128. package/neural-interface/lib/pty-host/manager.js +27 -2
  129. package/neural-interface/lib/remote-policy.js +20 -11
  130. package/neural-interface/lib/session-title-generator.js +4 -3
  131. package/neural-interface/lib/system-about.js +340 -0
  132. package/neural-interface/lib/system-activity.js +66 -0
  133. package/neural-interface/lib/terminal-env.js +11 -3
  134. package/neural-interface/lib/terminal-path.js +55 -0
  135. package/neural-interface/lib/virtual-socket.js +12 -1
  136. package/neural-interface/lib/whatsapp/baileys-adapter.js +165 -45
  137. package/neural-interface/lib/whatsapp/bridge.js +78 -34
  138. package/neural-interface/lib/whatsapp/commands.js +13 -14
  139. package/neural-interface/lib/whatsapp/config.js +134 -20
  140. package/neural-interface/lib/whatsapp/fake-baileys.js +99 -14
  141. package/neural-interface/lib/whatsapp/host-core.js +168 -35
  142. package/neural-interface/lib/whatsapp/inbound.js +5 -3
  143. package/neural-interface/lib/whatsapp/local-file.js +524 -0
  144. package/neural-interface/lib/whatsapp/manager.js +92 -32
  145. package/neural-interface/lib/whatsapp/paths.js +11 -1
  146. package/neural-interface/lib/whatsapp/protocol.js +65 -5
  147. package/neural-interface/lib/whatsapp/redact.js +63 -0
  148. package/neural-interface/lib/whatsapp/service.js +343 -91
  149. package/neural-interface/lib/whatsapp/transcription.js +463 -0
  150. package/neural-interface/package-lock.json +2 -2
  151. package/neural-interface/package.json +1 -1
  152. package/neural-interface/public/index.html +2 -0
  153. package/neural-interface/public/index2d.html +2 -0
  154. package/neural-interface/public/offline.html +16 -1
  155. package/neural-interface/public/onboarding.html +96 -2
  156. package/neural-interface/public/shared/account-i18n.js +7 -0
  157. package/neural-interface/public/shared/account-menu.js +539 -0
  158. package/neural-interface/public/shared/account-usage.js +19 -0
  159. package/neural-interface/public/shared/account-view.js +335 -0
  160. package/neural-interface/public/shared/agent-runtime-options.js +28 -8
  161. package/neural-interface/public/shared/assistant/asst-accounts.js +182 -184
  162. package/neural-interface/public/shared/assistant/asst-api.js +32 -2
  163. package/neural-interface/public/shared/assistant/asst-brain-picker.js +124 -25
  164. package/neural-interface/public/shared/assistant/asst-budget.js +259 -303
  165. package/neural-interface/public/shared/assistant/asst-mascot.js +126 -7
  166. package/neural-interface/public/shared/assistant/asst-menu.js +15 -3
  167. package/neural-interface/public/shared/assistant/asst-panel.js +48 -14
  168. package/neural-interface/public/shared/assistant/asst-render.js +38 -7
  169. package/neural-interface/public/shared/assistant/asst-route.js +72 -14
  170. package/neural-interface/public/shared/assistant/asst-routes-editor.js +45 -6
  171. package/neural-interface/public/shared/assistant/asst-slash.js +1 -1
  172. package/neural-interface/public/shared/assistant/asst-state.js +1 -1
  173. package/neural-interface/public/shared/assistant/asst-styles.js +99 -17
  174. package/neural-interface/public/shared/cdx/cdx-commands.js +4 -1
  175. package/neural-interface/public/shared/cdx/cdx-features.js +22 -14
  176. package/neural-interface/public/shared/cdx/cdx-icons.js +2 -0
  177. package/neural-interface/public/shared/cdx/cdx-panel.js +48 -107
  178. package/neural-interface/public/shared/cdx/cdx-settings.js +231 -0
  179. package/neural-interface/public/shared/cdx/cdx-styles.js +128 -107
  180. package/neural-interface/public/shared/cdx/cdx-tabs.js +261 -664
  181. package/neural-interface/public/shared/cdx/cdx-ws.js +2 -0
  182. package/neural-interface/public/shared/claude-account-view.js +3 -0
  183. package/neural-interface/public/shared/cli-install-view.js +392 -0
  184. package/neural-interface/public/shared/cli-status.js +282 -22
  185. package/neural-interface/public/shared/cp/cp-events.js +2 -1
  186. package/neural-interface/public/shared/cp/cp-styles.js +46 -0
  187. package/neural-interface/public/shared/desktop-bridge.js +39 -0
  188. package/neural-interface/public/shared/desktop-titlebar-page.js +59 -0
  189. package/neural-interface/public/shared/desktop-titlebar-view.js +122 -0
  190. package/neural-interface/public/shared/desktop-titlebar.js +153 -0
  191. package/neural-interface/public/shared/desktop-update-view.js +462 -0
  192. package/neural-interface/public/shared/editor/editor-panel.js +1 -1
  193. package/neural-interface/public/shared/explorer/fx-main.js +1 -1
  194. package/neural-interface/public/shared/git-bash-view.js +145 -0
  195. package/neural-interface/public/shared/html-shell.js +1 -0
  196. package/neural-interface/public/shared/i18n.js +19 -1
  197. package/neural-interface/public/shared/install-details-view.js +86 -0
  198. package/neural-interface/public/shared/ocp-v2/ocp-v2-panel.js +32 -0
  199. package/neural-interface/public/shared/ocp-v2/ocp-v2-send.js +17 -0
  200. package/neural-interface/public/shared/ocp-v2/ocp-v2-styles.js +118 -0
  201. package/neural-interface/public/shared/pwa-install-page.js +3 -1
  202. package/neural-interface/public/shared/settings/settings-about-pane.js +271 -0
  203. package/neural-interface/public/shared/settings/settings-about.js +254 -0
  204. package/neural-interface/public/shared/settings/settings-ia.js +15 -0
  205. package/neural-interface/public/shared/showcase-privacy.js +489 -0
  206. package/neural-interface/public/shared/storage.js +30 -3
  207. package/neural-interface/public/shared/styles.css +278 -31
  208. package/neural-interface/public/shared/synabun-mascot.js +48 -2
  209. package/neural-interface/public/shared/ui-claude-panel.js +212 -213
  210. package/neural-interface/public/shared/ui-cli-banner.js +355 -0
  211. package/neural-interface/public/shared/ui-git-bash.js +124 -0
  212. package/neural-interface/public/shared/ui-judgments.js +10 -1
  213. package/neural-interface/public/shared/ui-loading.js +30 -6
  214. package/neural-interface/public/shared/ui-menubar.js +13 -3
  215. package/neural-interface/public/shared/ui-navbar.js +3 -1
  216. package/neural-interface/public/shared/ui-provider-auth.js +104 -0
  217. package/neural-interface/public/shared/ui-provider-terminal-auth.js +223 -0
  218. package/neural-interface/public/shared/ui-sessions.js +9 -3
  219. package/neural-interface/public/shared/ui-settings.js +55 -3
  220. package/neural-interface/public/shared/ui-sidepanel-sessions.js +3 -1
  221. package/neural-interface/public/shared/ui-sync.js +6 -0
  222. package/neural-interface/public/shared/ui-terminal.js +17 -0
  223. package/neural-interface/public/shared/ui-tool-updates.js +5 -0
  224. package/neural-interface/public/shared/ui-update.js +594 -5
  225. package/neural-interface/public/shared/ui-whatsapp.js +21 -6
  226. package/neural-interface/public/shared/whatsapp/wa-view.js +51 -8
  227. package/neural-interface/public/shared/worker-end.js +2 -0
  228. package/neural-interface/public/sw.js +1 -1
  229. package/neural-interface/public/variant/2d/main.js +10 -1
  230. package/neural-interface/public/variant/3d/main.js +10 -1
  231. package/neural-interface/server.js +1219 -608
  232. package/package.json +4 -1
  233. package/setup.js +9 -8
@@ -201,12 +201,14 @@ function handleSocketMessage(msg, tab, callbacks) {
201
201
  case 'verification_canceled':
202
202
  case 'account_info':
203
203
  case 'account_login_started':
204
+ case 'account_login_state':
204
205
  case 'account_logged_out':
205
206
  case 'account_list':
206
207
  case 'account_switched':
207
208
  case 'account_add_started':
208
209
  case 'account_removed':
209
210
  case 'account_renamed':
211
+ case 'account_state':
210
212
  case 'config_data':
211
213
  case 'config_requirements':
212
214
  case 'experimental_features':
@@ -0,0 +1,3 @@
1
+ // The first name of the account view, kept for its callers: everything lives in
2
+ // account-view.js, one provider-neutral module for Claude and Codex accounts.
3
+ export * from './account-view.js';
@@ -0,0 +1,392 @@
1
+ // ═══════════════════════════════════════════
2
+ // SynaBun Neural Interface — the CLI banner of the side panels: what it says
3
+ // ═══════════════════════════════════════════
4
+ //
5
+ // What the Claude Code, Codex and OpenCode panels show above their transcript
6
+ // for one CLI: nothing while it is installed, "not installed" with a one-click
7
+ // Install (the vendor's own installer, run by the server: POST
8
+ // /api/cli/install/:tool) and the command to run by hand, the progress of an
9
+ // install, why an install failed, and "failed to start" with the error the CLI
10
+ // gave. One reading for all three panels, so they never disagree; the page
11
+ // side is ui-cli-banner.js, the data cli-status.js.
12
+ //
13
+ // No imports and no DOM: `node --test` loads this file as it is
14
+ // (tests/cli-install-view.test.mjs). CLI_INSTALL_COPY is the English of every
15
+ // sentence; `cliInstall.*` in i18n/en.json must equal it and i18n/pt-BR.json
16
+ // must have the same keys (the test checks both).
17
+ //
18
+ // A server from before the three states answers only `installed` (a version or
19
+ // null): null reads as "not installed", and no Install button is offered,
20
+ // because such a server has no install route.
21
+
22
+ export const CLI_INSTALL_COPY = Object.freeze({
23
+ titleMissing: '{label} is not installed',
24
+ titleBroken: '{label} failed to start',
25
+ titleInstalling: 'Installing {label}…',
26
+ titleFailed: '{label} could not be installed',
27
+ titleDone: '{label} is installed',
28
+ titlePath: 'Run {label} in your other terminals',
29
+ bodyPath: 'SynaBun terminals are ready. Add {dir} to your PATH for other terminals.',
30
+ addPath: 'Add to PATH',
31
+ addingPath: 'Adding…',
32
+ pathFailed: 'PATH could not be updated: {error}',
33
+ copyPathDiagnostics: 'Copy PATH diagnostics',
34
+ stepPath: 'Adding the CLI folder to your PATH…',
35
+ stepPathAdded: 'Added {dir} to your PATH',
36
+ bodyMissing: 'Install it with one click from {vendor}\'s official installer, or run:',
37
+ bodyMissingArchive: 'Install it with one click from {vendor}\'s official release, or run:',
38
+ bodyMissingManual: 'Install it with this command, then re-check:',
39
+ bodyBroken: '{label} was found at {path}, but it could not be started:',
40
+ bodyBrokenVersion: '{label} {version} was found at {path}, but it could not be started:',
41
+ bodyBrokenLatched: '{label} is installed, but it could not be started:',
42
+ bodyBrokenHint: 'Check the CLI path in Settings, then re-check.',
43
+ configuredFailed: 'The path set in Settings did not work: {error}',
44
+ bodyFailedManual: 'Run it yourself:',
45
+ doneNotOnPath: 'It is in {dir}. SynaBun finds it there; add that folder to your PATH to run it in a terminal.',
46
+ doneAddedToPath: 'Added {dir} to your PATH. New terminal windows and tabs pick it up.',
47
+ stepStart: 'Starting the official installer…',
48
+ stepDownload: 'Downloading…',
49
+ stepDownloadProgress: 'Downloading: {received} of {total} MB',
50
+ stepDownloadReceived: 'Downloading: {received} MB',
51
+ stepVerify: 'Checking the download…',
52
+ stepRun: 'Running the official installer…',
53
+ stepExtract: 'Unpacking…',
54
+ stepPlace: 'Putting it in place…',
55
+ stepCheck: 'Checking that it starts…',
56
+ errorOffline: 'The download server could not be reached. Check the connection and try again.',
57
+ errorHttp: 'The download server answered with an error. Try again in a moment.',
58
+ errorChecksum: 'The download did not match its published checksum, so it was removed and never run.',
59
+ errorInterrupted: 'The download stopped before it was complete. Nothing was kept. Try again.',
60
+ errorTimeout: 'The installer took too long and was stopped.',
61
+ errorInstallerFailed: 'The official installer stopped with an error.',
62
+ errorNotFound: 'The installer finished, but SynaBun does not find {label} afterwards.',
63
+ errorUnpack: 'The download could not be unpacked, so it was removed.',
64
+ errorDisk: 'The files could not be written. Check free space and permissions, then try again.',
65
+ errorCancelled: 'The installation was stopped.',
66
+ errorUnsupported: 'There is no one-click install for this system. Use the command below or the install guide.',
67
+ errorRefused: 'SynaBun refused the request. Install from SynaBun on this computer.',
68
+ errorConnection: 'The connection to SynaBun closed before the installation finished.',
69
+ errorUnknown: 'The installation did not finish.',
70
+ install: 'Install',
71
+ installing: 'Installing…',
72
+ retry: 'Try again',
73
+ guide: 'Install guide',
74
+ recheck: 'Re-check',
75
+ checking: 'Checking…',
76
+ copy: 'Copy',
77
+ copied: 'Copied',
78
+ dismiss: 'Dismiss',
79
+ logLabel: 'Installer output',
80
+ });
81
+
82
+ /** Who publishes each CLI: the {vendor} of "from {vendor}'s official installer". Names, not translated. */
83
+ export const CLI_VENDORS = Object.freeze({
84
+ 'claude-code': 'Anthropic',
85
+ codex: 'OpenAI',
86
+ opencode: 'OpenCode',
87
+ gemini: 'Google',
88
+ });
89
+
90
+ /** The CLIs a server can install with one click (POST /api/cli/install/:tool). */
91
+ export const INSTALLABLE_CLIS = Object.freeze(['claude-code', 'codex', 'opencode']);
92
+
93
+ /** The steps an install reports (`{ type: 'step' }`), in the order they can come. */
94
+ export const INSTALL_STEPS = Object.freeze(['start', 'download', 'verify', 'run', 'extract', 'place', 'check', 'path']);
95
+
96
+ const STEP_COPY = Object.freeze({
97
+ start: 'stepStart', download: 'stepDownload', verify: 'stepVerify', run: 'stepRun',
98
+ extract: 'stepExtract', place: 'stepPlace', check: 'stepCheck', path: 'stepPath',
99
+ });
100
+
101
+ const ERROR_COPY = Object.freeze({
102
+ offline: 'errorOffline', http: 'errorHttp', checksum: 'errorChecksum', interrupted: 'errorInterrupted',
103
+ timeout: 'errorTimeout', 'installer-failed': 'errorInstallerFailed', 'not-found-after-install': 'errorNotFound',
104
+ unpack: 'errorUnpack', disk: 'errorDisk', cancelled: 'errorCancelled', unsupported: 'errorUnsupported',
105
+ refused: 'errorRefused', connection: 'errorConnection',
106
+ });
107
+
108
+ /** How many installer lines the banner keeps, and shows. */
109
+ export const INSTALL_LOG_KEEP = 200;
110
+ export const INSTALL_LOG_SHOWN = 6;
111
+
112
+ /** A CLI_INSTALL_COPY sentence with its `{params}` filled in: the translator when the page gives none. */
113
+ export function fillCliInstallCopy(key, params = {}) {
114
+ return String(CLI_INSTALL_COPY[key] ?? key).replace(/\{(\w+)\}/g, (whole, name) => (params[name] != null ? params[name] : whole));
115
+ }
116
+
117
+ let _translate = null;
118
+
119
+ /**
120
+ * The page's translator: `(key, params) => t('cliInstall.' + key, params)`. An answer that is empty, or the
121
+ * key itself (i18n's word for "no such string"), falls back to the English.
122
+ */
123
+ export function setCliInstallTranslator(fn) {
124
+ _translate = typeof fn === 'function' ? fn : null;
125
+ }
126
+
127
+ /** A sentence in the page's language (the translator set above, else the English). */
128
+ export function cliInstallText(key, params = {}) {
129
+ if (_translate) {
130
+ try {
131
+ const text = _translate(key, params);
132
+ if (typeof text === 'string' && text && text !== key && text !== `cliInstall.${key}`) return text;
133
+ } catch { /* the English below */ }
134
+ }
135
+ return fillCliInstallCopy(key, params);
136
+ }
137
+
138
+ /**
139
+ * The state of one CLI in an answer of GET /api/system/tool-versions:
140
+ * 'installed' found, and it answered --version
141
+ * 'missing' nowhere to be found
142
+ * 'broken' found, but it would not start (info.error says why)
143
+ * 'unknown' no answer yet, or an answer that says nothing about this CLI
144
+ * An older server sends no `state`: its `installed` (a version or null) decides.
145
+ */
146
+ export function cliStateOf(info) {
147
+ if (!info || typeof info !== 'object') return 'unknown';
148
+ if (info.state === 'installed' || info.state === 'missing' || info.state === 'broken' || info.state === 'unknown') return info.state;
149
+ return info.installed ? 'installed' : 'missing';
150
+ }
151
+
152
+ /** The one-click install the server offers for this CLI on its system, or null. */
153
+ export function cliInstallOffer(info) {
154
+ const install = info && typeof info === 'object' && info.install && typeof info.install === 'object' ? info.install : null;
155
+ return install && install.available === true ? install : null;
156
+ }
157
+
158
+ const line = (value, max = 400) => {
159
+ const text = String(value ?? '').replace(/\s+$/g, '');
160
+ return text.length > max ? `${text.slice(0, max - 1)}…` : text;
161
+ };
162
+ const words = (value) => (typeof value === 'string' && value.trim() ? value.trim() : '');
163
+
164
+ /**
165
+ * One install as the page saw it, after one more event of the stream (or of the job list, when another window
166
+ * started it). Pure: a new object every time.
167
+ *
168
+ * @param {object | null} job the job so far (null: a new one)
169
+ * @param {object} event `{ type: 'start' | 'step' | 'progress' | 'log' | 'done', … }` as POST /api/cli/install/:tool sends
170
+ * @returns {{ phase: 'running' | 'failed' | 'done', step: string, received: number, total: number, log: string[],
171
+ * code: string | null, error: string | null, manualCommand: string | null, guideUrl: string | null,
172
+ * status: object | null, path: object | null, method: string | null, source: string | null }}
173
+ */
174
+ export function reduceInstallJob(job, event) {
175
+ const next = job && typeof job === 'object'
176
+ ? { ...job, log: Array.isArray(job.log) ? [...job.log] : [] }
177
+ : { phase: 'running', step: 'start', received: 0, total: 0, log: [], code: null, error: null, manualCommand: null, guideUrl: null, status: null, path: null, method: null, source: null };
178
+ const e = event && typeof event === 'object' ? event : {};
179
+ const push = (text) => {
180
+ for (const piece of String(text ?? '').split(/\r?\n/)) {
181
+ const kept = line(piece, 300);
182
+ if (kept.trim()) next.log.push(kept);
183
+ }
184
+ if (next.log.length > INSTALL_LOG_KEEP) next.log.splice(0, next.log.length - INSTALL_LOG_KEEP);
185
+ };
186
+ switch (e.type) {
187
+ case 'start':
188
+ next.phase = 'running';
189
+ next.step = 'start';
190
+ next.method = typeof e.method === 'string' ? e.method : next.method;
191
+ next.source = typeof e.source === 'string' ? e.source : next.source;
192
+ break;
193
+ case 'step':
194
+ if (INSTALL_STEPS.includes(e.step)) {
195
+ if (e.step !== next.step) { next.received = 0; next.total = 0; }
196
+ next.step = e.step;
197
+ }
198
+ break;
199
+ case 'progress':
200
+ next.received = Math.max(0, Number(e.received) || 0);
201
+ next.total = Math.max(0, Number(e.total) || 0);
202
+ break;
203
+ case 'log':
204
+ push(e.line);
205
+ break;
206
+ case 'path':
207
+ if (e.added && words(e.dir)) push(cliInstallText('stepPathAdded', { dir: e.dir }));
208
+ break;
209
+ case 'done':
210
+ if (e.ok === true) {
211
+ next.phase = 'done';
212
+ next.status = e.status && typeof e.status === 'object' ? e.status : null;
213
+ next.path = e.path && typeof e.path === 'object' ? e.path : null;
214
+ next.code = null;
215
+ next.error = null;
216
+ } else {
217
+ next.phase = 'failed';
218
+ next.code = typeof e.code === 'string' && e.code ? e.code : 'internal';
219
+ next.error = words(e.error) || null;
220
+ next.manualCommand = words(e.manualCommand) || null;
221
+ next.guideUrl = words(e.guideUrl) || null;
222
+ if (Array.isArray(e.log) && !next.log.length) for (const entry of e.log) push(entry);
223
+ }
224
+ break;
225
+ default:
226
+ break;
227
+ }
228
+ return next;
229
+ }
230
+
231
+ const megabytes = (bytes) => (Math.max(0, Number(bytes) || 0) / (1024 * 1024)).toFixed(1);
232
+
233
+ /** The sentence for where an install is, and how far a download got (null when unknown). */
234
+ export function installStepView(job, { t = cliInstallText } = {}) {
235
+ const step = job?.step && STEP_COPY[job.step] ? job.step : 'start';
236
+ if (step === 'download') {
237
+ const total = Number(job?.total) || 0;
238
+ const received = Number(job?.received) || 0;
239
+ if (total > 0) {
240
+ const percent = Math.max(0, Math.min(100, Math.floor((Math.min(received, total) / total) * 100)));
241
+ return { step, text: t('stepDownloadProgress', { received: megabytes(Math.min(received, total)), total: megabytes(total) }), percent };
242
+ }
243
+ if (received > 0) return { step, text: t('stepDownloadReceived', { received: megabytes(received) }), percent: null };
244
+ }
245
+ return { step, text: t(STEP_COPY[step]), percent: null };
246
+ }
247
+
248
+ /** Why an install failed, as a sentence; the server's own words go in `detail` when they add something. */
249
+ export function installErrorView(job, { label = '', t = cliInstallText } = {}) {
250
+ const code = job?.code || 'internal';
251
+ const key = ERROR_COPY[code];
252
+ const raw = words(job?.error);
253
+ if (key) return { text: t(key, { label }), detail: raw || null };
254
+ return { text: t('errorUnknown'), detail: raw || null };
255
+ }
256
+
257
+ /** The guide to link: the server's (an https address only), else the page's own. */
258
+ function safeUrl(...candidates) {
259
+ for (const value of candidates) {
260
+ if (typeof value === 'string' && /^https:\/\/[^\s"'<>]+$/i.test(value.trim())) return value.trim();
261
+ }
262
+ return null;
263
+ }
264
+
265
+ /**
266
+ * What the banner of one panel shows.
267
+ *
268
+ * @param {object} o
269
+ * @param {string} o.toolKey 'claude-code' | 'codex' | 'opencode'
270
+ * @param {string} [o.label] the CLI's name ("Claude Code")
271
+ * @param {object | null} [o.info] this CLI in the latest GET /api/system/tool-versions (null: no answer yet)
272
+ * @param {{ reason: string | null } | null} [o.forced] a start of the CLI failed in a session, before the
273
+ * server was asked again (the panels' latch): shown as "failed to start" until an answer says otherwise
274
+ * @param {object | null} [o.job] the install under way or just ended (reduceInstallJob)
275
+ * @param {string | null} [o.guideUrl] the page's install guide for this CLI
276
+ * @param {string} [o.installCommand] the command to install it by hand (cli-status getCliInstallCommand)
277
+ * @param {object} [options]
278
+ * @param {(key: string, params?: object) => string} [options.t] a key of CLI_INSTALL_COPY → its sentence
279
+ * @returns {{
280
+ * show: boolean, blocked: boolean,
281
+ * kind: 'hidden' | 'missing' | 'broken' | 'installing' | 'failed' | 'done',
282
+ * tone: 'warn' | 'err' | 'busy' | 'ok' | null, icon: string, live: 'polite' | 'assertive',
283
+ * title: string, text: string, command: string, error: string, note: string,
284
+ * progress: { step: string, text: string, percent: number | null } | null, log: string[],
285
+ * install: { label: string, disabled: boolean } | null, guide: string | null,
286
+ * recheck: boolean, dismiss: boolean,
287
+ * }}
288
+ * `blocked`: sending stays off (missing, broken, installing, failed). `install`: the Install button, when the
289
+ * server offers one for this system and the CLI really is missing.
290
+ */
291
+ export function cliBannerView({ toolKey, label = '', info = null, forced = null, job = null, guideUrl = null, installCommand = '', addingPath = false, pathError = '' } = {}, { t = cliInstallText } = {}) {
292
+ const name = words(label) || String(toolKey || '');
293
+ const state = cliStateOf(info);
294
+ const guide = safeUrl(job?.guideUrl, info?.guideUrl, guideUrl);
295
+ const command = words(job?.manualCommand) || words(info?.installCommand) || words(installCommand);
296
+ const offer = cliInstallOffer(info);
297
+ const view = {
298
+ show: false, blocked: false, kind: 'hidden', tone: null, icon: '', live: 'polite',
299
+ title: '', text: '', command: '', error: '', note: '',
300
+ progress: null, log: [], install: null, guide, recheck: false, dismiss: false, addPath: null,
301
+ };
302
+ const lastLines = (log) => (Array.isArray(log) ? log.slice(-INSTALL_LOG_SHOWN) : []);
303
+
304
+ // An install under way: the banner stays, sending stays off, the steps and the installer's last lines show.
305
+ if (job?.phase === 'running') {
306
+ return Object.assign(view, {
307
+ show: true, blocked: true, kind: 'installing', tone: 'busy', icon: '…',
308
+ title: t('titleInstalling', { label: name }),
309
+ progress: installStepView(job, { t }),
310
+ log: lastLines(job.log),
311
+ install: { label: t('installing'), disabled: true },
312
+ recheck: false,
313
+ });
314
+ }
315
+
316
+ // Installed: nothing to show, except, for a moment, where an install that just ended put it.
317
+ if (state === 'installed' && !forced) {
318
+ const where = job?.phase === 'done' && job.path && typeof job.path === 'object' ? job.path : null;
319
+ const dir = words(where?.dir);
320
+ const pathStatus = info?.pathStatus || where;
321
+ const missingPath = INSTALLABLE_CLIS.includes(toolKey) && pathStatus?.onPath === false && (pathStatus.canAdd !== false);
322
+ const addPath = missingPath ? { label: t(addingPath ? 'addingPath' : 'addPath'), disabled: addingPath } : null;
323
+ if (missingPath) return Object.assign(view, {
324
+ show: true, blocked: false, kind: 'path', tone: 'warn', icon: '!',
325
+ title: t('titlePath', { label: name }), text: t('bodyPath', { dir: pathStatus.dir }),
326
+ error: pathError ? t('pathFailed', { error: pathError }) : '',
327
+ addPath, recheck: true, guide: null,
328
+ });
329
+ if (where && dir && (where.onPath === false || where.added === true)) {
330
+ return Object.assign(view, {
331
+ show: true, blocked: false, kind: 'done', tone: 'ok', icon: '✓',
332
+ title: t('titleDone', { label: name }),
333
+ text: where.onPath === false ? t('doneNotOnPath', { label: name, dir }) : t('doneAddedToPath', { label: name, dir }),
334
+ note: words(where.warning), guide: null, dismiss: true,
335
+ });
336
+ }
337
+ return view;
338
+ }
339
+
340
+ // Found, but it would not start: the error it gave, never an Install button (it is there already).
341
+ if (state === 'broken' || (forced && state !== 'missing')) {
342
+ const fromServer = state === 'broken';
343
+ const path = words(info?.binPath) || words(info?.realPath);
344
+ const version = words(info?.version) || words(info?.installed);
345
+ const error = fromServer ? words(info?.error) || words(forced?.reason) : words(forced?.reason) || words(info?.error);
346
+ let text;
347
+ if (path && version) text = t('bodyBrokenVersion', { label: name, version, path });
348
+ else if (path) text = t('bodyBroken', { label: name, path });
349
+ else text = t('bodyBrokenLatched', { label: name });
350
+ const configured = info?.configured && typeof info.configured === 'object' && info.configured.ok === false && words(info.configured.error)
351
+ ? t('configuredFailed', { error: words(info.configured.error) })
352
+ : '';
353
+ return Object.assign(view, {
354
+ show: true, blocked: true, kind: 'broken', tone: 'err', icon: '!', live: 'assertive',
355
+ title: t('titleBroken', { label: name }),
356
+ text, error: line(error, 600),
357
+ note: configured || (error ? '' : t('bodyBrokenHint')),
358
+ recheck: true,
359
+ });
360
+ }
361
+
362
+ if (state === 'missing') {
363
+ // The last install failed: why, what the installer said last, and the command to run by hand.
364
+ if (job?.phase === 'failed') {
365
+ const why = installErrorView(job, { label: name, t });
366
+ return Object.assign(view, {
367
+ show: true, blocked: true, kind: 'failed', tone: 'err', icon: '!', live: 'assertive',
368
+ title: t('titleFailed', { label: name }),
369
+ text: why.text,
370
+ error: why.detail ? line(why.detail, 600) : '',
371
+ note: command ? t('bodyFailedManual') : '',
372
+ command,
373
+ log: lastLines(job.log),
374
+ install: offer && job.code !== 'unsupported' ? { label: t('retry'), disabled: false } : null,
375
+ recheck: true,
376
+ });
377
+ }
378
+ const text = offer
379
+ ? t(offer.method === 'archive' ? 'bodyMissingArchive' : 'bodyMissing', { vendor: CLI_VENDORS[toolKey] || name, label: name })
380
+ : t('bodyMissingManual');
381
+ return Object.assign(view, {
382
+ show: true, blocked: true, kind: 'missing', tone: 'warn', icon: '!',
383
+ title: t('titleMissing', { label: name }),
384
+ text, command,
385
+ install: offer ? { label: t('install'), disabled: false } : null,
386
+ recheck: true,
387
+ });
388
+ }
389
+
390
+ // No answer yet: nothing to say, and nothing is held back (a start that fails says so itself).
391
+ return view;
392
+ }