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
@@ -1,19 +1,22 @@
1
1
  // ═══════════════════════════════════════════
2
- // SynaBun — Assistant budgets (caps · list prices · Codex usage)
2
+ // SynaBun — Assistant budget (the one setting · list prices · Codex usage)
3
3
  // ═══════════════════════════════════════════
4
4
  //
5
- // The money rails of the central assistant, in one place:
5
+ // One setting for every Assistant session: { unlimited, amountUsd, version }.
6
+ // It lives in assistant-config.json under `budget` and is read live. Every new
7
+ // session starts with it; a change applies to the open sessions at once.
6
8
  //
7
- // defaultRunUsd per-run cap a dispatch gets when the brain names none
8
- // maxRunUsd largest per-run cap a brain may ask for (budget_usd)
9
- // sessionWarnUsd one warning to the brain and the user at this session spend
10
- // sessionHardUsd session spend (brain + every run) that stops everything
11
- // brainUsd the central brain's own spend in one session
9
+ // unlimited false the session may spend `amountUsd` (brain + every run, at
10
+ // list price). The warning is fixed at 80 %; at 100 % the
11
+ // session stops. assistant-budget-split.js divides what is
12
+ // left across the providers (Jev or a fixed heuristic).
13
+ // unlimited true No limit: no caps, no shares, no Jev call. Spend is still
14
+ // metered.
12
15
  //
13
- // They live where the dispatcher and the runtime always read them, so a hand
14
- // edit keeps working: `limits.defaultBudgetUsd`, `limits.maxBudgetUsd`,
15
- // `limits.sessionWarnUsd` (older files: 80 % of `limits.sessionSoftBudgetUsd`),
16
- // `limits.sessionHardBudgetUsd`, `brains.claude.maxBudgetUsd`.
16
+ // Older files kept five caps in `limits` (default/largest per run, session
17
+ // warning, session hard cap) and `brains.claude.maxBudgetUsd`. They are read
18
+ // once (migrateBudget): the session hard cap becomes the amount, the rest is
19
+ // dropped.
17
20
  //
18
21
  // Prices: Claude and OpenCode report what a turn cost. Codex reports tokens
19
22
  // only, so its runs are priced from the list-price table (assistant-pricing.js,
@@ -28,17 +31,15 @@ import { homedir } from 'node:os';
28
31
  import { join } from 'node:path';
29
32
  import { PRICING_SOURCES, PRICING_VERIFIED, listPrice, pricingRows } from './assistant-pricing.js';
30
33
 
31
- export const BUDGET_FIELDS = Object.freeze(['defaultRunUsd', 'maxRunUsd', 'sessionWarnUsd', 'sessionHardUsd', 'brainUsd']);
32
- export const BUDGET_DEFAULTS = Object.freeze({ defaultRunUsd: 5, maxRunUsd: 25, sessionWarnUsd: 8, sessionHardUsd: 25, brainUsd: 10 });
33
34
  export const BUDGET_MIN_USD = 0.1;
34
35
  export const BUDGET_MAX_USD = 1000;
35
- export const BUDGET_LABELS = Object.freeze({
36
- defaultRunUsd: 'Default per-run cap',
37
- maxRunUsd: 'Largest per-run cap',
38
- sessionWarnUsd: 'Session warning',
39
- sessionHardUsd: 'Session hard cap',
40
- brainUsd: 'Brain cap',
41
- });
36
+ export const BUDGET_DEFAULT_USD = 25;
37
+ /** The one warning comes at this share of the budget. */
38
+ export const BUDGET_WARN_RATIO = 0.8;
39
+ export const BUDGET_FIELDS = Object.freeze(['unlimited', 'amountUsd']);
40
+ export const BUDGET_DEFAULTS = Object.freeze({ unlimited: false, amountUsd: BUDGET_DEFAULT_USD });
41
+ /** The keys of the old five-cap budget (`limits.*`, `brains.claude.maxBudgetUsd`), dropped by the migration. */
42
+ export const LEGACY_LIMIT_KEYS = Object.freeze(['defaultBudgetUsd', 'maxBudgetUsd', 'sessionWarnUsd', 'sessionSoftBudgetUsd', 'sessionHardBudgetUsd', 'budgetVersion', 'budgetUpdatedAt']);
42
43
 
