synabun 2.0.0 → 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 (507) hide show
  1. package/CHANGELOG.md +97 -0
  2. package/NOTICE +5 -0
  3. package/README.md +2 -1
  4. package/THIRD-PARTY-LICENSES.md +59 -0
  5. package/benchmarks/fixtures/browser/README.md +1 -1
  6. package/benchmarks/fixtures/desktop/README.md +1 -1
  7. package/docs/whatsapp.md +153 -18
  8. package/hooks/claude-code/pre-compact.mjs +11 -0
  9. package/hooks/claude-code/prompt-submit.mjs +60 -184
  10. package/hooks/claude-code/session-start.mjs +73 -16
  11. package/hooks/claude-code/shared.mjs +26 -3
  12. package/hooks/claude-code/stop.mjs +5 -5
  13. package/lib/child-path.js +303 -0
  14. package/lib/claude-hooks.js +2 -1
  15. package/lib/data-home-migration.js +2 -0
  16. package/lib/desktop-app.js +265 -0
  17. package/lib/external-tools.js +7 -3
  18. package/lib/log-redact.js +102 -0
  19. package/lib/packaged-runtime.js +138 -11
  20. package/lib/start-launcher.js +88 -1
  21. package/mcp-server/dist/http.d.ts +3 -1
  22. package/mcp-server/dist/http.d.ts.map +1 -1
  23. package/mcp-server/dist/http.js +15 -4
  24. package/mcp-server/dist/http.js.map +1 -1
  25. package/mcp-server/dist/index.d.ts.map +1 -1
  26. package/mcp-server/dist/index.js +9 -6
  27. package/mcp-server/dist/index.js.map +1 -1
  28. package/mcp-server/dist/services/budget-judgments.d.ts +108 -0
  29. package/mcp-server/dist/services/budget-judgments.d.ts.map +1 -0
  30. package/mcp-server/dist/services/budget-judgments.js +153 -0
  31. package/mcp-server/dist/services/budget-judgments.js.map +1 -0
  32. package/mcp-server/dist/services/codex-config-heal.d.ts +5 -1
  33. package/mcp-server/dist/services/codex-config-heal.d.ts.map +1 -1
  34. package/mcp-server/dist/services/codex-config-heal.js +41 -4
  35. package/mcp-server/dist/services/codex-config-heal.js.map +1 -1
  36. package/mcp-server/dist/services/identity.d.ts +11 -0
  37. package/mcp-server/dist/services/identity.d.ts.map +1 -1
  38. package/mcp-server/dist/services/identity.js +24 -0
  39. package/mcp-server/dist/services/identity.js.map +1 -1
  40. package/mcp-server/dist/services/neural-interface.d.ts +13 -0
  41. package/mcp-server/dist/services/neural-interface.d.ts.map +1 -1
  42. package/mcp-server/dist/services/neural-interface.js +34 -2
  43. package/mcp-server/dist/services/neural-interface.js.map +1 -1
  44. package/mcp-server/dist/services/typesafe-config.d.ts +1 -1
  45. package/mcp-server/dist/services/typesafe-config.d.ts.map +1 -1
  46. package/mcp-server/dist/services/typesafe-config.js +4 -1
  47. package/mcp-server/dist/services/typesafe-config.js.map +1 -1
  48. package/mcp-server/dist/tools/agents.d.ts +10 -7
  49. package/mcp-server/dist/tools/agents.d.ts.map +1 -1
  50. package/mcp-server/dist/tools/agents.js +34 -13
  51. package/mcp-server/dist/tools/agents.js.map +1 -1
  52. package/mcp-server/dist/tools/whatsapp-send-file.d.ts +17 -0
  53. package/mcp-server/dist/tools/whatsapp-send-file.d.ts.map +1 -0
  54. package/mcp-server/dist/tools/whatsapp-send-file.js +44 -0
  55. package/mcp-server/dist/tools/whatsapp-send-file.js.map +1 -0
  56. package/mcp-server/dist/tui.js +2 -1
  57. package/mcp-server/dist/tui.js.map +1 -1
  58. package/mcp-server/dist/version.d.ts +5 -0
  59. package/mcp-server/dist/version.d.ts.map +1 -0
  60. package/mcp-server/dist/version.js +21 -0
  61. package/mcp-server/dist/version.js.map +1 -0
  62. package/mcp-server/package-lock.json +2 -2
  63. package/mcp-server/package.json +1 -3
  64. package/mcp-server/run.mjs +15 -9
  65. package/mcp-server/src/http.ts +18 -3
  66. package/mcp-server/src/index.ts +9 -6
  67. package/mcp-server/src/services/budget-judgments.ts +183 -0
  68. package/mcp-server/src/services/codex-config-heal.ts +35 -4
  69. package/mcp-server/src/services/identity.ts +27 -0
  70. package/mcp-server/src/services/neural-interface.ts +34 -2
  71. package/mcp-server/src/services/typesafe-config.ts +4 -1
  72. package/mcp-server/src/tools/agents.ts +33 -14
  73. package/mcp-server/src/tools/whatsapp-send-file.ts +51 -0
  74. package/mcp-server/src/tui.ts +2 -1
  75. package/mcp-server/src/version.ts +22 -0
  76. package/neural-interface/i18n/en.json +2126 -209
  77. package/neural-interface/i18n/pt-BR.json +2123 -207
  78. package/neural-interface/lib/account-usage.js +163 -0
  79. package/neural-interface/lib/assistant-api.js +316 -57
  80. package/neural-interface/lib/assistant-brains/claude.js +19 -2
  81. package/neural-interface/lib/assistant-brains/codex.js +3 -0
  82. package/neural-interface/lib/assistant-brains/opencode.js +17 -4
  83. package/neural-interface/lib/assistant-budget-planner.js +390 -0
  84. package/neural-interface/lib/assistant-budget-split.js +408 -0
  85. package/neural-interface/lib/assistant-budget.js +94 -95
  86. package/neural-interface/lib/assistant-catalog.js +179 -27
  87. package/neural-interface/lib/assistant-config.js +91 -14
  88. package/neural-interface/lib/assistant-dispatch.js +494 -138
  89. package/neural-interface/lib/assistant-hidden-models.js +393 -66
  90. package/neural-interface/lib/assistant-jev.js +26 -3
  91. package/neural-interface/lib/assistant-memory.js +24 -10
  92. package/neural-interface/lib/assistant-persona.js +73 -12
  93. package/neural-interface/lib/assistant-plan-permissions.js +8 -1
  94. package/neural-interface/lib/assistant-pricing.js +21 -6
  95. package/neural-interface/lib/assistant-route-accounts.js +14 -0
  96. package/neural-interface/lib/assistant-route-gate.js +19 -3
  97. package/neural-interface/lib/assistant-router.js +373 -42
  98. package/neural-interface/lib/assistant-runtime.js +488 -59
  99. package/neural-interface/lib/assistant-task-prompt.js +13 -3
  100. package/neural-interface/lib/assistant-test-density.js +142 -0
  101. package/neural-interface/lib/assistant-user-context-budgets.js +123 -0
  102. package/neural-interface/lib/assistant-user-context.js +908 -0
  103. package/neural-interface/lib/assistant-user-knowledge.js +328 -0
  104. package/neural-interface/lib/assistant-user-learn.js +404 -0
  105. package/neural-interface/lib/assistant-user-threads.js +226 -0
  106. package/neural-interface/lib/assistant-user-writer.js +183 -0
  107. package/neural-interface/lib/augmented-path.js +317 -53
  108. package/neural-interface/lib/browser-background-tab.js +134 -0
  109. package/neural-interface/lib/browser-screenshot.js +678 -61
  110. package/neural-interface/lib/claude-accounts.js +342 -31
  111. package/neural-interface/lib/claude-agent-bridge.js +123 -9
  112. package/neural-interface/lib/claude-executable.js +6 -1
  113. package/neural-interface/lib/claude-model-catalog.js +33 -5
  114. package/neural-interface/lib/claude-panel-auth.js +221 -0
  115. package/neural-interface/lib/claude-profile-api-key.js +62 -0
  116. package/neural-interface/lib/claude-temporary.js +46 -7
  117. package/neural-interface/lib/cli-install-api.js +137 -0
  118. package/neural-interface/lib/cli-install.js +797 -0
  119. package/neural-interface/lib/cli-path.js +130 -0
  120. package/neural-interface/lib/cli-resolver.js +744 -0
  121. package/neural-interface/lib/cli-update-planner.js +143 -7
  122. package/neural-interface/lib/cloudflared/install.js +533 -0
  123. package/neural-interface/lib/cloudflared/manager.js +545 -0
  124. package/neural-interface/lib/cloudflared/pins.js +115 -0
  125. package/neural-interface/lib/cloudflared/pins.json +63 -0
  126. package/neural-interface/lib/cloudflared/tunnel.js +265 -0
  127. package/neural-interface/lib/codex-account-auth.js +178 -0
  128. package/neural-interface/lib/codex-accounts.js +489 -0
  129. package/neural-interface/lib/codex-capabilities.js +2 -1
  130. package/neural-interface/lib/codex-compat-rpc.js +8 -2
  131. package/neural-interface/lib/codex-mcp-approvals.js +5 -1
  132. package/neural-interface/lib/codex-panel-auth.js +62 -0
  133. package/neural-interface/lib/codex-session-settings.js +1 -1
  134. package/neural-interface/lib/codex-settings-schema.js +120 -0
  135. package/neural-interface/lib/desktop/config.js +3 -0
  136. package/neural-interface/lib/editor-completion-endpoint.js +264 -0
  137. package/neural-interface/lib/editor-completion-opencode.js +436 -0
  138. package/neural-interface/lib/editor-completion.js +714 -0
  139. package/neural-interface/lib/editor-config.js +149 -0
  140. package/neural-interface/lib/editor-files-api.js +268 -0
  141. package/neural-interface/lib/editor-git-api.js +134 -0
  142. package/neural-interface/lib/file-explorer-api.js +1039 -0
  143. package/neural-interface/lib/file-explorer-system.js +355 -0
  144. package/neural-interface/lib/file-explorer-trash.ps1 +85 -0
  145. package/neural-interface/lib/file-explorer-watch.js +239 -0
  146. package/neural-interface/lib/git-bash/bzip2.js +339 -0
  147. package/neural-interface/lib/git-bash/claude-settings.js +73 -0
  148. package/neural-interface/lib/git-bash/extract-worker.js +34 -0
  149. package/neural-interface/lib/git-bash/install.js +529 -0
  150. package/neural-interface/lib/git-bash/licenses/COPYING +360 -0
  151. package/neural-interface/lib/git-bash/licenses/LGPL-2.1 +511 -0
  152. package/neural-interface/lib/git-bash/manager.js +268 -0
  153. package/neural-interface/lib/git-bash/pins.js +206 -0
  154. package/neural-interface/lib/git-bash/pins.json +34 -0
  155. package/neural-interface/lib/git-bash/resolver.js +194 -0
  156. package/neural-interface/lib/git-diff-api.js +103 -0
  157. package/neural-interface/lib/git-read.js +234 -0
  158. package/neural-interface/lib/greeting/api.js +89 -0
  159. package/neural-interface/lib/greeting/budgets.js +90 -0
  160. package/neural-interface/lib/greeting/engine.js +461 -0
  161. package/neural-interface/lib/greeting/facts.js +320 -0
  162. package/neural-interface/lib/greeting/sources.js +252 -0
  163. package/neural-interface/lib/greeting/writer.js +139 -0
  164. package/neural-interface/lib/http-guards.js +189 -13
  165. package/neural-interface/lib/install-details.js +145 -0
  166. package/neural-interface/lib/machine-names.js +88 -0
  167. package/neural-interface/lib/mcp-installer.js +14 -2
  168. package/neural-interface/lib/memory-api.js +31 -7
  169. package/neural-interface/lib/native-loop-providers.js +26 -8
  170. package/neural-interface/lib/native-loop-runtime.js +9 -4
  171. package/neural-interface/lib/os-path.js +319 -0
  172. package/neural-interface/lib/plugin-installer.js +4 -2
  173. package/neural-interface/lib/project-storage.js +5 -7
  174. package/neural-interface/lib/provider-panel-api-key-auth.js +92 -0
  175. package/neural-interface/lib/provider-panel-terminal-auth.js +95 -0
  176. package/neural-interface/lib/pty-host/core.js +2 -1
  177. package/neural-interface/lib/pty-host/manager.js +27 -2
  178. package/neural-interface/lib/remote-policy.js +20 -11
  179. package/neural-interface/lib/request-gate.js +589 -0
  180. package/neural-interface/lib/session-title-generator.js +4 -3
  181. package/neural-interface/lib/system-about.js +340 -0
  182. package/neural-interface/lib/system-activity.js +66 -0
  183. package/neural-interface/lib/terminal-env.js +11 -3
  184. package/neural-interface/lib/terminal-path.js +55 -0
  185. package/neural-interface/lib/ui-blob-api.js +108 -0
  186. package/neural-interface/lib/ui-blob-store.js +68 -0
  187. package/neural-interface/lib/virtual-socket.js +12 -1
  188. package/neural-interface/lib/whatsapp/baileys-adapter.js +165 -45
  189. package/neural-interface/lib/whatsapp/bridge.js +78 -34
  190. package/neural-interface/lib/whatsapp/commands.js +13 -14
  191. package/neural-interface/lib/whatsapp/config.js +134 -20
  192. package/neural-interface/lib/whatsapp/fake-baileys.js +99 -14
  193. package/neural-interface/lib/whatsapp/host-core.js +168 -35
  194. package/neural-interface/lib/whatsapp/inbound.js +5 -3
  195. package/neural-interface/lib/whatsapp/local-file.js +524 -0
  196. package/neural-interface/lib/whatsapp/manager.js +92 -32
  197. package/neural-interface/lib/whatsapp/paths.js +11 -1
  198. package/neural-interface/lib/whatsapp/protocol.js +65 -5
  199. package/neural-interface/lib/whatsapp/redact.js +63 -0
  200. package/neural-interface/lib/whatsapp/service.js +343 -91
  201. package/neural-interface/lib/whatsapp/transcription.js +463 -0
  202. package/neural-interface/package-lock.json +701 -2
  203. package/neural-interface/package.json +13 -2
  204. package/neural-interface/public/index.html +2 -0
  205. package/neural-interface/public/index2d.html +2 -0
  206. package/neural-interface/public/install-app.html +9 -6
  207. package/neural-interface/public/offline.html +19 -3
  208. package/neural-interface/public/onboarding.html +107 -2
  209. package/neural-interface/public/shared/account-i18n.js +7 -0
  210. package/neural-interface/public/shared/account-menu.js +539 -0
  211. package/neural-interface/public/shared/account-usage.js +19 -0
  212. package/neural-interface/public/shared/account-view.js +335 -0
  213. package/neural-interface/public/shared/agent-runtime-options.js +50 -14
  214. package/neural-interface/public/shared/assistant/asst-accounts.js +182 -184
  215. package/neural-interface/public/shared/assistant/asst-api.js +32 -2
  216. package/neural-interface/public/shared/assistant/asst-automation.js +286 -0
  217. package/neural-interface/public/shared/assistant/asst-brain-picker.js +430 -81
  218. package/neural-interface/public/shared/assistant/asst-budget.js +259 -303
  219. package/neural-interface/public/shared/assistant/asst-composer.js +11 -1
  220. package/neural-interface/public/shared/assistant/asst-mascot.js +140 -61
  221. package/neural-interface/public/shared/assistant/asst-menu.js +27 -11
  222. package/neural-interface/public/shared/assistant/asst-panel.js +198 -42
  223. package/neural-interface/public/shared/assistant/asst-render.js +197 -23
  224. package/neural-interface/public/shared/assistant/asst-route-models.js +108 -0
  225. package/neural-interface/public/shared/assistant/asst-route.js +95 -343
  226. package/neural-interface/public/shared/assistant/asst-routes-editor.js +534 -0
  227. package/neural-interface/public/shared/assistant/asst-sanitize.js +8 -1
  228. package/neural-interface/public/shared/assistant/asst-sidepanel.js +31 -9
  229. package/neural-interface/public/shared/assistant/asst-slash.js +1 -1
  230. package/neural-interface/public/shared/assistant/asst-state.js +186 -2
  231. package/neural-interface/public/shared/assistant/asst-styles.js +247 -11
  232. package/neural-interface/public/shared/blob-store.js +111 -22
  233. package/neural-interface/public/shared/cdx/cdx-commands.js +4 -1
  234. package/neural-interface/public/shared/cdx/cdx-features.js +22 -14
  235. package/neural-interface/public/shared/cdx/cdx-icons.js +2 -0
  236. package/neural-interface/public/shared/cdx/cdx-panel.js +80 -118
  237. package/neural-interface/public/shared/cdx/cdx-render.js +25 -89
  238. package/neural-interface/public/shared/cdx/cdx-settings.js +231 -0
  239. package/neural-interface/public/shared/cdx/cdx-styles.js +164 -107
  240. package/neural-interface/public/shared/cdx/cdx-tabs.js +348 -668
  241. package/neural-interface/public/shared/cdx/cdx-ws.js +2 -0
  242. package/neural-interface/public/shared/claude-account-view.js +3 -0
  243. package/neural-interface/public/shared/cli-install-view.js +392 -0
  244. package/neural-interface/public/shared/cli-status.js +282 -22
  245. package/neural-interface/public/shared/constants.js +6 -1
  246. package/neural-interface/public/shared/cp/cp-events.js +2 -1
  247. package/neural-interface/public/shared/cp/cp-rehydrate.js +2 -1
  248. package/neural-interface/public/shared/cp/cp-styles.js +61 -0
  249. package/neural-interface/public/shared/desktop-bridge.js +39 -0
  250. package/neural-interface/public/shared/desktop-titlebar-page.js +59 -0
  251. package/neural-interface/public/shared/desktop-titlebar-view.js +122 -0
  252. package/neural-interface/public/shared/desktop-titlebar.js +153 -0
  253. package/neural-interface/public/shared/desktop-update-view.js +462 -0
  254. package/neural-interface/public/shared/editor/editor-ai-logic.js +409 -0
  255. package/neural-interface/public/shared/editor/editor-ai.js +619 -0
  256. package/neural-interface/public/shared/editor/editor-commands.js +212 -0
  257. package/neural-interface/public/shared/editor/editor-compare.js +103 -0
  258. package/neural-interface/public/shared/editor/editor-completion-list.js +172 -0
  259. package/neural-interface/public/shared/editor/editor-doc.js +257 -0
  260. package/neural-interface/public/shared/editor/editor-git-logic.js +131 -0
  261. package/neural-interface/public/shared/editor/editor-git.js +95 -0
  262. package/neural-interface/public/shared/editor/editor-languages.js +156 -0
  263. package/neural-interface/public/shared/editor/editor-lint.js +229 -0
  264. package/neural-interface/public/shared/editor/editor-loader.js +51 -0
  265. package/neural-interface/public/shared/editor/editor-markdown.js +90 -0
  266. package/neural-interface/public/shared/editor/editor-menu.js +233 -0
  267. package/neural-interface/public/shared/editor/editor-minimap.js +371 -0
  268. package/neural-interface/public/shared/editor/editor-palette-logic.js +192 -0
  269. package/neural-interface/public/shared/editor/editor-palette.js +125 -0
  270. package/neural-interface/public/shared/editor/editor-panel-logic.js +288 -0
  271. package/neural-interface/public/shared/editor/editor-panel.js +2099 -0
  272. package/neural-interface/public/shared/editor/editor-prefs.js +224 -0
  273. package/neural-interface/public/shared/editor/editor-safety.js +500 -0
  274. package/neural-interface/public/shared/editor/editor-search.js +352 -0
  275. package/neural-interface/public/shared/editor/editor-statusbar.js +144 -0
  276. package/neural-interface/public/shared/editor/editor-tabs-logic.js +331 -0
  277. package/neural-interface/public/shared/editor/editor-tabs.js +233 -0
  278. package/neural-interface/public/shared/editor/editor-theme.js +244 -0
  279. package/neural-interface/public/shared/editor/editor-view.js +332 -0
  280. package/neural-interface/public/shared/explorer/fx-api.js +216 -0
  281. package/neural-interface/public/shared/explorer/fx-links.js +204 -0
  282. package/neural-interface/public/shared/explorer/fx-logic.js +537 -0
  283. package/neural-interface/public/shared/explorer/fx-main.js +865 -0
  284. package/neural-interface/public/shared/explorer/fx-names.js +87 -0
  285. package/neural-interface/public/shared/explorer/fx-ops.js +370 -0
  286. package/neural-interface/public/shared/explorer/fx-popups.js +490 -0
  287. package/neural-interface/public/shared/explorer/fx-tree.js +790 -0
  288. package/neural-interface/public/shared/file-links.js +339 -0
  289. package/neural-interface/public/shared/git-bash-view.js +145 -0
  290. package/neural-interface/public/shared/greeting/greeting-card.js +178 -0
  291. package/neural-interface/public/shared/greeting/greeting-view.js +148 -0
  292. package/neural-interface/public/shared/html-shell.js +161 -165
  293. package/neural-interface/public/shared/i18n.js +19 -1
  294. package/neural-interface/public/shared/install-details-view.js +86 -0
  295. package/neural-interface/public/shared/ocp-v2/ocp-v2-childpanel.js +4 -0
  296. package/neural-interface/public/shared/ocp-v2/ocp-v2-greeting.js +47 -0
  297. package/neural-interface/public/shared/ocp-v2/ocp-v2-panel.js +131 -17
  298. package/neural-interface/public/shared/ocp-v2/ocp-v2-render.js +7 -1
  299. package/neural-interface/public/shared/ocp-v2/ocp-v2-send.js +22 -0
  300. package/neural-interface/public/shared/ocp-v2/ocp-v2-styles.js +158 -0
  301. package/neural-interface/public/shared/ocp-v2/ocp-v2-ws.js +3 -2
  302. package/neural-interface/public/shared/pwa-install-page.js +32 -2
  303. package/neural-interface/public/shared/pwa-install.css +3 -3
  304. package/neural-interface/public/shared/pwa-install.js +100 -1
  305. package/neural-interface/public/shared/settings/settings-about-pane.js +271 -0
  306. package/neural-interface/public/shared/settings/settings-about.js +254 -0
  307. package/neural-interface/public/shared/settings/settings-code-editor-pane.js +412 -0
  308. package/neural-interface/public/shared/settings/settings-code-editor.js +328 -0
  309. package/neural-interface/public/shared/settings/settings-greeting.js +133 -0
  310. package/neural-interface/public/shared/settings/settings-ia.js +33 -4
  311. package/neural-interface/public/shared/settings/settings-user-context.js +154 -0
  312. package/neural-interface/public/shared/showcase-privacy.js +489 -0
  313. package/neural-interface/public/shared/start-bridge.js +238 -19
  314. package/neural-interface/public/shared/storage.js +30 -3
  315. package/neural-interface/public/shared/styles.css +1710 -770
  316. package/neural-interface/public/shared/synabun-mascot.js +68 -58
  317. package/neural-interface/public/shared/tunnel-view.js +167 -0
  318. package/neural-interface/public/shared/ui-claude-panel.js +369 -229
  319. package/neural-interface/public/shared/ui-cli-banner.js +355 -0
  320. package/neural-interface/public/shared/ui-detail.js +15 -0
  321. package/neural-interface/public/shared/ui-explorer.js +22 -0
  322. package/neural-interface/public/shared/ui-file-explorer.js +481 -2496
  323. package/neural-interface/public/shared/ui-file-links.js +103 -0
  324. package/neural-interface/public/shared/ui-git-bash.js +124 -0
  325. package/neural-interface/public/shared/ui-help.js +26 -0
  326. package/neural-interface/public/shared/ui-invite.js +126 -116
  327. package/neural-interface/public/shared/ui-judgments.js +10 -1
  328. package/neural-interface/public/shared/ui-keybinds.js +4 -0
  329. package/neural-interface/public/shared/ui-loading.js +187 -19
  330. package/neural-interface/public/shared/ui-menubar.js +25 -33
  331. package/neural-interface/public/shared/ui-native-loop-router-state.js +11 -0
  332. package/neural-interface/public/shared/ui-native-loop-router.js +15 -2
  333. package/neural-interface/public/shared/ui-navbar.js +48 -22
  334. package/neural-interface/public/shared/ui-provider-auth.js +104 -0
  335. package/neural-interface/public/shared/ui-provider-terminal-auth.js +223 -0
  336. package/neural-interface/public/shared/ui-sessions.js +9 -3
  337. package/neural-interface/public/shared/ui-settings.js +509 -490
  338. package/neural-interface/public/shared/ui-sidepanel-runtime.js +2 -1
  339. package/neural-interface/public/shared/ui-sidepanel-sessions.js +3 -1
  340. package/neural-interface/public/shared/ui-sync.js +22 -0
  341. package/neural-interface/public/shared/ui-terminal.js +40 -5
  342. package/neural-interface/public/shared/ui-tool-updates.js +5 -0
  343. package/neural-interface/public/shared/ui-tunnel.js +130 -0
  344. package/neural-interface/public/shared/ui-tutorial-steps.js +6 -2
  345. package/neural-interface/public/shared/ui-update.js +594 -5
  346. package/neural-interface/public/shared/ui-whatsapp.js +21 -6
  347. package/neural-interface/public/shared/ui-whiteboard.js +12 -0
  348. package/neural-interface/public/shared/ui-worker-end.js +245 -0
  349. package/neural-interface/public/shared/ui-workspaces.js +41 -27
  350. package/neural-interface/public/shared/whatsapp/wa-view.js +51 -8
  351. package/neural-interface/public/shared/worker-end.js +253 -0
  352. package/neural-interface/public/sw.js +2 -2
  353. package/neural-interface/public/variant/2d/main.js +10 -1
  354. package/neural-interface/public/variant/3d/main.js +10 -1
  355. package/neural-interface/public/vendor/codemirror/chunks/apl-CSDKT72Z.js +1 -0
  356. package/neural-interface/public/vendor/codemirror/chunks/asciiarmor-JYYPU5BM.js +1 -0
  357. package/neural-interface/public/vendor/codemirror/chunks/asn1-NRWCZDA6.js +1 -0
  358. package/neural-interface/public/vendor/codemirror/chunks/asterisk-5OU6D4ZQ.js +1 -0
  359. package/neural-interface/public/vendor/codemirror/chunks/brainfuck-ID4N4SYC.js +1 -0
  360. package/neural-interface/public/vendor/codemirror/chunks/chunk-4RZWGCS2.js +1 -0
  361. package/neural-interface/public/vendor/codemirror/chunks/chunk-ACTJM3JR.js +11 -0
  362. package/neural-interface/public/vendor/codemirror/chunks/chunk-S6BMAXLK.js +1 -0
  363. package/neural-interface/public/vendor/codemirror/chunks/chunk-TO2RDOHQ.js +3 -0
  364. package/neural-interface/public/vendor/codemirror/chunks/chunk-US2GMZZQ.js +13 -0
  365. package/neural-interface/public/vendor/codemirror/chunks/chunk-WYYO5ZAJ.js +1 -0
  366. package/neural-interface/public/vendor/codemirror/chunks/chunk-XALZTDJC.js +1 -0
  367. package/neural-interface/public/vendor/codemirror/chunks/chunk-YRNUTRDV.js +1 -0
  368. package/neural-interface/public/vendor/codemirror/chunks/chunk-Z3QOKH62.js +1 -0
  369. package/neural-interface/public/vendor/codemirror/chunks/clike-OWQBSOGL.js +1 -0
  370. package/neural-interface/public/vendor/codemirror/chunks/clojure-KV75UQWC.js +1 -0
  371. package/neural-interface/public/vendor/codemirror/chunks/cmake-GTEP5Y3C.js +1 -0
  372. package/neural-interface/public/vendor/codemirror/chunks/cobol-WCZ7QEGJ.js +1 -0
  373. package/neural-interface/public/vendor/codemirror/chunks/coffeescript-TGLBOT6B.js +1 -0
  374. package/neural-interface/public/vendor/codemirror/chunks/commonlisp-WVVI432V.js +1 -0
  375. package/neural-interface/public/vendor/codemirror/chunks/crystal-S6R6HLKQ.js +1 -0
  376. package/neural-interface/public/vendor/codemirror/chunks/css-PDA2RINP.js +1 -0
  377. package/neural-interface/public/vendor/codemirror/chunks/cypher-HL5QPL4C.js +1 -0
  378. package/neural-interface/public/vendor/codemirror/chunks/d-HHJH2HAJ.js +1 -0
  379. package/neural-interface/public/vendor/codemirror/chunks/diff-MN3A35BY.js +1 -0
  380. package/neural-interface/public/vendor/codemirror/chunks/dist-25SQLBVS.js +3 -0
  381. package/neural-interface/public/vendor/codemirror/chunks/dist-27ZGGJJS.js +1 -0
  382. package/neural-interface/public/vendor/codemirror/chunks/dist-4E52OJIF.js +1 -0
  383. package/neural-interface/public/vendor/codemirror/chunks/dist-6FT73XDC.js +2 -0
  384. package/neural-interface/public/vendor/codemirror/chunks/dist-BE6YGRMW.js +1 -0
  385. package/neural-interface/public/vendor/codemirror/chunks/dist-CJSGKW2O.js +1 -0
  386. package/neural-interface/public/vendor/codemirror/chunks/dist-CRPH2WLI.js +1 -0
  387. package/neural-interface/public/vendor/codemirror/chunks/dist-CTG7JEQN.js +1 -0
  388. package/neural-interface/public/vendor/codemirror/chunks/dist-I3FEVELX.js +1 -0
  389. package/neural-interface/public/vendor/codemirror/chunks/dist-IM5BBSWZ.js +1 -0
  390. package/neural-interface/public/vendor/codemirror/chunks/dist-KERUKTIF.js +1 -0
  391. package/neural-interface/public/vendor/codemirror/chunks/dist-LXTMWZAM.js +1 -0
  392. package/neural-interface/public/vendor/codemirror/chunks/dist-MABASSET.js +1 -0
  393. package/neural-interface/public/vendor/codemirror/chunks/dist-MJVBUZLL.js +3 -0
  394. package/neural-interface/public/vendor/codemirror/chunks/dist-MNB3WZVU.js +1 -0
  395. package/neural-interface/public/vendor/codemirror/chunks/dist-QYWXOE3U.js +7 -0
  396. package/neural-interface/public/vendor/codemirror/chunks/dist-TEBOFWLZ.js +1 -0
  397. package/neural-interface/public/vendor/codemirror/chunks/dist-UNXFROM3.js +1 -0
  398. package/neural-interface/public/vendor/codemirror/chunks/dist-XL2SLQO6.js +1 -0
  399. package/neural-interface/public/vendor/codemirror/chunks/dist-XO42Y3AP.js +6 -0
  400. package/neural-interface/public/vendor/codemirror/chunks/dist-Y2WWDWJK.js +1 -0
  401. package/neural-interface/public/vendor/codemirror/chunks/dist-YFEZKWMU.js +1 -0
  402. package/neural-interface/public/vendor/codemirror/chunks/dist-ZY7SWQO6.js +1 -0
  403. package/neural-interface/public/vendor/codemirror/chunks/dockerfile-3Y4R4NCH.js +1 -0
  404. package/neural-interface/public/vendor/codemirror/chunks/dtd-5343GJ4P.js +1 -0
  405. package/neural-interface/public/vendor/codemirror/chunks/dylan-FI2CXSW2.js +1 -0
  406. package/neural-interface/public/vendor/codemirror/chunks/ebnf-GNQ77YWA.js +1 -0
  407. package/neural-interface/public/vendor/codemirror/chunks/ecl-MRPUAE2L.js +1 -0
  408. package/neural-interface/public/vendor/codemirror/chunks/eiffel-DZROES27.js +1 -0
  409. package/neural-interface/public/vendor/codemirror/chunks/elm-4KICRFKQ.js +1 -0
  410. package/neural-interface/public/vendor/codemirror/chunks/erlang-3IHYC263.js +1 -0
  411. package/neural-interface/public/vendor/codemirror/chunks/factor-C6D3E5UK.js +1 -0
  412. package/neural-interface/public/vendor/codemirror/chunks/fcl-VYTWUJMI.js +1 -0
  413. package/neural-interface/public/vendor/codemirror/chunks/forth-ZBO2EH5M.js +1 -0
  414. package/neural-interface/public/vendor/codemirror/chunks/fortran-5NDCRWIK.js +1 -0
  415. package/neural-interface/public/vendor/codemirror/chunks/gas-SKRMOXFP.js +1 -0
  416. package/neural-interface/public/vendor/codemirror/chunks/gherkin-L45YXZLW.js +1 -0
  417. package/neural-interface/public/vendor/codemirror/chunks/groovy-GLPTLN44.js +1 -0
  418. package/neural-interface/public/vendor/codemirror/chunks/haskell-E7NF7P4Y.js +1 -0
  419. package/neural-interface/public/vendor/codemirror/chunks/haxe-K2NVDOB6.js +1 -0
  420. package/neural-interface/public/vendor/codemirror/chunks/http-Q6DRQIJB.js +1 -0
  421. package/neural-interface/public/vendor/codemirror/chunks/idl-MZYNJVLN.js +1 -0
  422. package/neural-interface/public/vendor/codemirror/chunks/javascript-6YRYK2HR.js +1 -0
  423. package/neural-interface/public/vendor/codemirror/chunks/julia-HXEL2T5G.js +1 -0
  424. package/neural-interface/public/vendor/codemirror/chunks/livescript-MV7CI2JZ.js +1 -0
  425. package/neural-interface/public/vendor/codemirror/chunks/lua-E36NHOOD.js +1 -0
  426. package/neural-interface/public/vendor/codemirror/chunks/mathematica-3XOZZCMC.js +1 -0
  427. package/neural-interface/public/vendor/codemirror/chunks/mbox-ODYGFKS7.js +1 -0
  428. package/neural-interface/public/vendor/codemirror/chunks/mirc-WB43AWF5.js +1 -0
  429. package/neural-interface/public/vendor/codemirror/chunks/mllike-ZT6ET7ZH.js +1 -0
  430. package/neural-interface/public/vendor/codemirror/chunks/modelica-LLMZPFGA.js +1 -0
  431. package/neural-interface/public/vendor/codemirror/chunks/mscgen-2OPNNYFH.js +1 -0
  432. package/neural-interface/public/vendor/codemirror/chunks/mumps-6P3HWJH6.js +1 -0
  433. package/neural-interface/public/vendor/codemirror/chunks/nginx-TCGKQTRW.js +1 -0
  434. package/neural-interface/public/vendor/codemirror/chunks/nsis-ATIGIXTJ.js +1 -0
  435. package/neural-interface/public/vendor/codemirror/chunks/ntriples-5PLBI5OV.js +1 -0
  436. package/neural-interface/public/vendor/codemirror/chunks/octave-QF2S3NL6.js +1 -0
  437. package/neural-interface/public/vendor/codemirror/chunks/oz-JFIVFFH2.js +1 -0
  438. package/neural-interface/public/vendor/codemirror/chunks/pascal-JG6SW76V.js +1 -0
  439. package/neural-interface/public/vendor/codemirror/chunks/perl-4B3IWVVO.js +1 -0
  440. package/neural-interface/public/vendor/codemirror/chunks/pig-ABEBH4MI.js +1 -0
  441. package/neural-interface/public/vendor/codemirror/chunks/powershell-H32XE7TL.js +1 -0
  442. package/neural-interface/public/vendor/codemirror/chunks/properties-NN644MMA.js +1 -0
  443. package/neural-interface/public/vendor/codemirror/chunks/protobuf-YKWQK2J3.js +1 -0
  444. package/neural-interface/public/vendor/codemirror/chunks/pug-RIYXEKFW.js +1 -0
  445. package/neural-interface/public/vendor/codemirror/chunks/puppet-JZQ5WPEV.js +1 -0
  446. package/neural-interface/public/vendor/codemirror/chunks/python-EBCGGHAC.js +1 -0
  447. package/neural-interface/public/vendor/codemirror/chunks/q-5UMH2YQY.js +1 -0
  448. package/neural-interface/public/vendor/codemirror/chunks/r-7XMGG453.js +1 -0
  449. package/neural-interface/public/vendor/codemirror/chunks/rpm-IEV7TSLR.js +1 -0
  450. package/neural-interface/public/vendor/codemirror/chunks/ruby-T4SPFPNM.js +1 -0
  451. package/neural-interface/public/vendor/codemirror/chunks/sas-GEDX67ET.js +1 -0
  452. package/neural-interface/public/vendor/codemirror/chunks/scheme-FTLOWKPC.js +1 -0
  453. package/neural-interface/public/vendor/codemirror/chunks/shell-HUFRKFSJ.js +1 -0
  454. package/neural-interface/public/vendor/codemirror/chunks/sieve-P5QUDVYO.js +1 -0
  455. package/neural-interface/public/vendor/codemirror/chunks/smalltalk-2SORJNQI.js +1 -0
  456. package/neural-interface/public/vendor/codemirror/chunks/solr-M22TGAR5.js +1 -0
  457. package/neural-interface/public/vendor/codemirror/chunks/sparql-4M3KL752.js +1 -0
  458. package/neural-interface/public/vendor/codemirror/chunks/spreadsheet-3K23LA4N.js +1 -0
  459. package/neural-interface/public/vendor/codemirror/chunks/sql-VHRFMUBI.js +1 -0
  460. package/neural-interface/public/vendor/codemirror/chunks/stex-QZDAQUY2.js +1 -0
  461. package/neural-interface/public/vendor/codemirror/chunks/stylus-7ZDKAKBD.js +1 -0
  462. package/neural-interface/public/vendor/codemirror/chunks/swift-WCLCLGMR.js +1 -0
  463. package/neural-interface/public/vendor/codemirror/chunks/tcl-JLMU6WOK.js +1 -0
  464. package/neural-interface/public/vendor/codemirror/chunks/textile-4CHDKZ4T.js +1 -0
  465. package/neural-interface/public/vendor/codemirror/chunks/tiddlywiki-DKMCZZQ4.js +1 -0
  466. package/neural-interface/public/vendor/codemirror/chunks/tiki-F4JIG36Q.js +1 -0
  467. package/neural-interface/public/vendor/codemirror/chunks/toml-BHMHTIM2.js +1 -0
  468. package/neural-interface/public/vendor/codemirror/chunks/troff-YQ2KV5NT.js +1 -0
  469. package/neural-interface/public/vendor/codemirror/chunks/ttcn-ZHRCVFJY.js +1 -0
  470. package/neural-interface/public/vendor/codemirror/chunks/ttcn-cfg-KJOVTY4X.js +1 -0
  471. package/neural-interface/public/vendor/codemirror/chunks/turtle-535P5N6I.js +1 -0
  472. package/neural-interface/public/vendor/codemirror/chunks/vb-76UMQHOT.js +1 -0
  473. package/neural-interface/public/vendor/codemirror/chunks/vbscript-CGNDTK6Z.js +1 -0
  474. package/neural-interface/public/vendor/codemirror/chunks/velocity-VSJDO5T5.js +1 -0
  475. package/neural-interface/public/vendor/codemirror/chunks/verilog-4T6ETXIP.js +1 -0
  476. package/neural-interface/public/vendor/codemirror/chunks/vhdl-T7FNAXUN.js +1 -0
  477. package/neural-interface/public/vendor/codemirror/chunks/webidl-P6MJSJ6J.js +1 -0
  478. package/neural-interface/public/vendor/codemirror/chunks/xquery-UOVILRGN.js +1 -0
  479. package/neural-interface/public/vendor/codemirror/chunks/yacas-CV2APROL.js +1 -0
  480. package/neural-interface/public/vendor/codemirror/chunks/z80-3KFNVM4M.js +1 -0
  481. package/neural-interface/public/vendor/codemirror/codemirror-6.43.14-75f05e67.min.js +2 -0
  482. package/neural-interface/public/vendor/codemirror/manifest.json +60 -0
  483. package/neural-interface/scripts/build-vendor.mjs +78 -2
  484. package/neural-interface/scripts/vendor-codemirror-entry.js +121 -0
  485. package/neural-interface/server.js +2085 -1629
  486. package/neural-interface/templates/rulesets/README.md +1 -1
  487. package/package.json +5 -8
  488. package/setup.js +53 -15
  489. package/benchmarks/browser-live-results.json +0 -79
  490. package/benchmarks/browser-v2-results.json +0 -258
  491. package/neural-interface/lib/codex-capabilities.d.ts +0 -41
  492. package/neural-interface/public/category-logos/claude.svg +0 -1
  493. package/neural-interface/public/category-logos/gemini-color.svg +0 -1
  494. package/neural-interface/public/category-logos/openai.svg +0 -1
  495. package/neural-interface/public/category-logos/synabun.png +0 -0
  496. package/neural-interface/public/claude.svg +0 -1
  497. package/neural-interface/public/gallery.svg +0 -6
  498. package/neural-interface/public/leonardoai.svg +0 -1
  499. package/neural-interface/public/mcp.svg +0 -4
  500. package/neural-interface/public/opencode-logo-dark-square.svg +0 -1
  501. package/neural-interface/public/pin-svgrepo-com.svg +0 -25
  502. package/neural-interface/public/sessionicon.svg +0 -8
  503. package/neural-interface/public/toolbar-album.svg +0 -8
  504. package/neural-interface/public/toolbar-question.svg +0 -6
  505. package/neural-interface/public/toolbar-sessions.svg +0 -6
  506. package/neural-interface/scripts/codex-protocol-fixture.mjs +0 -83
  507. package/skills/synabun/synabunicon.png +0 -0
