synabun 2.0.0-beta → 2.0.5

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 (374) hide show
  1. package/CHANGELOG.md +65 -0
  2. package/NOTICE +5 -0
  3. package/README.md +1 -1
  4. package/THIRD-PARTY-LICENSES.md +51 -0
  5. package/benchmarks/fixtures/browser/README.md +1 -1
  6. package/benchmarks/fixtures/desktop/README.md +1 -1
  7. package/docs/whatsapp.md +2 -2
  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 +20 -0
  12. package/hooks/claude-code/stop.mjs +5 -5
  13. package/launcher.mjs +40 -0
  14. package/lib/claude-hooks.js +61 -8
  15. package/lib/client-config-repair.js +4 -3
  16. package/lib/data-home-migration.js +4 -0
  17. package/lib/desktop-pwa.js +206 -0
  18. package/lib/external-tools.js +221 -0
  19. package/lib/first-launch-protection.js +5 -0
  20. package/lib/packaged-runtime.js +110 -0
  21. package/lib/start-launcher.js +1396 -0
  22. package/mcp-server/package-lock.json +2 -2
  23. package/mcp-server/package.json +1 -3
  24. package/neural-interface/i18n/en.json +1540 -154
  25. package/neural-interface/i18n/pt-BR.json +1538 -153
  26. package/neural-interface/lib/assistant-api.js +62 -6
  27. package/neural-interface/lib/assistant-brains/claude.js +4 -0
  28. package/neural-interface/lib/assistant-catalog.js +179 -27
  29. package/neural-interface/lib/assistant-config.js +31 -2
  30. package/neural-interface/lib/assistant-dispatch.js +62 -13
  31. package/neural-interface/lib/assistant-envelope.js +9 -6
  32. package/neural-interface/lib/assistant-hidden-models.js +393 -66
  33. package/neural-interface/lib/assistant-memory.js +24 -10
  34. package/neural-interface/lib/assistant-persona.js +6 -2
  35. package/neural-interface/lib/assistant-pricing.js +21 -6
  36. package/neural-interface/lib/assistant-runtime.js +181 -12
  37. package/neural-interface/lib/assistant-task-prompt.js +13 -3
  38. package/neural-interface/lib/assistant-test-density.js +142 -0
  39. package/neural-interface/lib/assistant-user-context-budgets.js +123 -0
  40. package/neural-interface/lib/assistant-user-context.js +908 -0
  41. package/neural-interface/lib/assistant-user-knowledge.js +328 -0
  42. package/neural-interface/lib/assistant-user-learn.js +404 -0
  43. package/neural-interface/lib/assistant-user-threads.js +226 -0
  44. package/neural-interface/lib/assistant-user-writer.js +181 -0
  45. package/neural-interface/lib/augmented-path.js +23 -3
  46. package/neural-interface/lib/browser-background-tab.js +134 -0
  47. package/neural-interface/lib/browser-language.js +83 -0
  48. package/neural-interface/lib/browser-screenshot.js +678 -61
  49. package/neural-interface/lib/claude-agent-bridge.js +87 -153
  50. package/neural-interface/lib/claude-executable.js +241 -0
  51. package/neural-interface/lib/claude-model-catalog.js +82 -93
  52. package/neural-interface/lib/claude-temporary.js +46 -7
  53. package/neural-interface/lib/cloudflared/install.js +533 -0
  54. package/neural-interface/lib/cloudflared/manager.js +545 -0
  55. package/neural-interface/lib/cloudflared/pins.js +115 -0
  56. package/neural-interface/lib/cloudflared/pins.json +63 -0
  57. package/neural-interface/lib/cloudflared/tunnel.js +265 -0
  58. package/neural-interface/lib/editor-completion-endpoint.js +264 -0
  59. package/neural-interface/lib/editor-completion-opencode.js +436 -0
  60. package/neural-interface/lib/editor-completion.js +714 -0
  61. package/neural-interface/lib/editor-config.js +149 -0
  62. package/neural-interface/lib/editor-files-api.js +268 -0
  63. package/neural-interface/lib/editor-git-api.js +134 -0
  64. package/neural-interface/lib/file-explorer-api.js +1039 -0
  65. package/neural-interface/lib/file-explorer-system.js +355 -0
  66. package/neural-interface/lib/file-explorer-trash.ps1 +85 -0
  67. package/neural-interface/lib/file-explorer-watch.js +239 -0
  68. package/neural-interface/lib/git-diff-api.js +103 -0
  69. package/neural-interface/lib/git-read.js +202 -0
  70. package/neural-interface/lib/greeting/api.js +89 -0
  71. package/neural-interface/lib/greeting/budgets.js +90 -0
  72. package/neural-interface/lib/greeting/engine.js +461 -0
  73. package/neural-interface/lib/greeting/facts.js +320 -0
  74. package/neural-interface/lib/greeting/sources.js +252 -0
  75. package/neural-interface/lib/greeting/writer.js +139 -0
  76. package/neural-interface/lib/http-guards.js +189 -13
  77. package/neural-interface/lib/machine-names.js +88 -0
  78. package/neural-interface/lib/memory-api.js +31 -7
  79. package/neural-interface/lib/native-binary-runtime.js +69 -337
  80. package/neural-interface/lib/native-loop-providers.js +40 -49
  81. package/neural-interface/lib/offline-page.js +54 -0
  82. package/neural-interface/lib/plugin-installer.js +4 -2
  83. package/neural-interface/lib/project-storage.js +5 -7
  84. package/neural-interface/lib/request-gate.js +589 -0
  85. package/neural-interface/lib/session-title-generator.js +5 -0
  86. package/neural-interface/lib/synabun-update-plan.js +19 -0
  87. package/neural-interface/lib/ui-blob-api.js +108 -0
  88. package/neural-interface/lib/ui-blob-store.js +68 -0
  89. package/neural-interface/package-lock.json +701 -253
  90. package/neural-interface/package.json +13 -2
  91. package/neural-interface/public/install-app.html +34 -0
  92. package/neural-interface/public/manifest.json +2 -0
  93. package/neural-interface/public/offline.html +402 -66
  94. package/neural-interface/public/onboarding.html +264 -147
  95. package/neural-interface/public/shared/agent-runtime-options.js +22 -6
  96. package/neural-interface/public/shared/assistant/asst-automation.js +286 -0
  97. package/neural-interface/public/shared/assistant/asst-brain-picker.js +312 -62
  98. package/neural-interface/public/shared/assistant/asst-composer.js +11 -1
  99. package/neural-interface/public/shared/assistant/asst-mascot.js +26 -64
  100. package/neural-interface/public/shared/assistant/asst-menu.js +12 -8
  101. package/neural-interface/public/shared/assistant/asst-panel.js +150 -28
  102. package/neural-interface/public/shared/assistant/asst-rack-styles.js +0 -3
  103. package/neural-interface/public/shared/assistant/asst-render.js +219 -67
  104. package/neural-interface/public/shared/assistant/asst-route-models.js +108 -0
  105. package/neural-interface/public/shared/assistant/asst-route.js +23 -329
  106. package/neural-interface/public/shared/assistant/asst-routes-editor.js +495 -0
  107. package/neural-interface/public/shared/assistant/asst-sanitize.js +8 -1
  108. package/neural-interface/public/shared/assistant/asst-sidepanel.js +31 -9
  109. package/neural-interface/public/shared/assistant/asst-state.js +185 -1
  110. package/neural-interface/public/shared/assistant/asst-styles.js +162 -6
  111. package/neural-interface/public/shared/blob-store.js +111 -22
  112. package/neural-interface/public/shared/cdx/cdx-panel.js +32 -11
  113. package/neural-interface/public/shared/cdx/cdx-render.js +25 -89
  114. package/neural-interface/public/shared/cdx/cdx-styles.js +36 -0
  115. package/neural-interface/public/shared/cdx/cdx-tabs.js +87 -4
  116. package/neural-interface/public/shared/command-runner-model.js +30 -0
  117. package/neural-interface/public/shared/constants.js +6 -1
  118. package/neural-interface/public/shared/cp/cp-rehydrate.js +2 -1
  119. package/neural-interface/public/shared/cp/cp-styles.js +15 -0
  120. package/neural-interface/public/shared/editor/editor-ai-logic.js +409 -0
  121. package/neural-interface/public/shared/editor/editor-ai.js +619 -0
  122. package/neural-interface/public/shared/editor/editor-commands.js +212 -0
  123. package/neural-interface/public/shared/editor/editor-compare.js +103 -0
  124. package/neural-interface/public/shared/editor/editor-completion-list.js +172 -0
  125. package/neural-interface/public/shared/editor/editor-doc.js +257 -0
  126. package/neural-interface/public/shared/editor/editor-git-logic.js +131 -0
  127. package/neural-interface/public/shared/editor/editor-git.js +95 -0
  128. package/neural-interface/public/shared/editor/editor-languages.js +156 -0
  129. package/neural-interface/public/shared/editor/editor-lint.js +229 -0
  130. package/neural-interface/public/shared/editor/editor-loader.js +51 -0
  131. package/neural-interface/public/shared/editor/editor-markdown.js +90 -0
  132. package/neural-interface/public/shared/editor/editor-menu.js +233 -0
  133. package/neural-interface/public/shared/editor/editor-minimap.js +371 -0
  134. package/neural-interface/public/shared/editor/editor-palette-logic.js +192 -0
  135. package/neural-interface/public/shared/editor/editor-palette.js +125 -0
  136. package/neural-interface/public/shared/editor/editor-panel-logic.js +288 -0
  137. package/neural-interface/public/shared/editor/editor-panel.js +2099 -0
  138. package/neural-interface/public/shared/editor/editor-prefs.js +224 -0
  139. package/neural-interface/public/shared/editor/editor-safety.js +500 -0
  140. package/neural-interface/public/shared/editor/editor-search.js +352 -0
  141. package/neural-interface/public/shared/editor/editor-statusbar.js +144 -0
  142. package/neural-interface/public/shared/editor/editor-tabs-logic.js +331 -0
  143. package/neural-interface/public/shared/editor/editor-tabs.js +233 -0
  144. package/neural-interface/public/shared/editor/editor-theme.js +244 -0
  145. package/neural-interface/public/shared/editor/editor-view.js +332 -0
  146. package/neural-interface/public/shared/explorer/fx-api.js +216 -0
  147. package/neural-interface/public/shared/explorer/fx-links.js +204 -0
  148. package/neural-interface/public/shared/explorer/fx-logic.js +537 -0
  149. package/neural-interface/public/shared/explorer/fx-main.js +865 -0
  150. package/neural-interface/public/shared/explorer/fx-names.js +87 -0
  151. package/neural-interface/public/shared/explorer/fx-ops.js +370 -0
  152. package/neural-interface/public/shared/explorer/fx-popups.js +490 -0
  153. package/neural-interface/public/shared/explorer/fx-tree.js +790 -0
  154. package/neural-interface/public/shared/file-links.js +339 -0
  155. package/neural-interface/public/shared/greeting/greeting-card.js +178 -0
  156. package/neural-interface/public/shared/greeting/greeting-view.js +148 -0
  157. package/neural-interface/public/shared/html-shell.js +169 -170
  158. package/neural-interface/public/shared/ocp-v2/ocp-v2-childpanel.js +4 -0
  159. package/neural-interface/public/shared/ocp-v2/ocp-v2-greeting.js +47 -0
  160. package/neural-interface/public/shared/ocp-v2/ocp-v2-panel.js +99 -17
  161. package/neural-interface/public/shared/ocp-v2/ocp-v2-render.js +7 -1
  162. package/neural-interface/public/shared/ocp-v2/ocp-v2-send.js +5 -0
  163. package/neural-interface/public/shared/ocp-v2/ocp-v2-styles.js +40 -0
  164. package/neural-interface/public/shared/ocp-v2/ocp-v2-ws.js +3 -2
  165. package/neural-interface/public/shared/pwa-install-page.js +62 -0
  166. package/neural-interface/public/shared/pwa-install.css +12 -0
  167. package/neural-interface/public/shared/pwa-install.js +127 -0
  168. package/neural-interface/public/shared/settings/settings-code-editor-pane.js +412 -0
  169. package/neural-interface/public/shared/settings/settings-code-editor.js +328 -0
  170. package/neural-interface/public/shared/settings/settings-greeting.js +133 -0
  171. package/neural-interface/public/shared/settings/settings-ia.js +18 -4
  172. package/neural-interface/public/shared/settings/settings-rules.js +160 -0
  173. package/neural-interface/public/shared/settings/settings-user-context.js +154 -0
  174. package/neural-interface/public/shared/start-bridge.js +802 -0
  175. package/neural-interface/public/shared/styleguide/sg-colors.js +5 -2
  176. package/neural-interface/public/shared/styleguide/sg-import-export.js +4 -3
  177. package/neural-interface/public/shared/styleguide/sg-kit.js +4 -1
  178. package/neural-interface/public/shared/styleguide/sg-panel.js +9 -1
  179. package/neural-interface/public/shared/styleguide/sg-typography.js +1 -0
  180. package/neural-interface/public/shared/styles.css +2219 -1117
  181. package/neural-interface/public/shared/synabun-mascot.js +20 -56
  182. package/neural-interface/public/shared/term-ready.js +307 -0
  183. package/neural-interface/public/shared/tunnel-view.js +167 -0
  184. package/neural-interface/public/shared/ui-claude-panel.js +160 -19
  185. package/neural-interface/public/shared/ui-command-runner.js +251 -468
  186. package/neural-interface/public/shared/ui-detail.js +15 -0
  187. package/neural-interface/public/shared/ui-explorer.js +22 -0
  188. package/neural-interface/public/shared/ui-file-explorer.js +481 -2496
  189. package/neural-interface/public/shared/ui-file-links.js +103 -0
  190. package/neural-interface/public/shared/ui-help.js +26 -0
  191. package/neural-interface/public/shared/ui-invite.js +126 -116
  192. package/neural-interface/public/shared/ui-judgments.js +1 -1
  193. package/neural-interface/public/shared/ui-keybinds.js +4 -0
  194. package/neural-interface/public/shared/ui-loading.js +367 -12
  195. package/neural-interface/public/shared/ui-menubar.js +21 -55
  196. package/neural-interface/public/shared/ui-native-loop-router-state.js +11 -0
  197. package/neural-interface/public/shared/ui-native-loop-router.js +15 -2
  198. package/neural-interface/public/shared/ui-navbar.js +45 -21
  199. package/neural-interface/public/shared/ui-settings.js +491 -547
  200. package/neural-interface/public/shared/ui-sidepanel-runtime.js +2 -1
  201. package/neural-interface/public/shared/ui-sync.js +16 -0
  202. package/neural-interface/public/shared/ui-terminal.js +153 -149
  203. package/neural-interface/public/shared/ui-tunnel.js +130 -0
  204. package/neural-interface/public/shared/ui-tutorial-steps.js +6 -2
  205. package/neural-interface/public/shared/ui-whiteboard.js +12 -0
  206. package/neural-interface/public/shared/ui-worker-end.js +245 -0
  207. package/neural-interface/public/shared/ui-workspaces.js +41 -27
  208. package/neural-interface/public/shared/worker-end.js +251 -0
  209. package/neural-interface/public/sw.js +3 -1
  210. package/neural-interface/public/variant/2d/main.js +2 -1
  211. package/neural-interface/public/variant/3d/main.js +2 -1
  212. package/neural-interface/public/vendor/codemirror/chunks/apl-CSDKT72Z.js +1 -0
  213. package/neural-interface/public/vendor/codemirror/chunks/asciiarmor-JYYPU5BM.js +1 -0
  214. package/neural-interface/public/vendor/codemirror/chunks/asn1-NRWCZDA6.js +1 -0
  215. package/neural-interface/public/vendor/codemirror/chunks/asterisk-5OU6D4ZQ.js +1 -0
  216. package/neural-interface/public/vendor/codemirror/chunks/brainfuck-ID4N4SYC.js +1 -0
  217. package/neural-interface/public/vendor/codemirror/chunks/chunk-4RZWGCS2.js +1 -0
  218. package/neural-interface/public/vendor/codemirror/chunks/chunk-ACTJM3JR.js +11 -0
  219. package/neural-interface/public/vendor/codemirror/chunks/chunk-S6BMAXLK.js +1 -0
  220. package/neural-interface/public/vendor/codemirror/chunks/chunk-TO2RDOHQ.js +3 -0
  221. package/neural-interface/public/vendor/codemirror/chunks/chunk-US2GMZZQ.js +13 -0
  222. package/neural-interface/public/vendor/codemirror/chunks/chunk-WYYO5ZAJ.js +1 -0
  223. package/neural-interface/public/vendor/codemirror/chunks/chunk-XALZTDJC.js +1 -0
  224. package/neural-interface/public/vendor/codemirror/chunks/chunk-YRNUTRDV.js +1 -0
  225. package/neural-interface/public/vendor/codemirror/chunks/chunk-Z3QOKH62.js +1 -0
  226. package/neural-interface/public/vendor/codemirror/chunks/clike-OWQBSOGL.js +1 -0
  227. package/neural-interface/public/vendor/codemirror/chunks/clojure-KV75UQWC.js +1 -0
  228. package/neural-interface/public/vendor/codemirror/chunks/cmake-GTEP5Y3C.js +1 -0
  229. package/neural-interface/public/vendor/codemirror/chunks/cobol-WCZ7QEGJ.js +1 -0
  230. package/neural-interface/public/vendor/codemirror/chunks/coffeescript-TGLBOT6B.js +1 -0
  231. package/neural-interface/public/vendor/codemirror/chunks/commonlisp-WVVI432V.js +1 -0
  232. package/neural-interface/public/vendor/codemirror/chunks/crystal-S6R6HLKQ.js +1 -0
  233. package/neural-interface/public/vendor/codemirror/chunks/css-PDA2RINP.js +1 -0
  234. package/neural-interface/public/vendor/codemirror/chunks/cypher-HL5QPL4C.js +1 -0
  235. package/neural-interface/public/vendor/codemirror/chunks/d-HHJH2HAJ.js +1 -0
  236. package/neural-interface/public/vendor/codemirror/chunks/diff-MN3A35BY.js +1 -0
  237. package/neural-interface/public/vendor/codemirror/chunks/dist-25SQLBVS.js +3 -0
  238. package/neural-interface/public/vendor/codemirror/chunks/dist-27ZGGJJS.js +1 -0
  239. package/neural-interface/public/vendor/codemirror/chunks/dist-4E52OJIF.js +1 -0
  240. package/neural-interface/public/vendor/codemirror/chunks/dist-6FT73XDC.js +2 -0
  241. package/neural-interface/public/vendor/codemirror/chunks/dist-BE6YGRMW.js +1 -0
  242. package/neural-interface/public/vendor/codemirror/chunks/dist-CJSGKW2O.js +1 -0
  243. package/neural-interface/public/vendor/codemirror/chunks/dist-CRPH2WLI.js +1 -0
  244. package/neural-interface/public/vendor/codemirror/chunks/dist-CTG7JEQN.js +1 -0
  245. package/neural-interface/public/vendor/codemirror/chunks/dist-I3FEVELX.js +1 -0
  246. package/neural-interface/public/vendor/codemirror/chunks/dist-IM5BBSWZ.js +1 -0
  247. package/neural-interface/public/vendor/codemirror/chunks/dist-KERUKTIF.js +1 -0
  248. package/neural-interface/public/vendor/codemirror/chunks/dist-LXTMWZAM.js +1 -0
  249. package/neural-interface/public/vendor/codemirror/chunks/dist-MABASSET.js +1 -0
  250. package/neural-interface/public/vendor/codemirror/chunks/dist-MJVBUZLL.js +3 -0
  251. package/neural-interface/public/vendor/codemirror/chunks/dist-MNB3WZVU.js +1 -0
  252. package/neural-interface/public/vendor/codemirror/chunks/dist-QYWXOE3U.js +7 -0
  253. package/neural-interface/public/vendor/codemirror/chunks/dist-TEBOFWLZ.js +1 -0
  254. package/neural-interface/public/vendor/codemirror/chunks/dist-UNXFROM3.js +1 -0
  255. package/neural-interface/public/vendor/codemirror/chunks/dist-XL2SLQO6.js +1 -0
  256. package/neural-interface/public/vendor/codemirror/chunks/dist-XO42Y3AP.js +6 -0
  257. package/neural-interface/public/vendor/codemirror/chunks/dist-Y2WWDWJK.js +1 -0
  258. package/neural-interface/public/vendor/codemirror/chunks/dist-YFEZKWMU.js +1 -0
  259. package/neural-interface/public/vendor/codemirror/chunks/dist-ZY7SWQO6.js +1 -0
  260. package/neural-interface/public/vendor/codemirror/chunks/dockerfile-3Y4R4NCH.js +1 -0
  261. package/neural-interface/public/vendor/codemirror/chunks/dtd-5343GJ4P.js +1 -0
  262. package/neural-interface/public/vendor/codemirror/chunks/dylan-FI2CXSW2.js +1 -0
  263. package/neural-interface/public/vendor/codemirror/chunks/ebnf-GNQ77YWA.js +1 -0
  264. package/neural-interface/public/vendor/codemirror/chunks/ecl-MRPUAE2L.js +1 -0
  265. package/neural-interface/public/vendor/codemirror/chunks/eiffel-DZROES27.js +1 -0
  266. package/neural-interface/public/vendor/codemirror/chunks/elm-4KICRFKQ.js +1 -0
  267. package/neural-interface/public/vendor/codemirror/chunks/erlang-3IHYC263.js +1 -0
  268. package/neural-interface/public/vendor/codemirror/chunks/factor-C6D3E5UK.js +1 -0
  269. package/neural-interface/public/vendor/codemirror/chunks/fcl-VYTWUJMI.js +1 -0
  270. package/neural-interface/public/vendor/codemirror/chunks/forth-ZBO2EH5M.js +1 -0
  271. package/neural-interface/public/vendor/codemirror/chunks/fortran-5NDCRWIK.js +1 -0
  272. package/neural-interface/public/vendor/codemirror/chunks/gas-SKRMOXFP.js +1 -0
  273. package/neural-interface/public/vendor/codemirror/chunks/gherkin-L45YXZLW.js +1 -0
  274. package/neural-interface/public/vendor/codemirror/chunks/groovy-GLPTLN44.js +1 -0
  275. package/neural-interface/public/vendor/codemirror/chunks/haskell-E7NF7P4Y.js +1 -0
  276. package/neural-interface/public/vendor/codemirror/chunks/haxe-K2NVDOB6.js +1 -0
  277. package/neural-interface/public/vendor/codemirror/chunks/http-Q6DRQIJB.js +1 -0
  278. package/neural-interface/public/vendor/codemirror/chunks/idl-MZYNJVLN.js +1 -0
  279. package/neural-interface/public/vendor/codemirror/chunks/javascript-6YRYK2HR.js +1 -0
  280. package/neural-interface/public/vendor/codemirror/chunks/julia-HXEL2T5G.js +1 -0
  281. package/neural-interface/public/vendor/codemirror/chunks/livescript-MV7CI2JZ.js +1 -0
  282. package/neural-interface/public/vendor/codemirror/chunks/lua-E36NHOOD.js +1 -0
  283. package/neural-interface/public/vendor/codemirror/chunks/mathematica-3XOZZCMC.js +1 -0
  284. package/neural-interface/public/vendor/codemirror/chunks/mbox-ODYGFKS7.js +1 -0
  285. package/neural-interface/public/vendor/codemirror/chunks/mirc-WB43AWF5.js +1 -0
  286. package/neural-interface/public/vendor/codemirror/chunks/mllike-ZT6ET7ZH.js +1 -0
  287. package/neural-interface/public/vendor/codemirror/chunks/modelica-LLMZPFGA.js +1 -0
  288. package/neural-interface/public/vendor/codemirror/chunks/mscgen-2OPNNYFH.js +1 -0
  289. package/neural-interface/public/vendor/codemirror/chunks/mumps-6P3HWJH6.js +1 -0
  290. package/neural-interface/public/vendor/codemirror/chunks/nginx-TCGKQTRW.js +1 -0
  291. package/neural-interface/public/vendor/codemirror/chunks/nsis-ATIGIXTJ.js +1 -0
  292. package/neural-interface/public/vendor/codemirror/chunks/ntriples-5PLBI5OV.js +1 -0
  293. package/neural-interface/public/vendor/codemirror/chunks/octave-QF2S3NL6.js +1 -0
  294. package/neural-interface/public/vendor/codemirror/chunks/oz-JFIVFFH2.js +1 -0
  295. package/neural-interface/public/vendor/codemirror/chunks/pascal-JG6SW76V.js +1 -0
  296. package/neural-interface/public/vendor/codemirror/chunks/perl-4B3IWVVO.js +1 -0
  297. package/neural-interface/public/vendor/codemirror/chunks/pig-ABEBH4MI.js +1 -0
  298. package/neural-interface/public/vendor/codemirror/chunks/powershell-H32XE7TL.js +1 -0
  299. package/neural-interface/public/vendor/codemirror/chunks/properties-NN644MMA.js +1 -0
  300. package/neural-interface/public/vendor/codemirror/chunks/protobuf-YKWQK2J3.js +1 -0
  301. package/neural-interface/public/vendor/codemirror/chunks/pug-RIYXEKFW.js +1 -0
  302. package/neural-interface/public/vendor/codemirror/chunks/puppet-JZQ5WPEV.js +1 -0
  303. package/neural-interface/public/vendor/codemirror/chunks/python-EBCGGHAC.js +1 -0
  304. package/neural-interface/public/vendor/codemirror/chunks/q-5UMH2YQY.js +1 -0
  305. package/neural-interface/public/vendor/codemirror/chunks/r-7XMGG453.js +1 -0
  306. package/neural-interface/public/vendor/codemirror/chunks/rpm-IEV7TSLR.js +1 -0
  307. package/neural-interface/public/vendor/codemirror/chunks/ruby-T4SPFPNM.js +1 -0
  308. package/neural-interface/public/vendor/codemirror/chunks/sas-GEDX67ET.js +1 -0
  309. package/neural-interface/public/vendor/codemirror/chunks/scheme-FTLOWKPC.js +1 -0
  310. package/neural-interface/public/vendor/codemirror/chunks/shell-HUFRKFSJ.js +1 -0
  311. package/neural-interface/public/vendor/codemirror/chunks/sieve-P5QUDVYO.js +1 -0
  312. package/neural-interface/public/vendor/codemirror/chunks/smalltalk-2SORJNQI.js +1 -0
  313. package/neural-interface/public/vendor/codemirror/chunks/solr-M22TGAR5.js +1 -0
  314. package/neural-interface/public/vendor/codemirror/chunks/sparql-4M3KL752.js +1 -0
  315. package/neural-interface/public/vendor/codemirror/chunks/spreadsheet-3K23LA4N.js +1 -0
  316. package/neural-interface/public/vendor/codemirror/chunks/sql-VHRFMUBI.js +1 -0
  317. package/neural-interface/public/vendor/codemirror/chunks/stex-QZDAQUY2.js +1 -0
  318. package/neural-interface/public/vendor/codemirror/chunks/stylus-7ZDKAKBD.js +1 -0
  319. package/neural-interface/public/vendor/codemirror/chunks/swift-WCLCLGMR.js +1 -0
  320. package/neural-interface/public/vendor/codemirror/chunks/tcl-JLMU6WOK.js +1 -0
  321. package/neural-interface/public/vendor/codemirror/chunks/textile-4CHDKZ4T.js +1 -0
  322. package/neural-interface/public/vendor/codemirror/chunks/tiddlywiki-DKMCZZQ4.js +1 -0
  323. package/neural-interface/public/vendor/codemirror/chunks/tiki-F4JIG36Q.js +1 -0
  324. package/neural-interface/public/vendor/codemirror/chunks/toml-BHMHTIM2.js +1 -0
  325. package/neural-interface/public/vendor/codemirror/chunks/troff-YQ2KV5NT.js +1 -0
  326. package/neural-interface/public/vendor/codemirror/chunks/ttcn-ZHRCVFJY.js +1 -0
  327. package/neural-interface/public/vendor/codemirror/chunks/ttcn-cfg-KJOVTY4X.js +1 -0
  328. package/neural-interface/public/vendor/codemirror/chunks/turtle-535P5N6I.js +1 -0
  329. package/neural-interface/public/vendor/codemirror/chunks/vb-76UMQHOT.js +1 -0
  330. package/neural-interface/public/vendor/codemirror/chunks/vbscript-CGNDTK6Z.js +1 -0
  331. package/neural-interface/public/vendor/codemirror/chunks/velocity-VSJDO5T5.js +1 -0
  332. package/neural-interface/public/vendor/codemirror/chunks/verilog-4T6ETXIP.js +1 -0
  333. package/neural-interface/public/vendor/codemirror/chunks/vhdl-T7FNAXUN.js +1 -0
  334. package/neural-interface/public/vendor/codemirror/chunks/webidl-P6MJSJ6J.js +1 -0
  335. package/neural-interface/public/vendor/codemirror/chunks/xquery-UOVILRGN.js +1 -0
  336. package/neural-interface/public/vendor/codemirror/chunks/yacas-CV2APROL.js +1 -0
  337. package/neural-interface/public/vendor/codemirror/chunks/z80-3KFNVM4M.js +1 -0
  338. package/neural-interface/public/vendor/codemirror/codemirror-6.43.14-75f05e67.min.js +2 -0
  339. package/neural-interface/public/vendor/codemirror/manifest.json +60 -0
  340. package/neural-interface/scripts/build-vendor.mjs +78 -2
  341. package/neural-interface/scripts/rebuild-pty.js +15 -12
  342. package/neural-interface/scripts/vendor-codemirror-entry.js +121 -0
  343. package/neural-interface/server.js +1092 -1212
  344. package/neural-interface/templates/rulesets/README.md +1 -1
  345. package/package.json +13 -7
  346. package/postinstall.js +11 -81
  347. package/setup.js +111 -6
  348. package/benchmarks/browser-live-results.json +0 -79
  349. package/benchmarks/browser-v2-results.json +0 -258
  350. package/docs/browser-v2.md +0 -109
  351. package/docs/hook-integrity.md +0 -89
  352. package/docs/judgments.md +0 -182
  353. package/docs/style-guide.md +0 -246
  354. package/neural-interface/lib/codex-capabilities.d.ts +0 -41
  355. package/neural-interface/public/category-logos/claude.svg +0 -1
  356. package/neural-interface/public/category-logos/gemini-color.svg +0 -1
  357. package/neural-interface/public/category-logos/openai.svg +0 -1
  358. package/neural-interface/public/category-logos/synabun.png +0 -0
  359. package/neural-interface/public/claude.svg +0 -1
  360. package/neural-interface/public/gallery.svg +0 -6
  361. package/neural-interface/public/leonardoai.svg +0 -1
  362. package/neural-interface/public/mcp.svg +0 -4
  363. package/neural-interface/public/opencode-logo-dark-square.svg +0 -1
  364. package/neural-interface/public/pin-svgrepo-com.svg +0 -25
  365. package/neural-interface/public/sessionicon.svg +0 -8
  366. package/neural-interface/public/toolbar-album.svg +0 -8
  367. package/neural-interface/public/toolbar-question.svg +0 -6
  368. package/neural-interface/public/toolbar-sessions.svg +0 -6
  369. package/neural-interface/scripts/codex-protocol-fixture.mjs +0 -83
  370. package/scripts/audit-claude-hook-usage.mjs +0 -12
  371. package/scripts/audit-memory-duplicates.mjs +0 -44
  372. package/scripts/measure-claude-hook-overhead.mjs +0 -77
  373. package/scripts/recover-user-config.mjs +0 -254
  374. package/skills/synabun/synabunicon.png +0 -0