43
44
  function num(value) {
44
45
  if (value === null || value === undefined || value === '' || typeof value === 'boolean') return null;
@@ -47,6 +48,8 @@ function num(value) {
47
48
  }
48
49
  function obj(value) { return value && typeof value === 'object' && !Array.isArray(value) ? value : {}; }
49
50
  export function roundUsd(value) { return Math.round(Number(value) * 100) / 100; }
51
+ /** Whole cents, never more than `value` (a budget never rounds up). */
52
+ export function floorCents(value) { return Math.max(0, Math.floor(Number(value) * 100 + 1e-6)); }
50
53
 
51
54
  function budgetError(field, message) {
52
55
  const error = new Error(message);
@@ -55,109 +58,105 @@ function budgetError(field, message) {
55
58
  error.status = 400;
56
59
  return error;
57
60
  }
61
+ const inRange = (n) => n !== null && n >= BUDGET_MIN_USD && n <= BUDGET_MAX_USD;
58
62
 
59
- /** The raw values a config file holds (null = not set). */
60
- export function storedBudget(raw = {}) {
63
+ /** Whether a config file still carries the old five-cap budget and no `budget` of its own. */
64
+ export function needsBudgetMigration(raw = {}) {
65
+ if (raw?.budget && typeof raw.budget === 'object' && !Array.isArray(raw.budget)) return false;
61
66
  const limits = obj(raw?.limits);
62
- const soft = num(limits.sessionSoftBudgetUsd);
63
- const warn = num(limits.sessionWarnUsd);
64
- return {
65
- defaultRunUsd: num(limits.defaultBudgetUsd),
66
- maxRunUsd: num(limits.maxBudgetUsd),
67
- sessionWarnUsd: warn ?? (soft !== null ? roundUsd(soft * 0.8) : null),
68
- sessionHardUsd: num(limits.sessionHardBudgetUsd),
69
- brainUsd: num(obj(obj(raw?.brains).claude).maxBudgetUsd),
70
- };
67
+ return LEGACY_LIMIT_KEYS.some((key) => Object.hasOwn(limits, key)) || Object.hasOwn(obj(obj(raw?.brains).claude), 'maxBudgetUsd');
71
68
  }
72
69
 
73
70
  /**
74
- * The invariant that fails first, or null: default ≤ largest ≤ hard,
75
- * warning < hard, brain ≤ hard.
71
+ * The budget a config file holds: its `budget` when it has one, else what an
72
+ * older file says (the saved session hard cap, else the default).
73
+ * → { unlimited, amountUsd, version, updatedAt, source: 'saved' | 'migrated' | 'default' }
76
74
  */
77
- export function budgetInvariantError(b) {
78
- if (b.maxRunUsd > b.sessionHardUsd) return { field: 'maxRunUsd', message: `${BUDGET_LABELS.maxRunUsd} ($${b.maxRunUsd}) cannot exceed the ${BUDGET_LABELS.sessionHardUsd.toLowerCase()} ($${b.sessionHardUsd}).` };
79
- if (b.defaultRunUsd > b.maxRunUsd) return { field: 'defaultRunUsd', message: `${BUDGET_LABELS.defaultRunUsd} ($${b.defaultRunUsd}) cannot exceed the ${BUDGET_LABELS.maxRunUsd.toLowerCase()} ($${b.maxRunUsd}).` };
80
- if (b.sessionWarnUsd >= b.sessionHardUsd) return { field: 'sessionWarnUsd', message: `${BUDGET_LABELS.sessionWarnUsd} ($${b.sessionWarnUsd}) must be below the ${BUDGET_LABELS.sessionHardUsd.toLowerCase()} ($${b.sessionHardUsd}).` };
81
- if (b.brainUsd > b.sessionHardUsd) return { field: 'brainUsd', message: `${BUDGET_LABELS.brainUsd} ($${b.brainUsd}) cannot exceed the ${BUDGET_LABELS.sessionHardUsd.toLowerCase()} ($${b.sessionHardUsd}).` };
82
- return null;
75
+ export function effectiveBudget(raw = {}) {
76
+ const stored = raw?.budget && typeof raw.budget === 'object' && !Array.isArray(raw.budget) ? raw.budget : null;
77
+ if (stored) {
78
+ const amount = num(stored.amountUsd);
79
+ return {
80
+ unlimited: stored.unlimited === true,
81
+ amountUsd: inRange(amount) ? roundUsd(amount) : BUDGET_DEFAULT_USD,
82
+ version: Math.max(0, Math.floor(num(stored.version) || 0)),
83
+ updatedAt: typeof stored.updatedAt === 'string' ? stored.updatedAt : null,
84
+ source: 'saved',
85
+ };
86
+ }
87
+ const limits = obj(raw?.limits);
88
+ const hard = num(limits.sessionHardBudgetUsd);
89
+ const migrated = needsBudgetMigration(raw);
90
+ return {
91
+ unlimited: false,
92
+ // The saved session hard cap is the amount (a hand edit outside the bounds is brought inside them).
93
+ amountUsd: hard !== null && hard > 0 ? roundUsd(Math.min(BUDGET_MAX_USD, Math.max(BUDGET_MIN_USD, hard))) : BUDGET_DEFAULT_USD,
94
+ version: Math.max(0, Math.floor(num(limits.budgetVersion) || 0)),
95
+ updatedAt: typeof limits.budgetUpdatedAt === 'string' ? limits.budgetUpdatedAt : null,
96
+ source: migrated ? 'migrated' : 'default',
97
+ };
83
98
  }
84
99
 
85
- /**
86
- * What applies now: each saved value in range, else its default; a hand edit
87
- * that breaks an invariant is repaired (and listed in `repairs`), never trusted.
88
- */
89
- export function effectiveBudget(raw = {}) {
90
- const stored = storedBudget(raw);
91
- const out = {};
92
- const sources = {};
93
- for (const field of BUDGET_FIELDS) {
94
- const value = stored[field];
95
- const ok = value !== null && value >= BUDGET_MIN_USD && value <= BUDGET_MAX_USD;
96
- out[field] = ok ? roundUsd(value) : BUDGET_DEFAULTS[field];
97
- sources[field] = !ok ? 'default' : (field === 'sessionWarnUsd' && num(obj(raw?.limits).sessionWarnUsd) === null ? 'legacy' : 'saved');
100
+ /** A config object without the old five-cap keys (every other key kept). */
101
+ export function withoutLegacyBudget(raw = {}) {
102
+ const base = { ...obj(raw) };
103
+ if (base.limits && typeof base.limits === 'object') {
104
+ const limits = { ...base.limits };
105
+ for (const key of LEGACY_LIMIT_KEYS) delete limits[key];
106
+ base.limits = limits;
98
107
  }
99
- const repairs = [];
100
- for (let guard = 0; guard < BUDGET_FIELDS.length; guard += 1) {
101
- const broken = budgetInvariantError(out);
102
- if (!broken) break;
103
- repairs.push(broken.field);
104
- if (broken.field === 'maxRunUsd') out.maxRunUsd = out.sessionHardUsd;
105
- else if (broken.field === 'defaultRunUsd') out.defaultRunUsd = out.maxRunUsd;
106
- else if (broken.field === 'sessionWarnUsd') out.sessionWarnUsd = roundUsd(out.sessionHardUsd * 0.8);
107
- else if (broken.field === 'brainUsd') out.brainUsd = out.sessionHardUsd;
108
+ const brains = obj(base.brains);
109
+ if (brains.claude && typeof brains.claude === 'object' && Object.hasOwn(brains.claude, 'maxBudgetUsd')) {
110
+ const { maxBudgetUsd: _cap, ...claude } = brains.claude;
111
+ base.brains = { ...brains, claude };
108
112
  }
109
- return { ...out, sources, repairs };
113
+ return base;
110
114
  }
111
115
 
112
116
  /**
113
- * Validate a patch against the current values and return the full next set.
114
- * `null` resets a field to its default. Throws BUDGET_INVALID (status 400,
115
- * `field`) on a bad value or a broken invariant.
117
+ * Validate a patch ({ unlimited?, amountUsd? }) against the current budget and
118
+ * return the full next one. Throws BUDGET_INVALID (status 400, `field`) on an
119
+ * unknown field, a bad value or an empty patch. Amounts are kept in cents.
116
120
  */
117
121
  export function validateBudgetPatch(patch = {}, current = BUDGET_DEFAULTS) {
118
122
  if (!patch || typeof patch !== 'object' || Array.isArray(patch)) throw budgetError('budget', 'budget must be an object');
119
- const next = {};
120
- for (const field of BUDGET_FIELDS) next[field] = num(current?.[field]) ?? BUDGET_DEFAULTS[field];
123
+ const next = { unlimited: current?.unlimited === true, amountUsd: inRange(num(current?.amountUsd)) ? roundUsd(current.amountUsd) : BUDGET_DEFAULT_USD };
124
+ let changed = 0;
121
125
  for (const [field, value] of Object.entries(patch)) {
122
- if (!BUDGET_FIELDS.includes(field)) throw budgetError(field, `Unknown budget field "${field}"`);
123
- if (value === null) { next[field] = BUDGET_DEFAULTS[field]; continue; }
126
+ if (!BUDGET_FIELDS.includes(field)) throw budgetError(field, `Unknown budget field "${field}". Send unlimited and/or amountUsd.`);
127
+ changed += 1;
128
+ if (field === 'unlimited') {
129
+ if (typeof value !== 'boolean') throw budgetError('unlimited', 'unlimited must be true or false');
130
+ next.unlimited = value;
131
+ continue;
132
+ }
124
133
  const n = typeof value === 'number' || (typeof value === 'string' && value.trim() !== '') ? Number(value) : NaN;
125
- if (!Number.isFinite(n)) throw budgetError(field, `${BUDGET_LABELS[field]} must be an amount in US dollars`);
126
- if (n < BUDGET_MIN_USD || n > BUDGET_MAX_USD) throw budgetError(field, `${BUDGET_LABELS[field]} must be between $${BUDGET_MIN_USD.toFixed(2)} and $${BUDGET_MAX_USD}`);
127
- next[field] = roundUsd(n);
134
+ if (!Number.isFinite(n)) throw budgetError('amountUsd', 'The budget must be an amount in US dollars');
135
+ if (n < BUDGET_MIN_USD || n > BUDGET_MAX_USD) throw budgetError('amountUsd', `The budget must be between $${BUDGET_MIN_USD.toFixed(2)} and $${BUDGET_MAX_USD}`);
136
+ next.amountUsd = roundUsd(n);
128
137
  }
129
- const broken = budgetInvariantError(next);
130
- if (broken) throw budgetError(broken.field, broken.message);
138
+ if (!changed) throw budgetError('budget', 'Nothing to change: send unlimited and/or amountUsd');
131
139
  return next;
132
140
  }
133
141
 
134
- /** A config object with `budget` written into the keys its readers use (every other key kept). */
142
+ /** A config object with `budget` written (the old caps dropped, every other key kept). */
135
143
  export function writeBudgetInto(raw = {}, budget, { version = 0, at = new Date().toISOString() } = {}) {
136
- const base = obj(raw);
137
- const brains = obj(base.brains);
138
144
  return {
139
- ...base,
140
- limits: {
141
- ...obj(base.limits),
142
- defaultBudgetUsd: budget.defaultRunUsd,
143
- maxBudgetUsd: budget.maxRunUsd,
144
- sessionWarnUsd: budget.sessionWarnUsd,
145
- sessionHardBudgetUsd: budget.sessionHardUsd,
146
- budgetVersion: version,
147
- budgetUpdatedAt: at,
148
- },
149
- brains: { ...brains, claude: { ...obj(brains.claude), maxBudgetUsd: budget.brainUsd } },
145
+ ...withoutLegacyBudget(raw),
146
+ budget: { unlimited: budget.unlimited === true, amountUsd: roundUsd(budget.amountUsd), version, updatedAt: at },
150
147
  };
151
148
  }
152
149
 
153
- /** The dispatcher's limit keys for a budget. */
154
- export function budgetLimits(budget) {
150
+ /** What the Assistant's budget limits at `spentUsd` (null amounts under No limit). */
151
+ export function budgetLimitsAt(budget, spentUsd = 0) {
152
+ if (!budget || budget.unlimited) return { unlimited: true, amountUsd: null, warnAtUsd: null, leftUsd: null, warned: false, exceeded: false };
153
+ const amount = Number(budget.amountUsd) || 0;
154
+ const spent = Math.max(0, Number(spentUsd) || 0);
155
+ const warnAt = roundUsd(amount * BUDGET_WARN_RATIO);
155
156
  return {
156
- defaultBudgetUsd: budget.defaultRunUsd,
157
- maxBudgetUsd: budget.maxRunUsd,
158
- sessionWarnUsd: budget.sessionWarnUsd,
159
- sessionHardBudgetUsd: budget.sessionHardUsd,
160
- brainBudgetUsd: budget.brainUsd,
157
+ unlimited: false, amountUsd: amount, warnAtUsd: warnAt,
158
+ leftUsd: Math.max(0, Number((amount - spent).toFixed(8))),
159
+ warned: spent >= warnAt, exceeded: spent >= amount,
161
160
  };
162
161
  }
163
162
 
@@ -15,6 +15,13 @@
15
15
  // planner) and the router can pick cheap-first, send image work to a model that
16
16
  // can see, and image / video creation to a model that makes them. Everything
17
17
  // I/O is injected; parsers are pure.
18
+ //
19
+ // Which models exist is never decided here. Each provider's rows are the list
20
+ // its own model selector reads (/api/claude/models, /api/codex/models,
21
+ // /api/opencode/providers/full); the static tables (prices, tier hints, the 1M
22
+ // rule) only describe a row. A model the provider lists and no table knows is
23
+ // offered with what is unknown left unknown; a model only a table knows is
24
+ // never offered. `sources` says how each provider's list was obtained.
18
25
 
19
26
  import { sortEfforts, variantNames } from './effort-levels.js';
20
27
  import { listPrice } from './assistant-pricing.js';
@@ -82,6 +89,43 @@ function resolvedKey(row) {
82
89
  return `${baseModelId(row.upstream)}${contextSuffix(row.id)}`.toLowerCase();
83
90
  }
84
91
 
92
+ // ── moving aliases ───────────────────────────────────────────────────────────
93
+ // "opus", "haiku", "default" name whatever model Claude Code points them at, and
94
+ // that changes with a CLI update. A hidden alias is pinned to the model it named
95
+ // when it was hidden (lib/assistant-hidden-models.js); these say what a row names.
96
+
97
+ /** A model id without its snapshot date ("claude-haiku-4-5-20251001", "…@20251001"), lower-case: what makes two ids one model. */
98
+ export function modelIdentity(id) {
99
+ return baseModelId(id).trim().toLowerCase().replace(/[-@]\d{8}$/, '');
100
+ }
101
+
102
+ /**
103
+ * What a moving alias row names right now: { model, variant } ("opus[1m]" →
104
+ * { model: 'claude-opus-5-5', variant: '1m' }), lower-case. null for a row that
105
+ * is its own model (a concrete id, every Codex row), for OpenCode (its
106
+ * `upstream` is the provider, not a model) and whenever the provider did not
107
+ * name a versioned model behind the alias (the static stand-in list, an alias
108
+ * resolving to another alias): nothing is guessed.
109
+ */
110
+ export function aliasTargetOf(row) {
111
+ if (!row?.id || row.provider === 'opencode' || String(row.id).includes('/')) return null;
112
+ const model = baseModelId(row.upstream || '').trim().toLowerCase();
113
+ if (!model || !/\d/.test(model) || modelIdentity(model) === modelIdentity(row.id)) return null;
114
+ return { model, variant: contextVariantOf(row.id) };
115
+ }
116
+
117
+ /** The id of the model a pin or an alias target names, context included ("claude-opus-5-5[1m]"), lower-case. */
118
+ export function pinnedModelId(pin) {
119
+ const model = baseModelId(pin?.model || '').trim().toLowerCase();
120
+ return model ? `${model}${pin.variant ? `[${String(pin.variant).toLowerCase()}]` : ''}` : '';
121
+ }
122
+
123
+ /** Do a pin and an alias target name the same model at the same context window? (A new snapshot date is the same model.) */
124
+ export function sameModelPin(a, b) {
125
+ return !!a && !!b && !!modelIdentity(a.model) && modelIdentity(a.model) === modelIdentity(b.model)
126
+ && String(a.variant || '').toLowerCase() === String(b.variant || '').toLowerCase();
127
+ }
128
+
85
129
  /**
86
130
  * Every Claude row that can run at 1M gets a "[1m]" sibling right after it,
87
131
  * unless the CLI already lists one: the CLI accepts "<id>[1m]" for those
@@ -305,6 +349,9 @@ export function rowIsHidden(catalog, row) {
305
349
  * "opus[1m]"). Context variants hide independently: hiding "opus" leaves
306
350
  * "opus[1m]" alone and the other way round; same for "gpt-x" / "gpt-x[extended]".
307
351
  * OpenCode: the full "provider/model" id, or a bare model id.
352
+ * An entry with a pin record (catalog.hiddenPins: an alias hidden as the model
353
+ * it named, or that model's own id once the alias moved on) hides its own row
354
+ * only: it never reaches another alias through the resolved model.
308
355
  */
309
356
  export function hiddenModelId(catalog, provider, id, { row = null } = {}) {
310
357
  if (!provider || !id) return null;
@@ -322,7 +369,8 @@ export function hiddenModelId(catalog, provider, id, { row = null } = {}) {
322
369
  .find((r) => String(r?.id || '').toLowerCase() === want);
323
370
  const resolved = resolvedKey(own);
324
371
  if (!resolved || resolved === want) return null;
325
- return hidden.find((h) => String(h).toLowerCase() === resolved) || null;
372
+ const pins = catalog?.hiddenPins?.[provider] || null;
373
+ return hidden.find((h) => String(h).toLowerCase() === resolved && !pins?.[resolved]) || null;
326
374
  }
327
375
 
328
376
  /**
@@ -442,7 +490,9 @@ function ctxText(ctx) {
442
490
 
443
491
  /** One routing-sheet line: `id · tier · $in/$out · vision · image-out · video-out · ctx · efforts`. */
444
492
  export function formatModelLine(row) {
445
- const parts = [row.id, row.tier || '?', priceText(row.price, row.listPrice)];
493
+ // Only Codex bills a plan; any other row without a price is a model no table prices yet.
494
+ const planBilled = row.provider === 'codex' || !!row.listPrice;
495
+ const parts = [row.id, row.tier || '?', row.price || planBilled ? priceText(row.price, row.listPrice) : 'price unknown'];
446
496
  if (row.vision) parts.push('vision');
447
497
  if (rowMakes(row, 'image')) parts.push('image-out');
448
498
  if (rowMakes(row, 'video')) parts.push('video-out');
@@ -549,6 +599,12 @@ export function compactRow(row) {
549
599
  * @param {()=>object} [deps.readFilter] → { opencode:[globs] } (routing.catalogFilter)
550
600
  * @param {()=>object} [deps.readHidden] → { 'claude-code': [], codex: [], opencode: [] } ids the user
551
601
  * hid (Assistant → Models; OpenCode's list is Settings → OpenCode's). A bare array = OpenCode ids.
602
+ * @param {(args:{provider:string, rows:object[], source:string})=>{ids:string[], pins:object}|null} [deps.reconcileHidden]
603
+ * settles a provider's hidden list against the rows just listed (hidden aliases are pinned to a model:
604
+ * lib/assistant-hidden-models.js `reconcile`); its ids and pins replace readHidden's for that build
605
+ * @param {(change:{providers:string[], generatedAt:string})=>void} [deps.onChange] a build listed other
606
+ * models than the build before it (hiding has its own event and does not count)
607
+ * @param {number} [deps.retryMs] how soon a build is redone while Claude Code or Codex gave no verified list
552
608
  */
553
609
  export function createAssistantCatalog({
554
610
  fetchJson,
@@ -557,24 +613,53 @@ export function createAssistantCatalog({
557
613
  listAccounts = () => ({}),
558
614
  readFilter = () => ({}),
559
615
  readHidden = () => [],
616
+ reconcileHidden = null,
617
+ onChange = null,
560
618
  ttlMs = 30_000,
619
+ retryMs = 5_000,
561
620
  now = Date.now,
562
621
  log = () => {},
563
622
  } = {}) {
564
623
  if (typeof fetchJson !== 'function') throw new Error('createAssistantCatalog requires fetchJson');
565
624
  let cache = null;
566
625
  let inflight = null;
626
+ // The last verified list Claude Code and Codex gave: what their rows are built
627
+ // from while the provider does not answer, or answers with its static stand-in.
628
+ const lastLive = {};
629
+ let degradedBuilds = 0;
630
+ let epoch = 0; // bumped by invalidate(): a build that began before it is not kept
631
+ let lastListed = null; // provider → signature of the rows the last build listed
632
+
633
+ const pickArray = (value, keys = []) => {
634
+ if (Array.isArray(value)) return value;
635
+ for (const key of keys) if (Array.isArray(value?.[key])) return value[key];
636
+ return [];
637
+ };
567
638
 
568
- async function build() {
639
+ /**
640
+ * One provider's answer → the list to build rows from and how it was obtained:
641
+ * 'live' (the provider answered), 'stale' (it did not: its last verified list),
642
+ * 'fallback' (never verified: the endpoint's static stand-in), 'unavailable'.
643
+ */
644
+ function settle(provider, payload) {
645
+ const list = pickArray(payload, ['models']);
646
+ const verified = !!payload && payload.ok !== false && payload.source !== 'fallback' && list.length > 0;
647
+ if (verified) { lastLive[provider] = list; return { list, state: 'live' }; }
648
+ if (lastLive[provider]) return { list: lastLive[provider], state: 'stale' };
649
+ return list.length ? { list, state: 'fallback' } : { list: [], state: 'unavailable' };
650
+ }
651
+
652
+ function listedSignature(rows) {
653
+ return JSON.stringify(arr(rows).map((row) => [row.id, row.label, row.upstream || '', row.status || '', row.contextWindow || 0, arr(row.efforts).join(',')]));
654
+ }
655
+
656
+ async function build({ deep = false } = {}) {
657
+ // `deep` also drops the providers' own discovery caches (a CLI or app-server round trip).
658
+ const fresh = deep ? '?refresh=1' : '';
569
659
  const [claudeModels, codexModels, opencodeProviders, projects, mcpProfile] = await Promise.all([
570
- fetchJson('/api/claude/models'), fetchJson('/api/codex/models'), fetchJson('/api/opencode/providers/full'),
660
+ fetchJson(`/api/claude/models${fresh}`), fetchJson(`/api/codex/models${fresh}`), fetchJson('/api/opencode/providers/full'),
571
661
  fetchJson('/api/projects'), fetchJson('/api/mcp/profile'),
572
662
  ]);
573
- const pickArray = (value, keys = []) => {
574
- if (Array.isArray(value)) return value;
575
- for (const key of keys) if (Array.isArray(value?.[key])) return value[key];
576
- return [];
577
- };
578
663
  const filter = (() => { try { return readFilter() || {}; } catch { return {}; } })();
579
664
  const presets = mcpProfile?.presets && typeof mcpProfile.presets === 'object'
580
665
  ? Object.entries(mcpProfile.presets).map(([name, preset]) => ({ name, label: preset?.label || name, groups: preset?.groups || [], tools: preset?.tools || null }))
@@ -590,12 +675,34 @@ export function createAssistantCatalog({
590
675
  if (Array.isArray(raw)) raw = { opencode: raw };
591
676
  return Object.fromEntries(CATALOG_PROVIDERS.map((p) => [p, arr(raw[p]).map(String)]));
592
677
  })();
678
+ const claude = settle('claude-code', claudeModels);
679
+ const codex = settle('codex', codexModels);
680
+ // OpenCode is read from its serve on every build; while it is down its models are unknown, not remembered.
681
+ const sources = {
682
+ 'claude-code': claude.state,
683
+ codex: codex.state,
684
+ opencode: opencodeProviders && opencodeProviders.ok !== false ? 'live' : 'unavailable',
685
+ };
686
+ for (const p of ['claude-code', 'codex']) if (sources[p] !== 'live') log('catalog:source', `${p}: ${sources[p]}`);
593
687
  const all = {
594
- 'claude-code': withClaudeContextVariants(normalizeClaudeRows(pickArray(claudeModels, ['models']), { pricing: claudePricing })),
595
- codex: normalizeCodexRows(pickArray(codexModels, ['models']), { priceOf: codexPrice }),
688
+ 'claude-code': withClaudeContextVariants(normalizeClaudeRows(claude.list, { pricing: claudePricing })),
689
+ codex: normalizeCodexRows(codex.list, { priceOf: codexPrice }),
596
690
  opencode: parseOpenCodeProviders(opencodeProviders, { filter: filter.opencode || null }),
597
691
  };
598
- const lookup = { hidden };
692
+ // A hidden alias hides the model it named when it was hidden. With the rows at hand the
693
+ // store settles that: an alias that moved on is shown again and its old model stays hidden.
694
+ const hiddenPins = Object.fromEntries(CATALOG_PROVIDERS.map((p) => [p, {}]));
695
+ if (typeof reconcileHidden === 'function') {
696
+ for (const p of CATALOG_PROVIDERS) {
697
+ try {
698
+ const settled = reconcileHidden({ provider: p, rows: all[p], source: sources[p] });
699
+ if (!settled || !Array.isArray(settled.ids)) continue;
700
+ hidden[p] = settled.ids.map(String);
701
+ hiddenPins[p] = settled.pins && typeof settled.pins === 'object' ? settled.pins : {};
702
+ } catch (error) { log('catalog:hidden-pins-error', `${p}: ${error?.message || String(error)}`); }
703
+ }
704
+ }
705
+ const lookup = { hidden, hiddenPins };
599
706
  const models = {};
600
707
  const hiddenRows = {};
601
708
  const listedCounts = {};
@@ -606,11 +713,20 @@ export function createAssistantCatalog({
606
713
  for (const row of all[p]) (hidden[p].length && hiddenModelId(lookup, p, row.id, { row }) ? hiddenRows[p] : models[p]).push(row);
607
714
  }
608
715
  const opencode = models.opencode;
716
+ const listedNow = Object.fromEntries(CATALOG_PROVIDERS.map((p) => [p, listedSignature(all[p])]));
717
+ const changed = lastListed ? CATALOG_PROVIDERS.filter((p) => listedNow[p] !== lastListed[p]) : [];
718
+ lastListed = listedNow;
609
719
  return {
610
720
  generatedAt: new Date(now()).toISOString(),
721
+ // Providers whose listed models differ from the build before (not part of the value's meaning: full() reports it).
722
+ changedProviders: changed,
611
723
  models,
612
724
  hidden,
725
+ // provider → { hidden id (lower-case) → { model, variant, label, at, … } }: the entries that hide one row only.
726
+ hiddenPins,
613
727
  hiddenRows,
728
+ // How each provider's list was obtained: live | stale | fallback | unavailable.
729
+ sources,
614
730
  listed: listedCounts,
615
731
  // The provider answered with an authoritative list, even an empty one (OpenCode's `connected`).
616
732
  known: { opencode: openCodeConnectedKnown(opencodeProviders) },
@@ -630,28 +746,57 @@ export function createAssistantCatalog({
630
746
  };
631
747
  }
632
748
 
633
- async function full({ force = false } = {}) {
634
- if (!force && cache && now() - cache.at < ttlMs) return cache.value;
749
+ /**
750
+ * `force` rebuilds now from the providers' endpoints (each answers from its own
751
+ * discovery cache, exactly as it does for its panel's selector); `deep` makes
752
+ * those endpoints ask the CLI / app-server again as well.
753
+ */
754
+ async function full({ force = false, deep = false } = {}) {
755
+ if (!force && !deep && cache && now() - cache.at < cache.ttl) return cache.value;
635
756
  if (inflight) return inflight;
636
- inflight = build().then((value) => { cache = { at: now(), value }; return value; }).finally(() => { inflight = null; });
757
+ const started = epoch;
758
+ inflight = build({ deep }).then((built) => {
759
+ const { changedProviders, ...value } = built;
760
+ // No verified list from Claude Code or Codex: ask again soon, backing off to the
761
+ // normal lifetime (a provider that is not installed must not be spawned every few seconds).
762
+ const degraded = ['claude-code', 'codex'].some((p) => value.sources[p] !== 'live');
763
+ degradedBuilds = degraded ? degradedBuilds + 1 : 0;
764
+ const ttl = degraded ? Math.min(ttlMs, retryMs * 2 ** (degradedBuilds - 1)) : ttlMs;
765
+ cache = { at: now(), value, ttl: started === epoch ? ttl : 0 };
766
+ if (changedProviders.length && typeof onChange === 'function') {
767
+ try { onChange({ providers: changedProviders, generatedAt: value.generatedAt }); } catch (error) { log('catalog:onchange-error', error?.message || String(error)); }
768
+ }
769
+ return value;
770
+ }).finally(() => { inflight = null; });
637
771
  return inflight;
638
772
  }
639
773
 
640
- /** Every row, hidden ones included, per provider (the Models manager, id resolution). */
641
- async function manageRows({ force = false } = {}) {
642
- const value = await full({ force });
643
- return Object.fromEntries(CATALOG_PROVIDERS.map((p) => [p, [...arr(value.models?.[p]), ...arr(value.hiddenRows?.[p])]]));
774
+ /**
775
+ * Every row, hidden ones included, per provider, and how each provider's list was obtained:
776
+ * { rows, sources, generatedAt } of one build, so what a row says is never read against another build's source.
777
+ */
778
+ async function manageSnapshot({ force = false, deep = false } = {}) {
779
+ const value = await full({ force, deep });
780
+ return {
781
+ rows: Object.fromEntries(CATALOG_PROVIDERS.map((p) => [p, [...arr(value.models?.[p]), ...arr(value.hiddenRows?.[p])]])),
782
+ sources: { ...value.sources },
783
+ generatedAt: value.generatedAt,
784
+ };
644
785
  }
645
786
 
787
+ /** Every row, hidden ones included, per provider (the Models manager, id resolution). */
788
+ async function manageRows(options = {}) { return (await manageSnapshot(options)).rows; }
789
+
646
790
  /**
647
791
  * view 'full' (default): every visible row. view 'brain': compact rows,
648
792
  * OpenCode capped; `provider` + `q` narrow the list (agent_catalog search).
649
793
  * view 'manage': every row with hidden: true|false and hiddenBy (the list
650
794
  * entry that hides it), plus `archived`: every hidden id per provider with
651
- * the rows it covers; listed:false = no current row (still restorable).
795
+ * the rows it covers; listed:false = no current row (still restorable); `pin`
796
+ * = the model an alias entry was hidden as (see hiddenPins).
652
797
  */
653
- async function get({ force = false, view = 'full', provider = null, q = null, brain = null, preferences = {} } = {}) {
654
- const value = await full({ force });
798
+ async function get({ force = false, deep = false, view = 'full', provider = null, q = null, brain = null, preferences = {} } = {}) {
799
+ const value = await full({ force, deep });
655
800
  if (view === 'manage') {
656
801
  const models = {};
657
802
  const archived = {};
@@ -672,10 +817,11 @@ export function createAssistantCatalog({
672
817
  });
673
818
  archived[p] = arr(value.hidden?.[p]).map((id) => {
674
819
  const covered = covers.get(String(id).toLowerCase()) || [];
675
- return { id, listed: covered.length > 0, group: p === 'opencode' ? String(id).split('/')[0] : null, rows: covered };
820
+ const pin = value.hiddenPins?.[p]?.[String(id).toLowerCase()] || null;
821
+ return { id, listed: covered.length > 0, group: p === 'opencode' ? String(id).split('/')[0] : null, rows: covered, ...(pin ? { pin } : {}) };
676
822
  });
677
823
  }
678
- return { view: 'manage', generatedAt: value.generatedAt, models, archived, hidden: value.hidden, listed: value.listed };
824
+ return { view: 'manage', generatedAt: value.generatedAt, models, archived, hidden: value.hidden, hiddenPins: value.hiddenPins, listed: value.listed, sources: value.sources };
679
825
  }
680
826
  if (view !== 'brain' && !provider && !q) return value;
681
827
  const needle = q ? String(q).toLowerCase() : null;
@@ -690,7 +836,7 @@ export function createAssistantCatalog({
690
836
  const sheet = selectSheetModels(value, { brain, preferences, max: 40 });
691
837
  for (const p of CATALOG_PROVIDERS) models[p] = (p === 'opencode' ? sheet[p] : value.models[p]).map(compactRow);
692
838
  }
693
- const { opencodeHidden, hidden, hiddenRows, ...rest } = value;
839
+ const { opencodeHidden, hidden, hiddenPins, hiddenRows, ...rest } = value;
694
840
  const hiddenCount = Object.fromEntries(CATALOG_PROVIDERS.map((p) => [p, arr(hiddenRows?.[p]).length]));
695
841
  return { ...rest, view: 'brain', models, opencodeTotal: value.opencodeTotal, hiddenCount, opencodeHiddenCount: arr(opencodeHidden).length };
696
842
  }
@@ -725,7 +871,13 @@ export function createAssistantCatalog({
725
871
  return { vision: row.vision ?? null, tier: row.tier || null, price: row.price || null, label: row.label || row.id, contextWindow: row.contextWindow || null, source: hit?.match || 'default', id: row.id };
726
872
  }
727
873
 
728
- function invalidate() { cache = null; }
874
+ function invalidate() { cache = null; epoch += 1; }
875
+ /** A provider's own list changed: drop the copy and build the next one now (onChange reports the difference). */
876
+ function refresh() {
877
+ invalidate();
878
+ const pending = inflight; // began before the change: let it end, then build again
879
+ return pending ? pending.catch(() => null).then(() => full()) : full();
880
+ }
729
881
 
730
- return { get, full, peek, find, hiddenId, manageRows, brainInfo, invalidate };
882
+ return { get, full, peek, find, hiddenId, manageRows, manageSnapshot, brainInfo, invalidate, refresh };
731
883
  }