@@ -12,24 +12,31 @@ import { DispatchError } from './assistant-dispatch.js';
12
12
  import { openServable } from './assistant-media.js';
13
13
  import { designFit, targetMakes } from './assistant-router.js';
14
14
  import { normalizeEffort } from './effort-levels.js';
15
- import { BUDGET_DEFAULTS, BUDGET_FIELDS, BUDGET_MAX_USD, BUDGET_MIN_USD } from './assistant-budget.js';
15
+ import { BUDGET_DEFAULT_USD, BUDGET_MAX_USD, BUDGET_MIN_USD, BUDGET_WARN_RATIO } from './assistant-budget.js';
16
16
  import { tooLargeMessage } from './assistant-attachments.js';
17
17
  import { getSessionPolicy, requiresHumanApproval } from './remote-policy.js';
18
18
  import { clampPlanDispatch, PLAN_DISPATCH_NOTE, runCanChangeCode } from './assistant-plan-permissions.js';
19
+ import { noStore, requireJson, requireLocal, requireLocalHost, sendGuardError } from './http-guards.js';
20
+
21
+ /** The header whatsapp_send_file's caller proves it is the WhatsApp conversation's own brain with (assistant-runtime.js BRAIN_CREDENTIAL_RE). */
22
+ export const BRAIN_CREDENTIAL_HEADER = 'x-synabun-brain-token';
23
+ const SEND_FILE_FIELDS = Object.freeze(['path', 'caption', 'as']);
19
24
 