@@ -30,6 +30,8 @@ export const BUDGET_ENFORCEMENT = Object.freeze({
30
30
  });
31
31
 
32
32
  const OWNER_ONLY = { error: 'The assistant is available to the owner only', code: 'GUEST_FORBIDDEN' };
33
+ // What GET /catalog is made from when no catalog builder is wired: one object, so its answer is reused like any build's.
34
+ const NO_CATALOG = Object.freeze({});
33
35
 
34
36
  function sendError(res, error, fallbackStatus = 500) {
35
37
  if (error instanceof DispatchError || (error && error.code && error.status)) {
@@ -119,6 +121,11 @@ export function createAssistantApi({
119
121
  // Remote sessions (lib/remote-policy.js): the session's policy, or null. Defaults
120
122
  // to the runtime's view, else the process-wide registry.
121
123
  sessionPolicy = null,
124
+ // The user-context engine (assistant-user-context.js) and the project a new conversation starts in.
125
+ userContext = null,
126
+ // Why the engine did not start on this server (a string or null), for the settings page.
127
+ userContextError = () => null,
128
+ defaultProject = () => 'global',
122
129
  } = {}) {
123
130
  if (!dispatcher) throw new Error('createAssistantApi requires a dispatcher');
124
131
  const router = Router();
@@ -268,22 +275,26 @@ export function createAssistantApi({
268
275
  // ── catalog ────────────────────────────────────────────────────────────────
269
276
  router.get('/catalog', async (req, res) => {
270
277
  try {
271
- const force = req.query.refresh === '1';
278
+ // refresh=1 rebuilds from the providers' endpoints; deep=1 also makes them ask the CLI / app-server again.
279
+ const deep = req.query.deep === '1';
280
+ const force = req.query.refresh === '1' || deep;
272
281
  // Brain view / search: compact rows (OpenCode can list hundreds of models).
273
282
  const view = req.query.view === 'brain' ? 'brain' : req.query.view === 'manage' ? 'manage' : 'full';
274
283
  const provider = req.query.provider ? String(req.query.provider) : null;
275
284
  const q = req.query.q || req.query.search ? String(req.query.q || req.query.search) : null;
276
285
  // 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 })) });
286
+ if (view === 'manage' && catalog?.get) return res.json({ ok: true, ...(await catalog.get({ force, deep, view: 'manage', provider })) });
278
287
  if (catalog?.get && (view === 'brain' || provider || q)) {
279
288
  let preferences = {};
280
289
  try { preferences = configStore?.routing?.().preferences || {}; } catch {}
281
- const brief = await catalog.get({ force, view: 'brain', provider, q, preferences });
290
+ const brief = await catalog.get({ force, deep, view: 'brain', provider, q, preferences });
282
291
  return res.json({ ok: true, generatedAt: new Date(now()).toISOString(), ...brief, limits: dispatcher.limits, runs: dispatcher.list({ activeOnly: true }), totals: dispatcher.totals() });
283
292
  }
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 }) : {};
293
+ // The catalog says how long one of its builds is good for (one without a verified list is redone
294
+ // within seconds), so it is asked every time: this answer is reused only while it hands back the
295
+ // build the answer was made from (a rebuild or invalidate() gives another one).
296
+ const base = typeof buildCatalog === 'function' ? await buildCatalog({ force, deep }) : NO_CATALOG;
297
+ if (!force && catalogCache && catalogCache.base === base && now() - catalogCache.at < 30_000) return res.json(catalogCache.value);
287
298
  // Hidden rows stay server-side (view=manage lists them); the lists themselves ship.
288
299
  const { hiddenRows: _hiddenRows, ...visible } = base || {};
289
300
  const value = {
@@ -470,6 +481,41 @@ export function createAssistantApi({
470
481
  bounds: { minUsd: BUDGET_MIN_USD, maxUsd: BUDGET_MAX_USD }, session, pricing: pricingStatus, enforcement: BUDGET_ENFORCEMENT,
471
482
  };
472
483
  }
484
+ // ── User context (Settings → Automation behavior): the three layers and what a new conversation is told ──
485
+ function userContextPayload(req) {
486
+ let fallback = 'global';
487
+ try { fallback = String(defaultProject() || 'global'); } catch { fallback = 'global'; }
488
+ const project = String(req.query?.project || '').trim().slice(0, 200) || fallback;
489
+ return { ok: true, settings: configStore.userContext(), capabilities: userContext.capabilities(), preview: userContext.preview({ project }) };
490
+ }
491
+ const userContextReady = (res) => {
492
+ if (userContext && configStore?.userContext) return true;
493
+ let reason = null;
494
+ try { reason = typeof userContextError === 'function' ? userContextError() : userContextError; } catch { reason = null; }
495
+ 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 });
496
+ return false;
497
+ };
498
+ router.get('/user-context', (req, res) => {
499
+ if (!userContextReady(res)) return;
500
+ try { res.json(userContextPayload(req)); } catch (error) { sendError(res, error); }
501
+ });
502
+ router.put('/user-context', (req, res) => {
503
+ if (!userContextReady(res)) return;
504
+ if (!configStore.patchUserContext) return res.status(503).json({ ok: false, code: 'CONFIG_UNAVAILABLE', error: 'User context settings are read-only' });
505
+ if (callerContext(req).kind !== 'ui') return res.status(403).json({ ok: false, code: 'UI_ONLY', error: 'User context settings are changed from Settings only.' });
506
+ try {
507
+ const body = req.body || {};
508
+ const { project: _project, ...patch } = body.settings && typeof body.settings === 'object' ? body.settings : body;
509
+ configStore.patchUserContext(patch);
510
+ // Read live by the engine: the next turn (and the next brain start) follows it.
511
+ broadcastSync({ type: 'assistant:user-context-changed', settings: configStore.userContext() });
512
+ res.json(userContextPayload(req));
513
+ } catch (error) {
514
+ 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 });
515
+ sendError(res, error, 400);
516
+ }
517
+ });
518
+
473
519
  router.get('/budget', (req, res) => {
474
520
  if (!configStore?.budget) return res.status(503).json({ ok: false, code: 'CONFIG_UNAVAILABLE', error: 'Budget settings are not available' });
475
521
  try { res.json(budgetPayload(req.query.sessionId || null)); } catch (error) { sendError(res, error); }
@@ -779,6 +825,12 @@ export function createAssistantApi({
779
825
  router.post('/runs/:runId/complete', orchestrationGuard, (req, res) => {
780
826
  try { res.json(dispatcher.complete(req.params.runId)); } catch (error) { sendError(res, error); }
781
827
  });
828
+ // The user closed the sidepanel tab of a worker that finished its task: completed, never "stopped
829
+ // by the user" (409 RUN_NOT_FINISHED for any other run, which the tab then stops as before).
830
+ router.post('/runs/:runId/tab-closed', (req, res) => {
831
+ 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.' });
832
+ try { res.json(dispatcher.completeClosedTab(req.params.runId)); } catch (error) { sendError(res, error); }
833
+ });
782
834
  router.post('/runs/:runId/focus', (req, res) => {
783
835
  const run = dispatcher.get(req.params.runId);
784
836
  if (!run) return res.status(404).json({ ok: false, code: 'RUN_NOT_FOUND', error: `Unknown run ${req.params.runId}` });
@@ -835,6 +887,10 @@ export function createAssistantApi({
835
887
  if (req.body && 'computerUse' in req.body && (req.get('x-synabun-terminal') || req.get('x-synabun-desktop-grant'))) {
836
888
  return res.status(403).json({ ok: false, code: 'UI_ONLY', error: 'Only the user can switch computer use on or off.' });
837
889
  }
890
+ // Test density is the user's setting too: an agent cannot raise or lower it.
891
+ if (req.body && 'testDensity' in req.body && (req.get('x-synabun-terminal') || req.get('x-synabun-desktop-grant'))) {
892
+ return res.status(403).json({ ok: false, code: 'UI_ONLY', error: 'Only the user can change the Test density.' });
893
+ }
838
894
  try {
839
895
  const session = await runtime.updateSession(req.params.id, req.body || {});
840
896
  if (!session) return res.status(404).json({ ok: false, code: 'SESSION_NOT_FOUND', error: `Unknown assistant session ${req.params.id}` });
@@ -125,9 +125,13 @@ export function createClaudeBrain({ session, sink, deps = {}, persona = '', hook
125
125
  // prompt-submit.mjs steps aside (it injected every memory twice and paid Jev
126
126
  // twice) and stop.mjs skips the claim check (a relayed worker "tests pass"
127
127
  // has its evidence in the worker's turn, not here). Hooks read it from env.
128
+ // SYNABUN_ASSISTANT_THREADS: the user-context engine keeps this conversation's
129
+ // thread record (its nudges layer is on), so pre-compact.mjs asks for no
130
+ // conversation memory of its own.
128
131
  env: {
129
132
  ...(typeof deps.claudeAccountEnv === 'function' ? deps.claudeAccountEnv(brain.accountId) : {}),
130
133
  SYNABUN_ASSISTANT_SESSION: String(session.id),
134
+ ...(deps.userContextThreads === true ? { SYNABUN_ASSISTANT_THREADS: '1' } : {}),
131
135
  },
132
136
  maxBudgetUsd: Number(deps.maxBudgetUsd) > 0 ? Number(deps.maxBudgetUsd) : undefined,
133
137
  });
@@ -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
  }
@@ -4,7 +4,8 @@
4
4
  //
5
5
  // The assistant's user-editable knobs live in one JSON file: `defaultBrain`,
6
6
  // `persona`, `limits`, `memory` (read by the other assistant modules),
7
- // `routing` and the budget caps (owned here; see assistant-budget.js). Knobs
7
+ // `routing`, the budget caps (owned here; see assistant-budget.js) and
8
+ // `userContext` (the three layers of assistant-user-context.js). Knobs
8
9
  // are data: the file is re-read when its mtime changes, so a PUT from the
9
10
  // routes or budget editor, or a hand edit, applies without a restart. Writes
10
11
  // are atomic (tmp + rename) and keep every unknown key.
@@ -12,6 +13,7 @@
12
13
  import { existsSync, mkdirSync, readFileSync, renameSync, statSync, writeFileSync } from 'node:fs';
13
14
  import { dirname } from 'node:path';
14
15
  import { effectiveBudget, validateBudgetPatch, writeBudgetInto } from './assistant-budget.js';
16
+ import { normalizeUserContext, USER_CONTEXT_DEFAULTS } from './assistant-user-context-budgets.js';
15
17
 
16
18
  export const ROUTE_MODES = ['always-ask', 'ask-unsure', 'never'];
17
19
  export const PREFERENCE_KINDS = ['direct', 'dispatch'];
@@ -145,6 +147,22 @@ export function effectiveRouting(raw = {}) {
145
147
  };
146
148
  }
147
149
 
150
+ export const USER_CONTEXT_KEYS = Object.freeze(Object.keys(USER_CONTEXT_DEFAULTS));
151
+
152
+ /** A partial `userContext` patch: booleans only, known layers only. */
153
+ export function validateUserContextPatch(patch = {}) {
154
+ const fail = (field, message) => { const error = new Error(message); error.code = 'USER_CONTEXT_INVALID'; error.field = field; error.status = 400; return error; };
155
+ if (!patch || typeof patch !== 'object' || Array.isArray(patch)) throw fail('userContext', 'userContext must be an object');
156
+ const out = {};
157
+ for (const [key, value] of Object.entries(patch)) {
158
+ if (!USER_CONTEXT_KEYS.includes(key)) throw fail(key, `Unknown userContext setting "${key}"`);
159
+ if (typeof value !== 'boolean') throw fail(key, `userContext.${key} must be true or false`);
160
+ out[key] = value;
161
+ }
162
+ if (!Object.keys(out).length) throw fail('userContext', `Nothing to change: send one of ${USER_CONTEXT_KEYS.join(', ')}`);
163
+ return out;
164
+ }
165
+
148
166
  export function createAssistantConfigStore({ path, log = () => {}, now = Date.now } = {}) {
149
167
  if (!path) throw new Error('createAssistantConfigStore requires path');
150
168
  let cached = null; // { mtimeMs, value }
@@ -224,9 +242,20 @@ export function createAssistantConfigStore({ path, log = () => {}, now = Date.no
224
242
  return patchRouting({ preferences: { [key]: value } });
225
243
  }
226
244
 
245
+ /** The user-context layers (assistant-user-context.js): knowledge, nudges, trace. All on by default; read live. */
246
+ function userContext() { return normalizeUserContext(read().userContext); }
247
+ /** Switch layers (a validated partial patch). Every unknown key of the file is kept. */
248
+ function patchUserContext(patch = {}) {
249
+ const clean = validateUserContextPatch(patch);
250
+ const current = read();
251
+ const stored = current.userContext && typeof current.userContext === 'object' && !Array.isArray(current.userContext) ? current.userContext : {};
252
+ write({ ...current, userContext: { ...stored, ...clean, updatedAt: new Date(now()).toISOString() } });
253
+ return userContext();
254
+ }
255
+
227
256
  function onChange(fn) { listeners.add(fn); return () => listeners.delete(fn); }
228
257
 
229
- return { read, routing, version, patchRouting, setPreference, budget, budgetVersion, patchBudget, onChange, path };
258
+ return { read, routing, version, patchRouting, setPreference, budget, budgetVersion, patchBudget, userContext, patchUserContext, onChange, path };
230
259
  }
231
260
 
232
261
  export function fileExists(path) { try { return existsSync(path); } catch { return false; } }