20
25
  /**
21
- * How far each provider's spend can be held to a cap (shown in the Budget tab).
22
- * A cap is checked where the provider lets it be checked; between two checks a
23
- * run can go past it by that much.
26
+ * How far each provider's spend can be held to the budget (the Budget modal shows it). Spend is
27
+ * checked where the provider lets it be checked; between two checks a run can go past its share by
28
+ * that much.
24
29
  */
25
30
  export const BUDGET_ENFORCEMENT = Object.freeze({
26
- 'claude-code': { metered: 'reported', perRun: 'The Claude CLI stops the run (--max-budget-usd); it checks between model calls, so one call can pass the cap.', session: 'Checked after every turn; at the hard cap the run is stopped mid-turn.' },
27
- opencode: { metered: 'reported', perRun: 'OpenCode reports each step\'s cost; the run stops at the first step past its cap.', session: 'Checked at every step.' },
28
- codex: { metered: 'estimated', perRun: 'Codex reports tokens only when a turn ends: priced at the models.dev list price (an API-price estimate; ChatGPT-plan accounts are billed by plan quota) and checked after each turn, so one turn can pass the cap. A failed turn is priced from its rollout.', session: 'Checked after every turn.' },
29
- brain: { metered: 'reported', perRun: 'Checked before every brain turn; a Claude brain\'s CLI also stops at what is left of the brain cap. A Codex brain\'s tokens are priced at list price (estimated); a brain model with no price is refused.', session: 'The brain\'s spend counts toward the session hard cap.' },
31
+ 'claude-code': { metered: 'reported', share: 'Checked between model calls (each call at list price while the turn runs, the reported cost at its end), so one call can pass the share. The Claude CLI also stops a run at its ceiling, or at what the session had left when it started.', session: 'At the budget every run stops.' },
32
+ opencode: { metered: 'reported', share: 'OpenCode reports each step\'s cost; the share is checked at every step, so one step can pass it.', session: 'At the budget every run stops.' },
33
+ codex: { metered: 'estimated', share: 'Codex reports tokens: priced at list price (an API-price estimate; ChatGPT-plan accounts are billed by plan quota) as its rollout records them, so one record or turn can pass the share. A failed turn is priced from its rollout.', session: 'At the budget every run stops.' },
34
+ brain: { metered: 'reported', share: 'Your Assistant\'s own turns draw from its provider\'s share, which keeps a reserve for them; checked before every brain turn. A Claude brain\'s CLI also stops at what that share had left when it started. A brain model with no price is refused.', session: 'At the budget the brain takes no turn until the budget is raised.' },
30
35
  });
31
36
 
32
37
  const OWNER_ONLY = { error: 'The assistant is available to the owner only', code: 'GUEST_FORBIDDEN' };
38
+ // What GET /catalog is made from when no catalog builder is wired: one object, so its answer is reused like any build's.
39
+ const NO_CATALOG = Object.freeze({});
33
40
 
34
41
  function sendError(res, error, fallbackStatus = 500) {
35
42
  if (error instanceof DispatchError || (error && error.code && error.status)) {
@@ -87,9 +94,17 @@ export function slimRunView(run) {
87
94
  * @param {object} deps.dispatcher createAssistantDispatcher()
88
95
  * @param {object} [deps.runtime] createAssistantRuntime() (sessions; optional until wired)
89
96
  * @param {object} [deps.claudeAccounts] createClaudeAccounts()
90
- * @param {object} [deps.codexAccounts] { listForClient(), rename(id,label), remove(id, {inUse}), addStart() }
97
+ * @param {object} [deps.codexAccounts] createCodexAccounts() (lib/codex-accounts.js)
91
98
  * @param {Function} [deps.buildCatalog] async () => catalog object (models/accounts/projects/profiles)
92
99
  * @param {Function} [deps.createLoginTerminal] async ({ env, cwd, profile, accountId }) => { terminalSessionId, profile }
100
+ * @param {Function} [deps.claudeSignIn] async (accountId) => { sessionId, status, error } — Claude Code's own
101
+ * `auth login` in a SynaBun terminal under the account's config directory (lib/provider-panel-terminal-auth.js)
102
+ * @param {Function} [deps.claudeSetMode] async (accountId, mode) => row — what the account's sessions use
103
+ * @param {Function} [deps.claudeAccountInUse] (accountId) => '' | why it cannot be removed now
104
+ * @param {Function} [deps.codexSignIn] async (accountId) => { sessionId, status, error } — `codex login` in a SynaBun
105
+ * terminal under the account's CODEX_HOME (lib/provider-panel-terminal-auth.js)
106
+ * @param {Function} [deps.codexSetMode] async (accountId, mode) => row — what the account's sessions use
107
+ * @param {Function} [deps.codexAccountInUse] (accountId) => '' | why it cannot be removed now
93
108
  * @param {object} [deps.attachments] createAttachmentStore() (POST /attachments)
94
109
  */
95
110
  export function createAssistantApi({
@@ -98,8 +113,15 @@ export function createAssistantApi({
98
113
  usage = null,
99
114
  claudeAccounts = null,
100
115
  codexAccounts = null,
116
+ accountUsage = null,
101
117
  buildCatalog = null,
102
118
  createLoginTerminal = null,
119
+ claudeSignIn = null,
120
+ claudeSetMode = null,
121
+ claudeAccountInUse = null,
122
+ codexSignIn = null,
123
+ codexSetMode = null,
124
+ codexAccountInUse = null,
103
125
  broadcastSync = () => {},
104
126
  isGuestRequest = () => false,
105
127
  log = () => {},
@@ -119,6 +141,15 @@ export function createAssistantApi({
119
141
  // Remote sessions (lib/remote-policy.js): the session's policy, or null. Defaults
120
142
  // to the runtime's view, else the process-wide registry.
121
143
  sessionPolicy = null,
144
+ // The user-context engine (assistant-user-context.js) and the project a new conversation starts in.
145
+ userContext = null,
146
+ // Why the engine did not start on this server (a string or null), for the settings page.
147
+ userContextError = () => null,
148
+ defaultProject = () => 'global',
149
+ // whatsapp_send_file: the WhatsApp service ({ sendFile }) or a function returning it (it is built
150
+ // after this router), and the port the server listens on (the Host check of that route).
151
+ whatsapp = null,
152
+ port = 3344,
122
153
  } = {}) {
123
154
  if (!dispatcher) throw new Error('createAssistantApi requires a dispatcher');
124
155
  const router = Router();
@@ -268,22 +299,35 @@ export function createAssistantApi({
268
299
  // ── catalog ────────────────────────────────────────────────────────────────
269
300
  router.get('/catalog', async (req, res) => {
270
301
  try {
271
- const force = req.query.refresh === '1';
302
+ await accountUsage?.refresh();
303
+ // refresh=1 rebuilds from the providers' endpoints; deep=1 also makes them ask the CLI / app-server again.
304
+ const deep = req.query.deep === '1';
305
+ const force = req.query.refresh === '1' || deep;
272
306
  // Brain view / search: compact rows (OpenCode can list hundreds of models).
273
307
  const view = req.query.view === 'brain' ? 'brain' : req.query.view === 'manage' ? 'manage' : 'full';
274
308
  const provider = req.query.provider ? String(req.query.provider) : null;
275
309
  const q = req.query.q || req.query.search ? String(req.query.q || req.query.search) : null;
276
310
  // The Models manager: every row with hidden: true|false.
277
- if (view === 'manage' && catalog?.get) return res.json({ ok: true, ...(await catalog.get({ force, view: 'manage', provider })) });
311
+ if (view === 'manage' && catalog?.get) return res.json({ ok: true, ...(await catalog.get({ force, deep, view: 'manage', provider })) });
278
312
  if (catalog?.get && (view === 'brain' || provider || q)) {
279
313
  let preferences = {};
280
314
  try { preferences = configStore?.routing?.().preferences || {}; } catch {}
281
- const brief = await catalog.get({ force, view: 'brain', provider, q, preferences });
282
- return res.json({ ok: true, generatedAt: new Date(now()).toISOString(), ...brief, limits: dispatcher.limits, runs: dispatcher.list({ activeOnly: true }), totals: dispatcher.totals() });
315
+ const brief = await catalog.get({ force, deep, view: 'brain', provider, q, preferences });
316
+ // Accounts are read live (the catalog build can be seconds old): every Claude and Codex account with
317
+ // its mode, whether it is signed in and its identity, and the account a worker gets when a dispatch
318
+ // names none.
319
+ const accounts = {
320
+ ...(brief.accounts || {}),
321
+ ...(claudeAccounts ? { 'claude-code': accountUsage?.list()?.['claude-code'] || claudeAccounts.listForClient() } : {}),
322
+ ...(codexAccounts ? { codex: accountUsage?.list()?.codex || codexAccounts.listForClient() } : {}),
323
+ };
324
+ return res.json({ ok: true, generatedAt: new Date(now()).toISOString(), ...brief, accounts, claudeWorkerAccount: claudeWorkerAccountId(), codexWorkerAccount: codexWorkerAccountId(), limits: dispatcher.limits, runs: dispatcher.list({ activeOnly: true }), totals: dispatcher.totals() });
283
325
  }
284
- // Reuse only while the catalog underneath is the same build (invalidate() drops it).
285
- if (!force && catalogCache && now() - catalogCache.at < 30_000 && (!catalog?.peek || catalog.peek() === catalogCache.base)) return res.json(catalogCache.value);
286
- const base = typeof buildCatalog === 'function' ? await buildCatalog({ force }) : {};
326
+ // The catalog says how long one of its builds is good for (one without a verified list is redone
327
+ // within seconds), so it is asked every time: this answer is reused only while it hands back the
328
+ // build the answer was made from (a rebuild or invalidate() gives another one).
329
+ const base = typeof buildCatalog === 'function' ? await buildCatalog({ force, deep }) : NO_CATALOG;
330
+ if (!force && catalogCache && catalogCache.base === base && now() - catalogCache.at < 30_000) return res.json(accountUsage ? { ...catalogCache.value, accounts: { ...catalogCache.value.accounts, ...accountUsage.list() } } : catalogCache.value);
287
331
  // Hidden rows stay server-side (view=manage lists them); the lists themselves ship.
288
332
  const { hiddenRows: _hiddenRows, ...visible } = base || {};
289
333
  const value = {
@@ -292,9 +336,11 @@ export function createAssistantApi({
292
336
  ...visible,
293
337
  accounts: {
294
338
  ...(base.accounts || {}),
295
- 'claude-code': claudeAccounts ? claudeAccounts.listForClient() : (base.accounts?.['claude-code'] || []),
296
- codex: codexAccounts ? codexAccounts.listForClient() : (base.accounts?.codex || []),
339
+ 'claude-code': accountUsage?.list()?.['claude-code'] || (claudeAccounts ? claudeAccounts.listForClient() : (base.accounts?.['claude-code'] || [])),
340
+ codex: accountUsage?.list()?.codex || (codexAccounts ? codexAccounts.listForClient() : (base.accounts?.codex || [])),
297
341
  },
342
+ claudeWorkerAccount: claudeWorkerAccountId(),
343
+ codexWorkerAccount: codexWorkerAccountId(),
298
344
  limits: dispatcher.limits,
299
345
  runs: dispatcher.list({ activeOnly: true }),
300
346
  totals: dispatcher.totals(),
@@ -458,18 +504,57 @@ export function createAssistantApi({
458
504
  } catch (error) { sendError(res, error, 400); }
459
505
  });
460
506
  // ── budget ─────────────────────────────────────────────────────────────────
507
+ /** A session's budget view (dispatcher → budget planner): the setting, spend, and the live split. */
508
+ function sessionBudgetView(sessionId) {
509
+ if (!sessionId) return null;
510
+ try { return dispatcher.sessionBudget?.(String(sessionId)) || null; } catch { return null; }
511
+ }
461
512
  function budgetPayload(sessionId = null) {
462
513
  const current = configStore.budget();
463
- const budget = Object.fromEntries(BUDGET_FIELDS.map((field) => [field, current[field]]));
464
- let session = null;
465
- if (sessionId) { try { session = dispatcher.sessionBudget?.(String(sessionId)) || null; } catch { session = null; } }
466
514
  let pricingStatus = null;
467
515
  try { pricingStatus = pricing?.status?.() || null; } catch { pricingStatus = null; }
468
516
  return {
469
- ok: true, version: current.version, budget, defaults: { ...BUDGET_DEFAULTS }, sources: current.sources, repairs: current.repairs,
470
- bounds: { minUsd: BUDGET_MIN_USD, maxUsd: BUDGET_MAX_USD }, session, pricing: pricingStatus, enforcement: BUDGET_ENFORCEMENT,
517
+ ok: true, version: current.version,
518
+ budget: { unlimited: current.unlimited === true, amountUsd: current.amountUsd, version: current.version, updatedAt: current.updatedAt || null, source: current.source || 'saved' },
519
+ defaults: { unlimited: false, amountUsd: BUDGET_DEFAULT_USD }, bounds: { minUsd: BUDGET_MIN_USD, maxUsd: BUDGET_MAX_USD }, warnRatio: BUDGET_WARN_RATIO,
520
+ session: sessionBudgetView(sessionId), pricing: pricingStatus, enforcement: BUDGET_ENFORCEMENT,
471
521
  };
472
522
  }
523
+ // ── User context (Settings → Automation behavior): the three layers and what a new conversation is told ──
524
+ function userContextPayload(req) {
525
+ let fallback = 'global';
526
+ try { fallback = String(defaultProject() || 'global'); } catch { fallback = 'global'; }
527
+ const project = String(req.query?.project || '').trim().slice(0, 200) || fallback;
528
+ return { ok: true, settings: configStore.userContext(), capabilities: userContext.capabilities(), preview: userContext.preview({ project }) };
529
+ }
530
+ const userContextReady = (res) => {
531
+ if (userContext && configStore?.userContext) return true;
532
+ let reason = null;
533
+ try { reason = typeof userContextError === 'function' ? userContextError() : userContextError; } catch { reason = null; }
534
+ res.status(503).json({ ok: false, code: 'USER_CONTEXT_UNAVAILABLE', error: 'User context is not available on this server', reason: reason ? String(reason).slice(0, 300) : null });
535
+ return false;
536
+ };
537
+ router.get('/user-context', (req, res) => {
538
+ if (!userContextReady(res)) return;
539
+ try { res.json(userContextPayload(req)); } catch (error) { sendError(res, error); }
540
+ });
541
+ router.put('/user-context', (req, res) => {
542
+ if (!userContextReady(res)) return;
543
+ if (!configStore.patchUserContext) return res.status(503).json({ ok: false, code: 'CONFIG_UNAVAILABLE', error: 'User context settings are read-only' });
544
+ if (callerContext(req).kind !== 'ui') return res.status(403).json({ ok: false, code: 'UI_ONLY', error: 'User context settings are changed from Settings only.' });
545
+ try {
546
+ const body = req.body || {};
547
+ const { project: _project, ...patch } = body.settings && typeof body.settings === 'object' ? body.settings : body;
548
+ configStore.patchUserContext(patch);
549
+ // Read live by the engine: the next turn (and the next brain start) follows it.
550
+ broadcastSync({ type: 'assistant:user-context-changed', settings: configStore.userContext() });
551
+ res.json(userContextPayload(req));
552
+ } catch (error) {
553
+ if (error?.code === 'USER_CONTEXT_INVALID') return res.status(400).json({ ok: false, code: error.code, error: error.message, message: error.message, field: error.field || null });
554
+ sendError(res, error, 400);
555
+ }
556
+ });
557
+
473
558
  router.get('/budget', (req, res) => {
474
559
  if (!configStore?.budget) return res.status(503).json({ ok: false, code: 'CONFIG_UNAVAILABLE', error: 'Budget settings are not available' });
475
560
  try { res.json(budgetPayload(req.query.sessionId || null)); } catch (error) { sendError(res, error); }
@@ -479,13 +564,16 @@ export function createAssistantApi({
479
564
  if (callerContext(req).kind !== 'ui') return res.status(403).json({ ok: false, code: 'UI_ONLY', error: 'Budget settings are changed from the assistant panel only.' });
480
565
  try {
481
566
  const body = req.body || {};
482
- const patch = body.budget && typeof body.budget === 'object' ? body.budget : {};
567
+ const patch = body.budget && typeof body.budget === 'object' && !Array.isArray(body.budget) ? body.budget : null;
568
+ if (!patch) return res.status(400).json({ ok: false, code: 'BUDGET_INVALID', error: 'Send { budget: { unlimited?, amountUsd? }, version }', message: 'Send { budget: { unlimited?, amountUsd? }, version }', field: 'budget', version: null });
483
569
  const expectedVersion = body.version ?? null;
484
570
  const saved = configStore.patchBudget(patch, { expectedVersion });
485
- // Applies now: the dispatcher re-reads its limits, the runtime re-checks every session.
571
+ // Applies now: the planner divides every open session's budget again, the runtime re-checks
572
+ // every session (held mailboxes, the brain's next turn) and every panel hears it.
486
573
  try { dispatcher.refreshLimits?.(); } catch {}
487
- try { runtime?.onBudgetChanged?.(); } catch {}
488
- broadcastSync({ type: 'assistant:budget-changed', version: saved.version });
574
+ if (runtime?.onBudgetChanged) { try { runtime.onBudgetChanged(); } catch {} }
575
+ else { try { dispatcher.budget?.onBudgetChanged?.(); } catch {} }
576
+ broadcastSync({ type: 'assistant:budget-changed', version: saved.version, budget: { unlimited: saved.unlimited, amountUsd: saved.amountUsd, version: saved.version, updatedAt: saved.updatedAt || null } });
489
577
  res.json(budgetPayload(body.sessionId || null));
490
578
  } catch (error) {
491
579
  if (error?.code === 'BUDGET_INVALID' || error?.code === 'VERSION_CONFLICT') {
@@ -505,6 +593,45 @@ export function createAssistantApi({
505
593
  try { res.json(runtime.gateCheck({ session: body.session, token: body.token, tool: body.tool, input, inputComplete: body.inputComplete === true, providerSessionId: body.providerSessionId || null, agent: body.agent || null, hello: body.hello === true, host: typeof body.host === 'string' ? body.host : null })); }
506
594
  catch (error) { sendError(res, error); }
507
595
  });
596
+ // ── whatsapp_send_file (docs/whatsapp.md → Sending files) ──────────────────
597
+ // A local file to the owner, for the WhatsApp conversation whose brain is calling. Who calls is the
598
+ // credential that brain alone holds (X-Synabun-Brain-Token: assistant-runtime.js mintBrainCredential),
599
+ // resolved to its session in constant time; never X-Synabun-Role or X-Synabun-Terminal, which any
600
+ // local process can send. A loopback socket and a localhost Host only (the checks of /api/whatsapp);
601
+ // guests were refused above. Only { path, caption?, as? }. Exactly one send, never retried.
602
+ const portOf = () => (typeof port === 'function' ? port() : port);
603
+ let hostGuard = null;
604
+ const localHostOnly = (req, res, next) => {
605
+ const current = portOf();
606
+ if (!hostGuard || hostGuard.port !== current) {
607
+ try { hostGuard = { port: current, check: requireLocalHost(current) }; } catch { return sendGuardError(res, 500, 'BAD_PORT', 'The file route has no valid port.'); }
608
+ }
609
+ return hostGuard.check(req, res, next);
610
+ };
611
+ router.post('/whatsapp/send-file', noStore, requireLocal, localHostOnly, requireJson, async (req, res) => {
612
+ let sessionId = null;
613
+ try { sessionId = runtime?.sessionForBrainCredential?.(req.get(BRAIN_CREDENTIAL_HEADER)) || null; } catch { sessionId = null; }
614
+ if (!sessionId) return sendGuardError(res, 403, 'BRAIN_CREDENTIAL_REQUIRED', 'Only the WhatsApp conversation\'s own Assistant sends files to the phone: nothing was sent.');
615
+ const body = req.body;
616
+ if (!body || typeof body !== 'object' || Array.isArray(body)) return sendGuardError(res, 400, 'BAD_REQUEST', 'Send { path, caption?, as? }.');
617
+ const extra = Object.keys(body).find((key) => !SEND_FILE_FIELDS.includes(key));
618
+ if (extra !== undefined) return sendGuardError(res, 400, 'BAD_REQUEST', `Unknown field "${extra.slice(0, 40)}": a file send takes path, caption and as only.`, extra.slice(0, 40));
619
+ let service = null;
620
+ try { service = typeof whatsapp === 'function' ? whatsapp() : whatsapp; } catch { service = null; }
621
+ if (typeof service?.sendFile !== 'function') return sendGuardError(res, 503, 'WHATSAPP_UNAVAILABLE', 'WhatsApp is not available on this computer: nothing was sent.');
622
+ try {
623
+ res.json(await service.sendFile(sessionId, body));
624
+ } catch (error) {
625
+ const status = Number(error?.status) || 500;
626
+ const out = { ok: false, code: error?.code || 'INTERNAL', error: error?.message || String(error) };
627
+ if (error?.field) out.field = error.field;
628
+ if (error?.uncertain === true) out.uncertain = true;
629
+ if (Number.isFinite(error?.retryAfterMs)) out.retryAfterMs = Math.max(0, Math.round(error.retryAfterMs));
630
+ if (status >= 500) log('assistant:whatsapp-send-file', `${sessionId}: ${out.code}${out.uncertain ? ' (delivery unknown)' : ''}`);
631
+ res.status(status).json(out);
632
+ }
633
+ });
634
+
508
635
  // agent_route: the brain proposes; the router approves, asks, or says pending.
509
636
  router.post('/route', orchestrationGuard, async (req, res) => {
510
637
  if (!requireRouter(res)) return;
@@ -726,13 +853,16 @@ export function createAssistantApi({
726
853
  res.json(brain ? brainWaitView(chat ? chatWaitView(result) : result) : result);
727
854
  } catch (error) { sendError(res, error); }
728
855
  });
729
- router.post('/runs/:runId/send', orchestrationGuard, (req, res) => {
856
+ router.post('/runs/:runId/send', orchestrationGuard, async (req, res) => {
730
857
  try {
731
858
  const body = req.body || {};
732
859
  const assistantSessionId = req.assistantCaller.assistantSessionId || dispatcher.get(req.params.runId)?.assistantSessionId || null;
733
860
  if (planRefusal(req, res, assistantSessionId, 'send a worker a message')) return;
734
861
  if (planState(req, assistantSessionId) === 'plan' && planSendRefusal(res, dispatcher.get(req.params.runId))) return;
735
862
  const origin = body.origin === 'user' ? 'user' : (req.assistantCaller.kind === 'ui' ? (body.origin || 'assistant') : 'assistant');
863
+ // A run whose provider share is used up: the session's unspent budget is divided again first, then
864
+ // sendTurn decides (it refuses with BUDGET_SHARE_EXHAUSTED when no money could move).
865
+ try { await dispatcher.coverFollowUp?.(req.params.runId); } catch { /* sendTurn decides on the shares as they are */ }
736
866
  res.json(dispatcher.sendTurn(req.params.runId, body.text ?? body.message, { origin, queue: body.queue !== false }));
737
867
  } catch (error) { sendError(res, error); }
738
868
  });
@@ -779,6 +909,12 @@ export function createAssistantApi({
779
909
  router.post('/runs/:runId/complete', orchestrationGuard, (req, res) => {
780
910
  try { res.json(dispatcher.complete(req.params.runId)); } catch (error) { sendError(res, error); }
781
911
  });
912
+ // The user closed the sidepanel tab of a worker that finished its task: completed, never "stopped
913
+ // by the user" (409 RUN_NOT_FINISHED for any other run, which the tab then stops as before).
914
+ router.post('/runs/:runId/tab-closed', (req, res) => {
915
+ if (callerContext(req).kind !== 'ui') return res.status(403).json({ ok: false, code: 'UI_ONLY', error: 'Only the sidepanel that closed the tab reports it.' });
916
+ try { res.json(dispatcher.completeClosedTab(req.params.runId)); } catch (error) { sendError(res, error); }
917
+ });
782
918
  router.post('/runs/:runId/focus', (req, res) => {
783
919
  const run = dispatcher.get(req.params.runId);
784
920
  if (!run) return res.status(404).json({ ok: false, code: 'RUN_NOT_FOUND', error: `Unknown run ${req.params.runId}` });
@@ -803,6 +939,14 @@ export function createAssistantApi({
803
939
  res.status(201).json({ ok: true, session });
804
940
  } catch (error) { sendError(res, error); }
805
941
  });
942
+ // A session's budget and its live split (the spend chip, the Budget modal): the same view the
943
+ // `assistant:budget` packet carries.
944
+ router.get('/sessions/:id/budget', (req, res) => {
945
+ if (!requireRuntime(res)) return;
946
+ const id = req.params.id;
947
+ if (!runtime.getSession(id, { transcript: false })) return res.status(404).json({ ok: false, code: 'SESSION_NOT_FOUND', error: `Unknown assistant session ${id}` });
948
+ try { res.json({ ok: true, budget: sessionBudgetView(id) }); } catch (error) { sendError(res, error); }
949
+ });
806
950
  router.get('/sessions/:id/usage', (req, res) => {
807
951
  if (!requireRuntime(res)) return;
808
952
  const id = req.params.id;
@@ -818,9 +962,10 @@ export function createAssistantApi({
818
962
  const session = usage?.sessionView(id) ?? null;
819
963
  // Whether work is in progress is the runtime's to say (a turn that has no tokens yet).
820
964
  const head = session ? runtime.usageView?.(id, 'current')?.session || null : null;
821
- return res.json({ ok: true, session: session && head ? { ...session, live: head.live, fidelity: head.fidelity } : session, ...(run ? { run: usage?.runView?.(id, run) ?? null } : {}) });
965
+ return res.json({ ok: true, session: session && head ? { ...session, live: head.live, fidelity: head.fidelity } : session, ...(run ? { run: usage?.runView?.(id, run) ?? null } : {}), budget: sessionBudgetView(id) });
822
966
  }
823
- res.json({ ok: true, usage: runtime.usageView?.(id, task) ?? usage?.taskView(id, task) ?? null });
967
+ // The session budget rides along (agent_usage: the mode and each provider's share / spent / left).
968
+ res.json({ ok: true, usage: runtime.usageView?.(id, task) ?? usage?.taskView(id, task) ?? null, budget: sessionBudgetView(id) });
824
969
  } catch (error) { sendError(res, error); }
825
970
  });
826
971
  router.get('/sessions/:id', (req, res) => {
@@ -835,6 +980,10 @@ export function createAssistantApi({
835
980
  if (req.body && 'computerUse' in req.body && (req.get('x-synabun-terminal') || req.get('x-synabun-desktop-grant'))) {
836
981
  return res.status(403).json({ ok: false, code: 'UI_ONLY', error: 'Only the user can switch computer use on or off.' });
837
982
  }
983
+ // Test density is the user's setting too: an agent cannot raise or lower it.
984
+ if (req.body && 'testDensity' in req.body && (req.get('x-synabun-terminal') || req.get('x-synabun-desktop-grant'))) {
985
+ return res.status(403).json({ ok: false, code: 'UI_ONLY', error: 'Only the user can change the Test density.' });
986
+ }
838
987
  try {
839
988
  const session = await runtime.updateSession(req.params.id, req.body || {});
840
989
  if (!session) return res.status(404).json({ ok: false, code: 'SESSION_NOT_FOUND', error: `Unknown assistant session ${req.params.id}` });
@@ -884,25 +1033,51 @@ export function createAssistantApi({
884
1033
  return (accountId) => {
885
1034
  if (dispatcher.accountInUse(provider, accountId)) return `Stop the dispatched ${provider} runs using account ${accountId} first`;
886
1035
  if (runtime?.accountInUse?.(provider, accountId)) return `Close the assistant sessions using account ${accountId} first`;
1036
+ if (provider === 'claude-code' && typeof claudeAccountInUse === 'function') return claudeAccountInUse(accountId) || null;
1037
+ if (provider === 'codex' && typeof codexAccountInUse === 'function') return codexAccountInUse(accountId) || null;
887
1038
  return null;
888
1039
  };
889
1040
  }
890
- router.get('/claude/accounts', (req, res) => {
891
- if (!claudeAccounts) return res.status(503).json({ ok: false, code: 'ACCOUNTS_UNAVAILABLE', error: 'Claude accounts are not available' });
892
- res.json({ ok: true, accounts: claudeAccounts.listForClient() });
1041
+ // The Claude account a Claude Code worker runs under when its dispatch names none (null = Default).
1042
+ function claudeWorkerAccountId() {
1043
+ try { return configStore?.workers?.().claudeAccount || null; } catch { return null; }
1044
+ }
1045
+ // The same for Codex workers.
1046
+ function codexWorkerAccountId() {
1047
+ try { return configStore?.workers?.().codexAccount || null; } catch { return null; }
1048
+ }
1049
+ const claudeUnavailable = (res) => res.status(503).json({ ok: false, code: 'ACCOUNTS_UNAVAILABLE', error: 'Claude accounts are not available' });
1050
+ // Every Claude account as every surface shows it (lib/claude-accounts.js describe()): mode, signed in,
1051
+ // identity (an email or a masked key, never a key). `?refresh=1` asks Claude Code about each first.
1052
+ router.get('/claude/accounts', async (req, res) => {
1053
+ if (!claudeAccounts) return claudeUnavailable(res);
1054
+ try {
1055
+ if (req.query.refresh === '1' || req.query.refresh === 'true') await claudeAccounts.refreshStatus?.(null, { force: true });
1056
+ else Promise.resolve(claudeAccounts.refreshStatus?.()).catch(() => {});
1057
+ await accountUsage?.refresh('claude-code');
1058
+ res.json({ ok: true, accounts: accountUsage?.list()?.['claude-code'] || claudeAccounts.listForClient(), workerAccountId: claudeWorkerAccountId() });
1059
+ } catch (error) { sendError(res, error); }
893
1060
  });
1061
+ // Create an account. `login: true` also opens its Claude sign-in in a SynaBun terminal; otherwise the
1062
+ // caller offers the two ways to sign it in (Claude, or an API key pasted into it).
894
1063
  router.post('/claude/accounts', async (req, res) => {
895
- if (!claudeAccounts) return res.status(503).json({ ok: false, code: 'ACCOUNTS_UNAVAILABLE', error: 'Claude accounts are not available' });
1064
+ if (!claudeAccounts) return claudeUnavailable(res);
896
1065
  try {
897
1066
  const account = claudeAccounts.create({ label: req.body?.label });
898
1067
  let login = null;
899
- if (req.body?.login !== false && typeof createLoginTerminal === 'function') {
900
- login = await startClaudeLogin(account.id);
901
- }
1068
+ if (req.body?.login === true) login = await startClaudeLogin(account.id);
1069
+ catalogCache = null;
902
1070
  res.status(201).json({ ok: true, account: claudeAccounts.listForClient().find((row) => row.id === account.id) || account, login });
903
1071
  } catch (error) { sendError(res, error); }
904
1072
  });
905
1073
  async function startClaudeLogin(accountId) {
1074
+ // Claude Code's own `auth login` under the account's config directory, in a SynaBun terminal (the
1075
+ // panel's sign-in). The account list says when it is signed in (claude:accounts-changed).
1076
+ if (typeof claudeSignIn === 'function') {
1077
+ const terminal = await claudeSignIn(accountId);
1078
+ if (terminal?.status === 'error') throw Object.assign(new Error(terminal.error || 'Could not open the sign-in terminal.'), { status: 500, code: 'LOGIN_FAILED' });
1079
+ return { mode: 'terminal', terminalSessionId: terminal?.sessionId || null, status: terminal?.status || 'pending', profile: 'shell' };
1080
+ }
906
1081
  const command = claudeAccounts.loginCommand(accountId);
907
1082
  const terminal = await createLoginTerminal({ env: command.env, cwd: command.cwd, profile: 'claude-code', accountId, command: command.command });
908
1083
  claudeAccounts.watchLogin(accountId, {
@@ -914,24 +1089,34 @@ export function createAssistantApi({
914
1089
  return { mode: 'terminal', terminalSessionId: terminal?.terminalSessionId || terminal?.id || null, profile: terminal?.profile || 'claude-code' };
915
1090
  }
916
1091
  router.post('/claude/accounts/:id/login', async (req, res) => {
917
- if (!claudeAccounts) return res.status(503).json({ ok: false, code: 'ACCOUNTS_UNAVAILABLE', error: 'Claude accounts are not available' });
918
- if (typeof createLoginTerminal !== 'function') return res.status(501).json({ ok: false, code: 'LOGIN_UNAVAILABLE', error: 'Login terminals are not wired' });
1092
+ if (!claudeAccounts) return claudeUnavailable(res);
1093
+ if (typeof claudeSignIn !== 'function' && typeof createLoginTerminal !== 'function') return res.status(501).json({ ok: false, code: 'LOGIN_UNAVAILABLE', error: 'Login terminals are not wired' });
919
1094
  try {
920
- if (!claudeAccounts.get(req.params.id)) return res.status(404).json({ ok: false, code: 'ACCOUNT_NOT_FOUND', error: `Unknown Claude account ${req.params.id}` });
1095
+ const row = claudeAccounts.get(req.params.id);
1096
+ if (!row || row.id !== req.params.id) return res.status(404).json({ ok: false, code: 'ACCOUNT_NOT_FOUND', error: `Unknown Claude account ${req.params.id}` });
921
1097
  res.json({ ok: true, login: await startClaudeLogin(req.params.id) });
922
1098
  } catch (error) { sendError(res, error); }
923
1099
  });
924
- router.patch('/claude/accounts/:id', (req, res) => {
925
- if (!claudeAccounts) return res.status(503).json({ ok: false, code: 'ACCOUNTS_UNAVAILABLE', error: 'Claude accounts are not available' });
1100
+ // Rename (`label`) and/or choose what the account's sessions use (`mode`: 'subscription' | 'apiKey').
1101
+ router.patch('/claude/accounts/:id', async (req, res) => {
1102
+ if (!claudeAccounts) return claudeUnavailable(res);
926
1103
  try {
927
- const account = claudeAccounts.rename(req.params.id, req.body?.label);
1104
+ const row = claudeAccounts.get(req.params.id);
1105
+ if (!row || row.id !== req.params.id) return res.status(404).json({ ok: false, code: 'ACCOUNT_NOT_FOUND', error: `Unknown Claude account ${req.params.id}` });
1106
+ const { label, mode } = req.body || {};
1107
+ if (label === undefined && mode === undefined) return res.status(400).json({ ok: false, code: 'NOTHING_TO_CHANGE', error: 'Send a label or a mode.' });
1108
+ if (label !== undefined) claudeAccounts.rename(req.params.id, label);
1109
+ if (mode !== undefined) {
1110
+ if (typeof claudeSetMode === 'function') await claudeSetMode(req.params.id, mode);
1111
+ else claudeAccounts.setMode(req.params.id, mode);
1112
+ }
928
1113
  catalogCache = null;
929
1114
  broadcastSync({ type: 'assistant:accounts-changed', provider: 'claude-code', accountId: req.params.id });
930
- res.json({ ok: true, account });
1115
+ res.json({ ok: true, account: claudeAccounts.describeForClient ? claudeAccounts.describeForClient(req.params.id) : claudeAccounts.get(req.params.id) });
931
1116
  } catch (error) { sendError(res, error); }
932
1117
  });
933
1118
  router.delete('/claude/accounts/:id', (req, res) => {
934
- if (!claudeAccounts) return res.status(503).json({ ok: false, code: 'ACCOUNTS_UNAVAILABLE', error: 'Claude accounts are not available' });
1119
+ if (!claudeAccounts) return claudeUnavailable(res);
935
1120
  try {
936
1121
  const result = claudeAccounts.remove(req.params.id, { inUse: accountInUse('claude-code') });
937
1122
  catalogCache = null;
@@ -939,30 +1124,89 @@ export function createAssistantApi({
939
1124
  res.json({ ok: true, ...result });
940
1125
  } catch (error) { sendError(res, error); }
941
1126
  });
942
- router.get('/codex/accounts', (req, res) => {
943
- if (!codexAccounts) return res.status(503).json({ ok: false, code: 'ACCOUNTS_UNAVAILABLE', error: 'Codex accounts are not available' });
944
- res.json({ ok: true, accounts: codexAccounts.listForClient() });
1127
+ // The account a Claude Code worker uses when its dispatch names none. Signed-out accounts are refused.
1128
+ router.put('/claude/worker-account', (req, res) => {
1129
+ if (!claudeAccounts || !configStore?.patchWorkers) return claudeUnavailable(res);
1130
+ try {
1131
+ const wanted = req.body?.accountId === null || req.body?.accountId === undefined || req.body?.accountId === '' ? 'default' : String(req.body.accountId);
1132
+ const row = claudeAccounts.get(wanted);
1133
+ if (!row || row.id !== wanted) return res.status(404).json({ ok: false, code: 'ACCOUNT_NOT_FOUND', error: `Unknown Claude account ${wanted}` });
1134
+ const unusable = claudeAccounts.unusableReason?.(wanted) || '';
1135
+ if (unusable) return res.status(409).json({ ok: false, code: 'ACCOUNT_SIGNED_OUT', error: unusable });
1136
+ const workers = configStore.patchWorkers({ claudeAccount: wanted });
1137
+ catalogCache = null;
1138
+ broadcastSync({ type: 'assistant:accounts-changed', provider: 'claude-code', accountId: wanted, worker: true });
1139
+ res.json({ ok: true, workerAccountId: workers.claudeAccount });
1140
+ } catch (error) { sendError(res, error); }
1141
+ });
1142
+ const codexUnavailable = (res) => res.status(503).json({ ok: false, code: 'ACCOUNTS_UNAVAILABLE', error: 'Codex accounts are not available' });
1143
+ // Every Codex account as every surface shows it (lib/codex-accounts.js describe()): mode, signed in,
1144
+ // identity (an email or a masked key, never a key). `?refresh=1` asks Codex about each own store first.
1145
+ router.get('/codex/accounts', async (req, res) => {
1146
+ if (!codexAccounts) return codexUnavailable(res);
1147
+ try {
1148
+ if (req.query.refresh === '1' || req.query.refresh === 'true') await codexAccounts.refreshStatus?.(null, { force: true });
1149
+ else Promise.resolve(codexAccounts.refreshStatus?.()).catch(() => {});
1150
+ await accountUsage?.refresh('codex');
1151
+ res.json({ ok: true, accounts: accountUsage?.list()?.codex || codexAccounts.listForClient(), workerAccountId: codexWorkerAccountId() });
1152
+ } catch (error) { sendError(res, error); }
945
1153
  });
1154
+ // `codex login` under the account's CODEX_HOME, in a SynaBun terminal (the panel's sign-in). The account
1155
+ // list says when it is signed in (codex:accounts-changed).
1156
+ async function startCodexLogin(accountId) {
1157
+ if (typeof codexSignIn !== 'function') throw Object.assign(new Error('Login terminals are not wired'), { status: 501, code: 'LOGIN_UNAVAILABLE' });
1158
+ const terminal = await codexSignIn(accountId);
1159
+ if (terminal?.status === 'error') throw Object.assign(new Error(terminal.error || 'Could not open the sign-in terminal.'), { status: 500, code: 'LOGIN_FAILED' });
1160
+ return { mode: 'terminal', terminalSessionId: terminal?.sessionId || null, status: terminal?.status || 'pending', profile: 'shell' };
1161
+ }
1162
+ // Create an account. `login: true` also opens its ChatGPT sign-in in a SynaBun terminal; otherwise the
1163
+ // caller offers the two ways to sign it in (ChatGPT, or an API key pasted into it).
1164
+ router.post('/codex/accounts', async (req, res) => {
1165
+ if (!codexAccounts?.create) return codexUnavailable(res);
1166
+ try {
1167
+ const account = codexAccounts.create({ label: req.body?.label });
1168
+ const login = req.body?.login === true ? await startCodexLogin(account.id) : null;
1169
+ catalogCache = null;
1170
+ res.status(201).json({ ok: true, account: codexAccounts.describeForClient?.(account.id) || account, login });
1171
+ } catch (error) { sendError(res, error); }
1172
+ });
1173
+ // Older pages: create the account and open its ChatGPT sign-in at once.
946
1174
  router.post('/codex/accounts/add-start', async (req, res) => {
947
- if (!codexAccounts?.addStart) return res.status(501).json({ ok: false, code: 'NOT_IMPLEMENTED', error: 'Add a Codex account from the Codex sidepanel account menu' });
1175
+ if (!codexAccounts?.create) return codexUnavailable(res);
948
1176
  try {
949
- const result = await codexAccounts.addStart(req.body || {});
1177
+ const account = codexAccounts.create({ label: req.body?.label });
1178
+ const login = await startCodexLogin(account.id).catch((error) => ({ error: error.message }));
950
1179
  catalogCache = null;
951
- broadcastSync({ type: 'assistant:accounts-changed', provider: 'codex', accountId: result?.accountId || null });
952
- res.status(201).json({ ok: true, ...result });
1180
+ res.status(201).json({ ok: true, accountId: account.id, account: codexAccounts.describeForClient?.(account.id) || account, login });
1181
+ } catch (error) { sendError(res, error); }
1182
+ });
1183
+ router.post('/codex/accounts/:id/login', async (req, res) => {
1184
+ if (!codexAccounts) return codexUnavailable(res);
1185
+ try {
1186
+ const row = codexAccounts.describe?.(req.params.id);
1187
+ if (!row || row.id !== req.params.id) return res.status(404).json({ ok: false, code: 'ACCOUNT_NOT_FOUND', error: `Unknown Codex account ${req.params.id}` });
1188
+ res.json({ ok: true, login: await startCodexLogin(req.params.id) });
953
1189
  } catch (error) { sendError(res, error); }
954
1190
  });
955
- router.patch('/codex/accounts/:id', (req, res) => {
956
- if (!codexAccounts?.rename) return res.status(503).json({ ok: false, code: 'ACCOUNTS_UNAVAILABLE', error: 'Codex accounts are not available' });
1191
+ // Rename (`label`) and/or choose what the account's sessions use (`mode`: 'subscription' (ChatGPT) | 'apiKey').
1192
+ router.patch('/codex/accounts/:id', async (req, res) => {
1193
+ if (!codexAccounts) return codexUnavailable(res);
957
1194
  try {
958
- const account = codexAccounts.rename(req.params.id, req.body?.label);
1195
+ const row = codexAccounts.describe?.(req.params.id);
1196
+ if (!row || row.id !== req.params.id) return res.status(404).json({ ok: false, code: 'ACCOUNT_NOT_FOUND', error: `Unknown Codex account ${req.params.id}` });
1197
+ const { label, mode } = req.body || {};
1198
+ if (label !== undefined) codexAccounts.rename(req.params.id, label);
1199
+ if (mode !== undefined) {
1200
+ if (typeof codexSetMode === 'function') await codexSetMode(req.params.id, mode);
1201
+ else codexAccounts.setMode(req.params.id, mode);
1202
+ }
959
1203
  catalogCache = null;
960
1204
  broadcastSync({ type: 'assistant:accounts-changed', provider: 'codex', accountId: req.params.id });
961
- res.json({ ok: true, account });
1205
+ res.json({ ok: true, account: codexAccounts.describeForClient(req.params.id) });
962
1206
  } catch (error) { sendError(res, error); }
963
1207
  });
964
1208
  router.delete('/codex/accounts/:id', (req, res) => {
965
- if (!codexAccounts?.remove) return res.status(503).json({ ok: false, code: 'ACCOUNTS_UNAVAILABLE', error: 'Codex accounts are not available' });
1209
+ if (!codexAccounts?.remove) return codexUnavailable(res);
966
1210
  try {
967
1211
  const result = codexAccounts.remove(req.params.id, { inUse: accountInUse('codex') });
968
1212
  catalogCache = null;
@@ -970,6 +1214,21 @@ export function createAssistantApi({
970
1214
  res.json({ ok: true, ...result });
971
1215
  } catch (error) { sendError(res, error); }
972
1216
  });
1217
+ // The account a Codex worker uses when its dispatch names none. Signed-out accounts are refused.
1218
+ router.put('/codex/worker-account', (req, res) => {
1219
+ if (!codexAccounts || !configStore?.patchWorkers) return codexUnavailable(res);
1220
+ try {
1221
+ const wanted = req.body?.accountId === null || req.body?.accountId === undefined || req.body?.accountId === '' ? 'default' : String(req.body.accountId);
1222
+ const row = codexAccounts.describe?.(wanted);
1223
+ if (!row || row.id !== wanted) return res.status(404).json({ ok: false, code: 'ACCOUNT_NOT_FOUND', error: `Unknown Codex account ${wanted}` });
1224
+ const unusable = codexAccounts.unusableReason?.(wanted) || '';
1225
+ if (unusable) return res.status(409).json({ ok: false, code: 'ACCOUNT_SIGNED_OUT', error: unusable });
1226
+ const workers = configStore.patchWorkers({ codexAccount: wanted });
1227
+ catalogCache = null;
1228
+ broadcastSync({ type: 'assistant:accounts-changed', provider: 'codex', accountId: wanted, worker: true });
1229
+ res.json({ ok: true, workerAccountId: workers.codexAccount });
1230
+ } catch (error) { sendError(res, error); }
1231
+ });
973
1232
 
974
1233
  // ── sidepanel tab registry (UI heartbeat) ──────────────────────────────────
975
1234
  function listTabs() {