synabun 2.0.0 → 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.
- package/CHANGELOG.md +57 -0
- package/NOTICE +5 -0
- package/README.md +1 -1
- package/THIRD-PARTY-LICENSES.md +46 -0
- package/benchmarks/fixtures/browser/README.md +1 -1
- package/benchmarks/fixtures/desktop/README.md +1 -1
- package/docs/whatsapp.md +2 -2
- package/hooks/claude-code/pre-compact.mjs +11 -0
- package/hooks/claude-code/prompt-submit.mjs +60 -184
- package/hooks/claude-code/session-start.mjs +73 -16
- package/hooks/claude-code/shared.mjs +20 -0
- package/hooks/claude-code/stop.mjs +5 -5
- package/lib/start-launcher.js +88 -1
- package/mcp-server/package-lock.json +2 -2
- package/mcp-server/package.json +1 -3
- package/neural-interface/i18n/en.json +1500 -153
- package/neural-interface/i18n/pt-BR.json +1498 -152
- package/neural-interface/lib/assistant-api.js +62 -6
- package/neural-interface/lib/assistant-brains/claude.js +4 -0
- package/neural-interface/lib/assistant-catalog.js +179 -27
- package/neural-interface/lib/assistant-config.js +31 -2
- package/neural-interface/lib/assistant-dispatch.js +62 -13
- package/neural-interface/lib/assistant-hidden-models.js +393 -66
- package/neural-interface/lib/assistant-memory.js +24 -10
- package/neural-interface/lib/assistant-persona.js +6 -2
- package/neural-interface/lib/assistant-pricing.js +21 -6
- package/neural-interface/lib/assistant-runtime.js +181 -12
- package/neural-interface/lib/assistant-task-prompt.js +13 -3
- package/neural-interface/lib/assistant-test-density.js +142 -0
- package/neural-interface/lib/assistant-user-context-budgets.js +123 -0
- package/neural-interface/lib/assistant-user-context.js +908 -0
- package/neural-interface/lib/assistant-user-knowledge.js +328 -0
- package/neural-interface/lib/assistant-user-learn.js +404 -0
- package/neural-interface/lib/assistant-user-threads.js +226 -0
- package/neural-interface/lib/assistant-user-writer.js +181 -0
- package/neural-interface/lib/browser-background-tab.js +134 -0
- package/neural-interface/lib/browser-screenshot.js +678 -61
- package/neural-interface/lib/claude-agent-bridge.js +33 -6
- package/neural-interface/lib/claude-model-catalog.js +30 -2
- package/neural-interface/lib/claude-temporary.js +46 -7
- package/neural-interface/lib/cloudflared/install.js +533 -0
- package/neural-interface/lib/cloudflared/manager.js +545 -0
- package/neural-interface/lib/cloudflared/pins.js +115 -0
- package/neural-interface/lib/cloudflared/pins.json +63 -0
- package/neural-interface/lib/cloudflared/tunnel.js +265 -0
- package/neural-interface/lib/editor-completion-endpoint.js +264 -0
- package/neural-interface/lib/editor-completion-opencode.js +436 -0
- package/neural-interface/lib/editor-completion.js +714 -0
- package/neural-interface/lib/editor-config.js +149 -0
- package/neural-interface/lib/editor-files-api.js +268 -0
- package/neural-interface/lib/editor-git-api.js +134 -0
- package/neural-interface/lib/file-explorer-api.js +1039 -0
- package/neural-interface/lib/file-explorer-system.js +355 -0
- package/neural-interface/lib/file-explorer-trash.ps1 +85 -0
- package/neural-interface/lib/file-explorer-watch.js +239 -0
- package/neural-interface/lib/git-diff-api.js +103 -0
- package/neural-interface/lib/git-read.js +202 -0
- package/neural-interface/lib/greeting/api.js +89 -0
- package/neural-interface/lib/greeting/budgets.js +90 -0
- package/neural-interface/lib/greeting/engine.js +461 -0
- package/neural-interface/lib/greeting/facts.js +320 -0
- package/neural-interface/lib/greeting/sources.js +252 -0
- package/neural-interface/lib/greeting/writer.js +139 -0
- package/neural-interface/lib/http-guards.js +189 -13
- package/neural-interface/lib/machine-names.js +88 -0
- package/neural-interface/lib/memory-api.js +31 -7
- package/neural-interface/lib/native-loop-providers.js +10 -5
- package/neural-interface/lib/plugin-installer.js +4 -2
- package/neural-interface/lib/project-storage.js +5 -7
- package/neural-interface/lib/request-gate.js +589 -0
- package/neural-interface/lib/ui-blob-api.js +108 -0
- package/neural-interface/lib/ui-blob-store.js +68 -0
- package/neural-interface/package-lock.json +701 -2
- package/neural-interface/package.json +13 -2
- package/neural-interface/public/install-app.html +9 -6
- package/neural-interface/public/offline.html +3 -2
- package/neural-interface/public/onboarding.html +12 -1
- package/neural-interface/public/shared/agent-runtime-options.js +22 -6
- package/neural-interface/public/shared/assistant/asst-automation.js +286 -0
- package/neural-interface/public/shared/assistant/asst-brain-picker.js +312 -62
- package/neural-interface/public/shared/assistant/asst-composer.js +11 -1
- package/neural-interface/public/shared/assistant/asst-mascot.js +18 -58
- package/neural-interface/public/shared/assistant/asst-menu.js +12 -8
- package/neural-interface/public/shared/assistant/asst-panel.js +150 -28
- package/neural-interface/public/shared/assistant/asst-render.js +162 -19
- package/neural-interface/public/shared/assistant/asst-route-models.js +108 -0
- package/neural-interface/public/shared/assistant/asst-route.js +23 -329
- package/neural-interface/public/shared/assistant/asst-routes-editor.js +495 -0
- package/neural-interface/public/shared/assistant/asst-sanitize.js +8 -1
- package/neural-interface/public/shared/assistant/asst-sidepanel.js +31 -9
- package/neural-interface/public/shared/assistant/asst-state.js +185 -1
- package/neural-interface/public/shared/assistant/asst-styles.js +158 -4
- package/neural-interface/public/shared/blob-store.js +111 -22
- package/neural-interface/public/shared/cdx/cdx-panel.js +32 -11
- package/neural-interface/public/shared/cdx/cdx-render.js +25 -89
- package/neural-interface/public/shared/cdx/cdx-styles.js +36 -0
- package/neural-interface/public/shared/cdx/cdx-tabs.js +87 -4
- package/neural-interface/public/shared/constants.js +6 -1
- package/neural-interface/public/shared/cp/cp-rehydrate.js +2 -1
- package/neural-interface/public/shared/cp/cp-styles.js +15 -0
- package/neural-interface/public/shared/editor/editor-ai-logic.js +409 -0
- package/neural-interface/public/shared/editor/editor-ai.js +619 -0
- package/neural-interface/public/shared/editor/editor-commands.js +212 -0
- package/neural-interface/public/shared/editor/editor-compare.js +103 -0
- package/neural-interface/public/shared/editor/editor-completion-list.js +172 -0
- package/neural-interface/public/shared/editor/editor-doc.js +257 -0
- package/neural-interface/public/shared/editor/editor-git-logic.js +131 -0
- package/neural-interface/public/shared/editor/editor-git.js +95 -0
- package/neural-interface/public/shared/editor/editor-languages.js +156 -0
- package/neural-interface/public/shared/editor/editor-lint.js +229 -0
- package/neural-interface/public/shared/editor/editor-loader.js +51 -0
- package/neural-interface/public/shared/editor/editor-markdown.js +90 -0
- package/neural-interface/public/shared/editor/editor-menu.js +233 -0
- package/neural-interface/public/shared/editor/editor-minimap.js +371 -0
- package/neural-interface/public/shared/editor/editor-palette-logic.js +192 -0
- package/neural-interface/public/shared/editor/editor-palette.js +125 -0
- package/neural-interface/public/shared/editor/editor-panel-logic.js +288 -0
- package/neural-interface/public/shared/editor/editor-panel.js +2099 -0
- package/neural-interface/public/shared/editor/editor-prefs.js +224 -0
- package/neural-interface/public/shared/editor/editor-safety.js +500 -0
- package/neural-interface/public/shared/editor/editor-search.js +352 -0
- package/neural-interface/public/shared/editor/editor-statusbar.js +144 -0
- package/neural-interface/public/shared/editor/editor-tabs-logic.js +331 -0
- package/neural-interface/public/shared/editor/editor-tabs.js +233 -0
- package/neural-interface/public/shared/editor/editor-theme.js +244 -0
- package/neural-interface/public/shared/editor/editor-view.js +332 -0
- package/neural-interface/public/shared/explorer/fx-api.js +216 -0
- package/neural-interface/public/shared/explorer/fx-links.js +204 -0
- package/neural-interface/public/shared/explorer/fx-logic.js +537 -0
- package/neural-interface/public/shared/explorer/fx-main.js +865 -0
- package/neural-interface/public/shared/explorer/fx-names.js +87 -0
- package/neural-interface/public/shared/explorer/fx-ops.js +370 -0
- package/neural-interface/public/shared/explorer/fx-popups.js +490 -0
- package/neural-interface/public/shared/explorer/fx-tree.js +790 -0
- package/neural-interface/public/shared/file-links.js +339 -0
- package/neural-interface/public/shared/greeting/greeting-card.js +178 -0
- package/neural-interface/public/shared/greeting/greeting-view.js +148 -0
- package/neural-interface/public/shared/html-shell.js +160 -165
- package/neural-interface/public/shared/ocp-v2/ocp-v2-childpanel.js +4 -0
- package/neural-interface/public/shared/ocp-v2/ocp-v2-greeting.js +47 -0
- package/neural-interface/public/shared/ocp-v2/ocp-v2-panel.js +99 -17
- package/neural-interface/public/shared/ocp-v2/ocp-v2-render.js +7 -1
- package/neural-interface/public/shared/ocp-v2/ocp-v2-send.js +5 -0
- package/neural-interface/public/shared/ocp-v2/ocp-v2-styles.js +40 -0
- package/neural-interface/public/shared/ocp-v2/ocp-v2-ws.js +3 -2
- package/neural-interface/public/shared/pwa-install-page.js +29 -1
- package/neural-interface/public/shared/pwa-install.css +3 -3
- package/neural-interface/public/shared/pwa-install.js +100 -1
- package/neural-interface/public/shared/settings/settings-code-editor-pane.js +412 -0
- package/neural-interface/public/shared/settings/settings-code-editor.js +328 -0
- package/neural-interface/public/shared/settings/settings-greeting.js +133 -0
- package/neural-interface/public/shared/settings/settings-ia.js +18 -4
- package/neural-interface/public/shared/settings/settings-user-context.js +154 -0
- package/neural-interface/public/shared/start-bridge.js +238 -19
- package/neural-interface/public/shared/styles.css +1414 -721
- package/neural-interface/public/shared/synabun-mascot.js +20 -56
- package/neural-interface/public/shared/tunnel-view.js +167 -0
- package/neural-interface/public/shared/ui-claude-panel.js +157 -16
- package/neural-interface/public/shared/ui-detail.js +15 -0
- package/neural-interface/public/shared/ui-explorer.js +22 -0
- package/neural-interface/public/shared/ui-file-explorer.js +481 -2496
- package/neural-interface/public/shared/ui-file-links.js +103 -0
- package/neural-interface/public/shared/ui-help.js +26 -0
- package/neural-interface/public/shared/ui-invite.js +126 -116
- package/neural-interface/public/shared/ui-keybinds.js +4 -0
- package/neural-interface/public/shared/ui-loading.js +159 -15
- package/neural-interface/public/shared/ui-menubar.js +12 -30
- package/neural-interface/public/shared/ui-native-loop-router-state.js +11 -0
- package/neural-interface/public/shared/ui-native-loop-router.js +15 -2
- package/neural-interface/public/shared/ui-navbar.js +45 -21
- package/neural-interface/public/shared/ui-settings.js +455 -488
- package/neural-interface/public/shared/ui-sidepanel-runtime.js +2 -1
- package/neural-interface/public/shared/ui-sync.js +16 -0
- package/neural-interface/public/shared/ui-terminal.js +23 -5
- package/neural-interface/public/shared/ui-tunnel.js +130 -0
- package/neural-interface/public/shared/ui-tutorial-steps.js +6 -2
- package/neural-interface/public/shared/ui-whiteboard.js +12 -0
- package/neural-interface/public/shared/ui-worker-end.js +245 -0
- package/neural-interface/public/shared/ui-workspaces.js +41 -27
- package/neural-interface/public/shared/worker-end.js +251 -0
- package/neural-interface/public/sw.js +2 -2
- package/neural-interface/public/vendor/codemirror/chunks/apl-CSDKT72Z.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/asciiarmor-JYYPU5BM.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/asn1-NRWCZDA6.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/asterisk-5OU6D4ZQ.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/brainfuck-ID4N4SYC.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/chunk-4RZWGCS2.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/chunk-ACTJM3JR.js +11 -0
- package/neural-interface/public/vendor/codemirror/chunks/chunk-S6BMAXLK.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/chunk-TO2RDOHQ.js +3 -0
- package/neural-interface/public/vendor/codemirror/chunks/chunk-US2GMZZQ.js +13 -0
- package/neural-interface/public/vendor/codemirror/chunks/chunk-WYYO5ZAJ.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/chunk-XALZTDJC.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/chunk-YRNUTRDV.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/chunk-Z3QOKH62.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/clike-OWQBSOGL.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/clojure-KV75UQWC.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/cmake-GTEP5Y3C.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/cobol-WCZ7QEGJ.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/coffeescript-TGLBOT6B.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/commonlisp-WVVI432V.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/crystal-S6R6HLKQ.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/css-PDA2RINP.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/cypher-HL5QPL4C.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/d-HHJH2HAJ.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/diff-MN3A35BY.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/dist-25SQLBVS.js +3 -0
- package/neural-interface/public/vendor/codemirror/chunks/dist-27ZGGJJS.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/dist-4E52OJIF.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/dist-6FT73XDC.js +2 -0
- package/neural-interface/public/vendor/codemirror/chunks/dist-BE6YGRMW.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/dist-CJSGKW2O.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/dist-CRPH2WLI.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/dist-CTG7JEQN.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/dist-I3FEVELX.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/dist-IM5BBSWZ.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/dist-KERUKTIF.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/dist-LXTMWZAM.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/dist-MABASSET.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/dist-MJVBUZLL.js +3 -0
- package/neural-interface/public/vendor/codemirror/chunks/dist-MNB3WZVU.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/dist-QYWXOE3U.js +7 -0
- package/neural-interface/public/vendor/codemirror/chunks/dist-TEBOFWLZ.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/dist-UNXFROM3.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/dist-XL2SLQO6.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/dist-XO42Y3AP.js +6 -0
- package/neural-interface/public/vendor/codemirror/chunks/dist-Y2WWDWJK.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/dist-YFEZKWMU.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/dist-ZY7SWQO6.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/dockerfile-3Y4R4NCH.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/dtd-5343GJ4P.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/dylan-FI2CXSW2.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/ebnf-GNQ77YWA.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/ecl-MRPUAE2L.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/eiffel-DZROES27.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/elm-4KICRFKQ.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/erlang-3IHYC263.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/factor-C6D3E5UK.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/fcl-VYTWUJMI.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/forth-ZBO2EH5M.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/fortran-5NDCRWIK.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/gas-SKRMOXFP.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/gherkin-L45YXZLW.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/groovy-GLPTLN44.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/haskell-E7NF7P4Y.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/haxe-K2NVDOB6.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/http-Q6DRQIJB.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/idl-MZYNJVLN.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/javascript-6YRYK2HR.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/julia-HXEL2T5G.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/livescript-MV7CI2JZ.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/lua-E36NHOOD.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/mathematica-3XOZZCMC.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/mbox-ODYGFKS7.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/mirc-WB43AWF5.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/mllike-ZT6ET7ZH.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/modelica-LLMZPFGA.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/mscgen-2OPNNYFH.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/mumps-6P3HWJH6.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/nginx-TCGKQTRW.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/nsis-ATIGIXTJ.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/ntriples-5PLBI5OV.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/octave-QF2S3NL6.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/oz-JFIVFFH2.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/pascal-JG6SW76V.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/perl-4B3IWVVO.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/pig-ABEBH4MI.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/powershell-H32XE7TL.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/properties-NN644MMA.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/protobuf-YKWQK2J3.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/pug-RIYXEKFW.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/puppet-JZQ5WPEV.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/python-EBCGGHAC.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/q-5UMH2YQY.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/r-7XMGG453.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/rpm-IEV7TSLR.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/ruby-T4SPFPNM.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/sas-GEDX67ET.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/scheme-FTLOWKPC.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/shell-HUFRKFSJ.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/sieve-P5QUDVYO.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/smalltalk-2SORJNQI.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/solr-M22TGAR5.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/sparql-4M3KL752.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/spreadsheet-3K23LA4N.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/sql-VHRFMUBI.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/stex-QZDAQUY2.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/stylus-7ZDKAKBD.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/swift-WCLCLGMR.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/tcl-JLMU6WOK.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/textile-4CHDKZ4T.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/tiddlywiki-DKMCZZQ4.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/tiki-F4JIG36Q.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/toml-BHMHTIM2.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/troff-YQ2KV5NT.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/ttcn-ZHRCVFJY.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/ttcn-cfg-KJOVTY4X.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/turtle-535P5N6I.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/vb-76UMQHOT.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/vbscript-CGNDTK6Z.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/velocity-VSJDO5T5.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/verilog-4T6ETXIP.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/vhdl-T7FNAXUN.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/webidl-P6MJSJ6J.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/xquery-UOVILRGN.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/yacas-CV2APROL.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/z80-3KFNVM4M.js +1 -0
- package/neural-interface/public/vendor/codemirror/codemirror-6.43.14-75f05e67.min.js +2 -0
- package/neural-interface/public/vendor/codemirror/manifest.json +60 -0
- package/neural-interface/scripts/build-vendor.mjs +78 -2
- package/neural-interface/scripts/vendor-codemirror-entry.js +121 -0
- package/neural-interface/server.js +873 -1028
- package/neural-interface/templates/rulesets/README.md +1 -1
- package/package.json +2 -8
- package/setup.js +44 -7
- package/benchmarks/browser-live-results.json +0 -79
- package/benchmarks/browser-v2-results.json +0 -258
- package/neural-interface/lib/codex-capabilities.d.ts +0 -41
- package/neural-interface/public/category-logos/claude.svg +0 -1
- package/neural-interface/public/category-logos/gemini-color.svg +0 -1
- package/neural-interface/public/category-logos/openai.svg +0 -1
- package/neural-interface/public/category-logos/synabun.png +0 -0
- package/neural-interface/public/claude.svg +0 -1
- package/neural-interface/public/gallery.svg +0 -6
- package/neural-interface/public/leonardoai.svg +0 -1
- package/neural-interface/public/mcp.svg +0 -4
- package/neural-interface/public/opencode-logo-dark-square.svg +0 -1
- package/neural-interface/public/pin-svgrepo-com.svg +0 -25
- package/neural-interface/public/sessionicon.svg +0 -8
- package/neural-interface/public/toolbar-album.svg +0 -8
- package/neural-interface/public/toolbar-question.svg +0 -6
- package/neural-interface/public/toolbar-sessions.svg +0 -6
- package/neural-interface/scripts/codex-protocol-fixture.mjs +0 -83
- package/skills/synabun/synabunicon.png +0 -0
|
@@ -12,18 +12,71 @@
|
|
|
12
12
|
// Matching (lib/assistant-catalog.js hiddenModelId): the id, or the row's
|
|
13
13
|
// resolved model at the same context window, case-insensitive; context
|
|
14
14
|
// variants ("opus" / "opus[1m]", "gpt-x" / "gpt-x[extended]") hide
|
|
15
|
-
// independently. OpenCode ids match full or bare. Re-read when the file
|
|
16
|
-
//
|
|
15
|
+
// independently. OpenCode ids match full or bare. Re-read when the file
|
|
16
|
+
// changes (time, size or inode); writes are atomic.
|
|
17
|
+
//
|
|
18
|
+
// Every save is one read-modify-write of the file as it is on disk at that
|
|
19
|
+
// moment, alone among SynaBun's writers (two servers can share a data home):
|
|
20
|
+
// an OS-held mutex next to the data (hiddenModelsLockPath; an empty SQLite file,
|
|
21
|
+
// lib/rulesets/lock.js) and, right before the new file takes the old one's
|
|
22
|
+
// place, a second look at what is there. A document someone saved in between (a
|
|
23
|
+
// hand edit, an older build, which take no mutex) is the newer one: the change
|
|
24
|
+
// is made again on it, and an automatic one (the pin migration below) that
|
|
25
|
+
// cannot be made that way is not saved at all.
|
|
26
|
+
//
|
|
27
|
+
// A hidden alias hides a model, not a name. "haiku", "opus", "default" name
|
|
28
|
+
// whatever Claude Code points them at, and a CLI update moves them. So an alias
|
|
29
|
+
// entry carries a pin: the model and context window it named when the user hid
|
|
30
|
+
// it, its label and the time (`pins` in the file, next to `providers`; a build
|
|
31
|
+
// that knows nothing of pins still reads the lists). Each catalog build settles
|
|
32
|
+
// the pins against the rows the provider lists (reconcile → planHiddenPins):
|
|
33
|
+
// the alias still names the pinned model → hidden, as before
|
|
34
|
+
// the alias names another model → the alias is shown again and the
|
|
35
|
+
// pinned model stays hidden under
|
|
36
|
+
// its own id (the entry is rewritten)
|
|
37
|
+
// an alias entry with no pin (written by → pinned to what the alias names
|
|
38
|
+
// an older build, or hidden while the now, marked `inferred`
|
|
39
|
+
// list was not a verified one)
|
|
40
|
+
// an entry already rewritten, whose model → the entry takes the id the model is
|
|
41
|
+
// is listed under another id listed by (it was absent, or respelled)
|
|
42
|
+
// A pinned entry hides its own row only. What an alias names comes from the
|
|
43
|
+
// row's `upstream`, which is the `resolvedModel` the Claude Code CLI reports
|
|
44
|
+
// for it. Nothing is decided without it: while the list is the static
|
|
45
|
+
// stand-in, the provider does not answer, the alias is not listed, or the CLI
|
|
46
|
+
// names no versioned model behind it, an entry keeps hiding the alias by name.
|
|
17
47
|
|
|
18
|
-
import {
|
|
19
|
-
import {
|
|
48
|
+
import { createHash } from 'node:crypto';
|
|
49
|
+
import { mkdirSync, readFileSync, renameSync, rmSync, statSync, writeFileSync } from 'node:fs';
|
|
50
|
+
import { basename, dirname, join } from 'node:path';
|
|
20
51
|
import { Router } from 'express';
|
|
21
|
-
import { CATALOG_PROVIDERS, baseModelId, canonicalModelId, contextSuffix } from './assistant-catalog.js';
|
|
52
|
+
import { CATALOG_PROVIDERS, aliasTargetOf, baseModelId, canonicalModelId, contextSuffix, contextVariantOf, modelIdentity, pinnedModelId, sameModelPin } from './assistant-catalog.js';
|
|
22
53
|
import { HIDDEN_MODELS_SYNC_TYPE as OPENCODE_SYNC_TYPE } from './opencode-hidden-models.js';
|
|
54
|
+
import { createWriterLock } from './rulesets/lock.js';
|
|
23
55
|
|
|
24
56
|
export const ASSISTANT_HIDDEN_SYNC_TYPE = 'assistant:hidden-models-changed';
|
|
25
57
|
export const OWN_PROVIDERS = Object.freeze(['claude-code', 'codex']);
|
|
26
58
|
export const MAX_HIDDEN_PER_PROVIDER = 2000;
|
|
59
|
+
/** How long a save waits for another process that is saving. */
|
|
60
|
+
export const HIDDEN_LOCK_TIMEOUT_MS = 2000;
|
|
61
|
+
/** The same for the pin migration, which runs inside a catalog build and is made again by the next one. */
|
|
62
|
+
export const HIDDEN_MIGRATION_WAIT_MS = 250;
|
|
63
|
+
/** How many times one save is made again on a file that changed under it before it gives up. */
|
|
64
|
+
export const HIDDEN_SAVE_ATTEMPTS = 3;
|
|
65
|
+
|
|
66
|
+
const FILE_IO = Object.freeze({ mkdirSync, readFileSync, renameSync, rmSync, statSync, writeFileSync });
|
|
67
|
+
const NO_FILE = 'absent';
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* The mutex that goes with a hidden-models file: <data home>/runtime/<name>.mutex
|
|
71
|
+
* for <data home>/data/<name>.json, outside data/ so no backup or restore ever
|
|
72
|
+
* opens it (see lib/rulesets/lock.js on why nothing may), and beside the file
|
|
73
|
+
* for any other layout.
|
|
74
|
+
*/
|
|
75
|
+
export function hiddenModelsLockPath(file) {
|
|
76
|
+
const dir = dirname(file);
|
|
77
|
+
const name = `${basename(file).replace(/\.json$/i, '')}.mutex`;
|
|
78
|
+
return basename(dir) === 'data' ? join(dirname(dir), 'runtime', name) : join(dir, name);
|
|
79
|
+
}
|
|
27
80
|
|
|
28
81
|
function hiddenError(code, message, status = 400, extra = {}) {
|
|
29
82
|
const error = new Error(message);
|
|
@@ -46,66 +99,266 @@ export function normalizeModelIds(list) {
|
|
|
46
99
|
return [...out.values()].sort();
|
|
47
100
|
}
|
|
48
101
|
|
|
49
|
-
|
|
50
|
-
|
|
102
|
+
const text = (value, max = 300) => (typeof value === 'string' && value.trim() ? value.trim().slice(0, max) : null);
|
|
103
|
+
|
|
104
|
+
/** One pin record, or null when it names no model: { model, variant, label, at, alias?, movedAt?, inferred? }. */
|
|
105
|
+
export function normalizePin(raw) {
|
|
106
|
+
if (!raw || typeof raw !== 'object') return null;
|
|
107
|
+
const model = text(baseModelId(text(raw.model) || ''))?.toLowerCase() || null;
|
|
108
|
+
if (!model) return null;
|
|
109
|
+
const variant = contextVariantOf(`x[${String(raw.variant || '').toLowerCase()}]`);
|
|
110
|
+
const pin = { model, variant, label: text(raw.label, 200), at: text(raw.at, 40) };
|
|
111
|
+
if (text(raw.alias)) pin.alias = text(raw.alias);
|
|
112
|
+
if (text(raw.movedAt, 40)) pin.movedAt = text(raw.movedAt, 40);
|
|
113
|
+
if (raw.inferred === true) pin.inferred = true;
|
|
114
|
+
return pin;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/** The pins of the ids in `ids`, keyed by lower-case id; anything else is dropped. */
|
|
118
|
+
export function normalizePins(raw, ids) {
|
|
119
|
+
const out = {};
|
|
120
|
+
if (!raw || typeof raw !== 'object') return out;
|
|
121
|
+
const source = new Map(Object.entries(raw).map(([key, value]) => [String(key).toLowerCase(), value]));
|
|
122
|
+
for (const key of arr(ids).map((id) => String(id).toLowerCase()).sort()) {
|
|
123
|
+
const pin = normalizePin(source.get(key));
|
|
124
|
+
if (pin) out[key] = pin;
|
|
125
|
+
}
|
|
126
|
+
return out;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/** The pin to record when `row` is hidden: what the alias names now, with its label. null for a row that is its own model. */
|
|
130
|
+
export function pinForRow(row) {
|
|
131
|
+
const target = aliasTargetOf(row);
|
|
132
|
+
return target ? { ...target, label: text(row.label, 200) } : null;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Settle one provider's hidden ids against the rows it lists (pure).
|
|
137
|
+
* ids, pins the stored list and its pin records
|
|
138
|
+
* rows every row of the provider, hidden ones included
|
|
139
|
+
* source how the list was obtained; only 'live' decides anything
|
|
140
|
+
* at the time to record (ISO)
|
|
141
|
+
* → { ids, pins, changed, moved: [{ alias, from, to, listed }], pinned: [ids] }
|
|
142
|
+
*/
|
|
143
|
+
export function planHiddenPins({ ids = [], pins = {}, rows = [], source = null, at = null } = {}) {
|
|
144
|
+
const live = source === 'live';
|
|
145
|
+
const list = arr(rows).filter((row) => row?.id);
|
|
146
|
+
const byId = new Map(list.map((row) => [String(row.id).toLowerCase(), row]));
|
|
147
|
+
const current = normalizePins(pins, ids);
|
|
148
|
+
const nextIds = [];
|
|
149
|
+
const nextPins = {};
|
|
150
|
+
const seen = new Set();
|
|
151
|
+
const keep = (id, pin) => {
|
|
152
|
+
const key = String(id).toLowerCase();
|
|
153
|
+
if (seen.has(key)) return false;
|
|
154
|
+
seen.add(key);
|
|
155
|
+
nextIds.push(id);
|
|
156
|
+
if (pin) nextPins[key] = pin;
|
|
157
|
+
return true;
|
|
158
|
+
};
|
|
159
|
+
const moved = [];
|
|
160
|
+
const pinned = [];
|
|
161
|
+
const movedOn = [];
|
|
162
|
+
const adrift = [];
|
|
163
|
+
// The row that is the model a pin names, under the id the provider lists it by (a snapshot date is the same model).
|
|
164
|
+
const ownRowOf = (pin) => list.find((row) => !aliasTargetOf(row) && sameModelPin(pin, { model: baseModelId(row.id), variant: contextVariantOf(row.id) })) || null;
|
|
165
|
+
for (const id of arr(ids)) {
|
|
166
|
+
const key = String(id).toLowerCase();
|
|
167
|
+
const pin = current[key] || null;
|
|
168
|
+
const row = byId.get(key) || null;
|
|
169
|
+
// An entry already rewritten to its model's own id (`alias` says from where) is that model, never an
|
|
170
|
+
// alias again. It hides the model whatever id the provider lists it by today: with no row under the
|
|
171
|
+
// stored id it is settled below, by model.
|
|
172
|
+
if (live && pin?.alias) { if (row) keep(id, pin); else adrift.push({ id, pin }); continue; }
|
|
173
|
+
const target = live && row ? aliasTargetOf(row) : null;
|
|
174
|
+
// Not an alias with a model named behind it, or nothing verified to read it from: as it is.
|
|
175
|
+
if (!target) { keep(id, pin); continue; }
|
|
176
|
+
if (!pin) {
|
|
177
|
+
// Written before pins existed: it hides what the alias names now, as it did until this moment.
|
|
178
|
+
keep(id, { ...target, label: text(row.label, 200), at, inferred: true });
|
|
179
|
+
pinned.push(id);
|
|
180
|
+
continue;
|
|
181
|
+
}
|
|
182
|
+
if (sameModelPin(pin, target)) { keep(id, pin); continue; }
|
|
183
|
+
movedOn.push({ id, pin, target });
|
|
184
|
+
}
|
|
185
|
+
// After every entry that stays, so an entry the user made for the old model keeps its own meaning.
|
|
186
|
+
// The model is listed again, or under another id (it was absent when its alias moved on, or the provider
|
|
187
|
+
// respelled it): the entry follows it. Not listed at all: it waits under the id it has.
|
|
188
|
+
for (const { id, pin } of adrift) {
|
|
189
|
+
const own = ownRowOf(pin);
|
|
190
|
+
keep(own ? String(own.id) : id, pin);
|
|
191
|
+
}
|
|
192
|
+
for (const { id, pin, target } of movedOn) {
|
|
193
|
+
const own = ownRowOf(pin);
|
|
194
|
+
const concrete = own ? String(own.id) : pinnedModelId(pin);
|
|
195
|
+
keep(concrete, { model: pin.model, variant: pin.variant, label: pin.label, at: pin.at, alias: String(id), movedAt: at });
|
|
196
|
+
moved.push({ alias: String(id), from: concrete, to: pinnedModelId(target), listed: !!own });
|
|
197
|
+
}
|
|
198
|
+
const sameIds = normalizeModelIds(nextIds).join('\n') === normalizeModelIds(ids).join('\n');
|
|
199
|
+
const finalPins = normalizePins(nextPins, nextIds);
|
|
200
|
+
const changed = !sameIds || JSON.stringify(finalPins) !== JSON.stringify(normalizePins(pins, ids));
|
|
201
|
+
return { ids: normalizeModelIds(nextIds), pins: finalPins, changed, moved, pinned };
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* The Claude Code / Codex store:
|
|
206
|
+
* { version, updatedAt, providers: { 'claude-code': [], codex: [] }, pins: { 'claude-code': { [id]: pin }, codex: {} } }.
|
|
207
|
+
*/
|
|
208
|
+
export function createAssistantHiddenModelsStore({
|
|
209
|
+
path, now = Date.now, log = () => {},
|
|
210
|
+
lockPath = path ? hiddenModelsLockPath(path) : null, lockTimeoutMs = HIDDEN_LOCK_TIMEOUT_MS,
|
|
211
|
+
// The file calls (node:fs by default): a seam for tests that save between two of them.
|
|
212
|
+
fs: io = FILE_IO,
|
|
213
|
+
} = {}) {
|
|
51
214
|
if (!path) throw new Error('createAssistantHiddenModelsStore requires path');
|
|
52
215
|
const emptyProviders = () => Object.fromEntries(OWN_PROVIDERS.map((p) => [p, []]));
|
|
53
|
-
const
|
|
54
|
-
|
|
216
|
+
const emptyPins = () => Object.fromEntries(OWN_PROVIDERS.map((p) => [p, {}]));
|
|
217
|
+
const empty = Object.freeze({ providers: emptyProviders(), pins: emptyPins(), version: 0, updatedAt: null });
|
|
218
|
+
let cached = null; // { stamp, value }
|
|
55
219
|
const listeners = new Set();
|
|
56
220
|
|
|
57
|
-
|
|
58
|
-
function
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
221
|
+
/** Which file is there, by stat alone: '' when there is none. An atomic save is a new inode even inside one clock tick. */
|
|
222
|
+
function stamp() { try { const info = io.statSync(path); return `${info.mtimeMs}:${info.size}:${info.ino}`; } catch { return ''; } }
|
|
223
|
+
/** The revision of the file on disk: a digest of its bytes, NO_FILE when there is none. */
|
|
224
|
+
function revisionOf(raw) { return createHash('sha256').update(raw).digest('hex'); }
|
|
225
|
+
function revisionOnDisk() {
|
|
226
|
+
try { return revisionOf(io.readFileSync(path, 'utf8')); }
|
|
227
|
+
catch (error) { return error?.code === 'ENOENT' ? NO_FILE : `unreadable:${error?.code || 'read'}`; }
|
|
228
|
+
}
|
|
229
|
+
/** The document on disk, never the copy in memory: { value, rev, readable } (`readable` false = it is there and does not parse). */
|
|
230
|
+
function load() {
|
|
231
|
+
let raw;
|
|
232
|
+
try { raw = io.readFileSync(path, 'utf8'); }
|
|
233
|
+
catch (error) {
|
|
234
|
+
if (error?.code === 'ENOENT') return { value: empty, rev: NO_FILE, readable: true };
|
|
235
|
+
log('hidden-models:unreadable', error?.message || String(error));
|
|
236
|
+
return { value: cached?.value || empty, rev: `unreadable:${error?.code || 'read'}`, readable: false };
|
|
237
|
+
}
|
|
238
|
+
try {
|
|
239
|
+
const doc = JSON.parse(raw) || {};
|
|
240
|
+
const providers = emptyProviders();
|
|
241
|
+
const pins = emptyPins();
|
|
242
|
+
for (const p of OWN_PROVIDERS) {
|
|
243
|
+
providers[p] = normalizeModelIds(doc.providers?.[p]);
|
|
244
|
+
pins[p] = normalizePins(doc.pins?.[p], providers[p]);
|
|
245
|
+
}
|
|
246
|
+
return { value: { providers, pins, version: Number(doc.version) || 0, updatedAt: doc.updatedAt || null }, rev: revisionOf(raw), readable: true };
|
|
247
|
+
} catch (error) {
|
|
248
|
+
log('hidden-models:unreadable', error?.message || String(error));
|
|
249
|
+
return { value: cached?.value || empty, rev: revisionOf(raw), readable: false };
|
|
69
250
|
}
|
|
70
|
-
|
|
251
|
+
}
|
|
252
|
+
function read() {
|
|
253
|
+
const s = stamp();
|
|
254
|
+
if (cached && cached.stamp === s) return cached.value;
|
|
255
|
+
const value = s ? load().value : empty;
|
|
256
|
+
cached = { stamp: s, value };
|
|
71
257
|
return value;
|
|
72
258
|
}
|
|
73
259
|
function list(provider) { return read().providers[provider] || []; }
|
|
260
|
+
function pins(provider) { return read().pins?.[provider] || {}; }
|
|
261
|
+
|
|
262
|
+
/** Run `fn` as the only SynaBun writer of the file. Throws (nothing was changed) when the mutex cannot be had within `waitMs`. */
|
|
263
|
+
function alone(fn, waitMs = lockTimeoutMs) {
|
|
264
|
+
const lock = createWriterLock(lockPath, { timeoutMs: Math.min(lockTimeoutMs, waitMs) });
|
|
265
|
+
try { lock.enter(); } catch (error) {
|
|
266
|
+
lock.close();
|
|
267
|
+
if (error?.code === 'BUSY') throw hiddenError('HIDDEN_BUSY', 'Another SynaBun process is saving the hidden-models list right now, so nothing was changed. Try again in a moment.', 503, { retryable: true });
|
|
268
|
+
throw hiddenError('HIDDEN_LOCK_UNAVAILABLE', `SynaBun could not lock the hidden-models list (${lockPath}): ${error?.cause?.message || error?.message || error}. Nothing was changed.`, 503);
|
|
269
|
+
}
|
|
270
|
+
// One connection per save, closed with it: nothing stays open on the mutex between two saves.
|
|
271
|
+
try { return fn(); } finally { lock.leave(); lock.close(); }
|
|
272
|
+
}
|
|
74
273
|
|
|
75
|
-
/**
|
|
76
|
-
|
|
274
|
+
/**
|
|
275
|
+
* One read-modify-write of one provider's list. `decide(document)` gets the document as it is
|
|
276
|
+
* on disk at that moment and answers { ids, pins? } (`pins` default: the ones the provider has; a
|
|
277
|
+
* pin whose id left the list goes with it) or null for no change. It runs again when the file
|
|
278
|
+
* changed before the new one could take its place, so it must be a function of the document it
|
|
279
|
+
* is given. Nothing is saved over a file that is there and does not parse (someone is in the
|
|
280
|
+
* middle of saving it, or it needs a hand). `reason` reaches the listeners; `waitMs` shortens
|
|
281
|
+
* the wait for another process that is saving (the server's event loop stands still meanwhile).
|
|
282
|
+
* Returns { providers, pins, version, updatedAt, changed }.
|
|
283
|
+
*/
|
|
284
|
+
function change(provider, decide, { reason = null, waitMs = lockTimeoutMs } = {}) {
|
|
77
285
|
if (!OWN_PROVIDERS.includes(provider)) throw hiddenError('PROVIDER_INVALID', `provider must be one of ${OWN_PROVIDERS.join(', ')}`);
|
|
78
|
-
const
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
286
|
+
const saved = alone(() => {
|
|
287
|
+
for (let attempt = 1; ; attempt += 1) {
|
|
288
|
+
const found = load();
|
|
289
|
+
if (!found.readable) throw hiddenError('HIDDEN_UNREADABLE', `The hidden-models file (${path}) could not be read as JSON right now, so it was left as it is. If it stays that way, fix or remove it.`, 409, { retryable: true });
|
|
290
|
+
const current = found.value;
|
|
291
|
+
const wanted = decide(current);
|
|
292
|
+
const nextList = wanted ? normalizeModelIds(wanted.ids) : current.providers[provider];
|
|
293
|
+
const nextPins = wanted ? normalizePins(wanted.pins === undefined ? current.pins?.[provider] : wanted.pins, nextList) : null;
|
|
294
|
+
const changed = !!wanted && (current.providers[provider].join('\n') !== nextList.join('\n')
|
|
295
|
+
|| JSON.stringify(nextPins) !== JSON.stringify(current.pins?.[provider] || {}));
|
|
296
|
+
if (!changed) return { value: current, changed: false };
|
|
297
|
+
const next = {
|
|
298
|
+
version: current.version + 1, updatedAt: new Date(now()).toISOString(),
|
|
299
|
+
providers: { ...current.providers, [provider]: nextList },
|
|
300
|
+
pins: { ...emptyPins(), ...current.pins, [provider]: nextPins },
|
|
301
|
+
};
|
|
302
|
+
io.mkdirSync(dirname(path), { recursive: true });
|
|
303
|
+
const tmp = `${path}.${process.pid}.${now()}.tmp`;
|
|
304
|
+
try {
|
|
305
|
+
io.writeFileSync(tmp, JSON.stringify(next, null, 2) + '\n', 'utf8');
|
|
306
|
+
// Once more, with the new file ready: a writer outside the mutex (a hand edit, an older build) may
|
|
307
|
+
// have saved since `found` was read. Its document is the newer one and is never replaced by a
|
|
308
|
+
// decision made without it.
|
|
309
|
+
if (revisionOnDisk() !== found.rev) {
|
|
310
|
+
if (attempt >= HIDDEN_SAVE_ATTEMPTS) throw hiddenError('HIDDEN_BUSY', 'The hidden-models file kept changing while SynaBun was saving it, so it was left as it is. Try again in a moment.', 503, { retryable: true });
|
|
311
|
+
continue;
|
|
312
|
+
}
|
|
313
|
+
io.renameSync(tmp, path);
|
|
314
|
+
} finally {
|
|
315
|
+
try { io.rmSync(tmp, { force: true }); } catch {}
|
|
316
|
+
}
|
|
317
|
+
cached = { stamp: stamp(), value: next };
|
|
318
|
+
return { value: next, changed: true };
|
|
319
|
+
}
|
|
320
|
+
}, waitMs);
|
|
321
|
+
// Outside the mutex: a listener may read or save in its turn.
|
|
322
|
+
if (saved.changed) for (const listener of listeners) { try { listener(saved.value, { provider, changed: true, reason }); } catch {} }
|
|
323
|
+
return { ...saved.value, changed: saved.changed };
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
/**
|
|
327
|
+
* Replace one provider's list. `pins` replaces its pin records (default: the ones it has);
|
|
328
|
+
* a pin whose id left the list goes with it. `reason` reaches the listeners.
|
|
329
|
+
* Returns { providers, pins, version, updatedAt, changed }.
|
|
330
|
+
*/
|
|
331
|
+
function set(provider, ids, { pins: nextPinsRaw = undefined, reason = null } = {}) {
|
|
332
|
+
return change(provider, () => ({ ids, pins: nextPinsRaw }), { reason });
|
|
90
333
|
}
|
|
91
334
|
|
|
92
335
|
function onChange(fn) { listeners.add(fn); return () => listeners.delete(fn); }
|
|
93
|
-
return { read, list, set, onChange, path };
|
|
336
|
+
return { read, list, pins, set, change, onChange, path, lockPath };
|
|
94
337
|
}
|
|
95
338
|
|
|
96
339
|
/**
|
|
97
340
|
* One view over both stores.
|
|
98
341
|
* list(provider) → ids; all() → { 'claude-code', codex, opencode }
|
|
99
|
-
*
|
|
100
|
-
*
|
|
342
|
+
* pins(provider) → { [id]: pin }; allPins() → the same per provider (OpenCode has none)
|
|
343
|
+
* update(provider, { hide, show, pins }) → { provider, hidden, changed, added, removed };
|
|
344
|
+
* `pins` = { [id]: { model, variant, label } } for the aliases among `hide` (pinForRow)
|
|
345
|
+
* reconcile({ provider, rows, source }) → { ids, pins, moved, pinned } | null: settles the
|
|
346
|
+
* pins against the rows a catalog build lists (planHiddenPins) and saves what changed
|
|
347
|
+
* Both save through the store's `change`: on the document that is on disk at that moment.
|
|
348
|
+
* onChange(fn) → fn({ provider, reason, moved }) after either store changes; reason 'pins' =
|
|
349
|
+
* reconcile's own write (`moved` lists the aliases that were shown again)
|
|
101
350
|
* OpenCode ids are "provider/model" (its store drops anything else); callers
|
|
102
351
|
* resolve bare ids first (the router does, from the catalog).
|
|
103
352
|
*/
|
|
104
|
-
export function createHiddenModels({ file, opencodeStore = null, now = Date.now, log = () => {} } = {}) {
|
|
105
|
-
const own = createAssistantHiddenModelsStore({ path: file, now, log });
|
|
353
|
+
export function createHiddenModels({ file, opencodeStore = null, now = Date.now, log = () => {}, lockPath = undefined, lockTimeoutMs = undefined, fs = undefined } = {}) {
|
|
354
|
+
const own = createAssistantHiddenModelsStore({ path: file, now, log, lockPath, lockTimeoutMs, fs });
|
|
106
355
|
const listeners = new Set();
|
|
107
|
-
|
|
108
|
-
|
|
356
|
+
let settling = null; // what reconcile is saving right now, for its change event
|
|
357
|
+
const emit = (provider, reason = null) => {
|
|
358
|
+
const event = { provider, reason, moved: reason === 'pins' ? settling?.moved || [] : [] };
|
|
359
|
+
for (const fn of listeners) { try { fn(event); } catch {} }
|
|
360
|
+
};
|
|
361
|
+
own.onChange((_value, meta) => emit(meta?.provider || null, meta?.reason || null));
|
|
109
362
|
opencodeStore?.onChange?.(() => emit('opencode'));
|
|
110
363
|
|
|
111
364
|
function list(provider) {
|
|
@@ -113,29 +366,78 @@ export function createHiddenModels({ file, opencodeStore = null, now = Date.now,
|
|
|
113
366
|
return OWN_PROVIDERS.includes(provider) ? own.list(provider) : [];
|
|
114
367
|
}
|
|
115
368
|
function all() { return Object.fromEntries(CATALOG_PROVIDERS.map((p) => [p, list(p)])); }
|
|
369
|
+
function pins(provider) { return OWN_PROVIDERS.includes(provider) ? own.pins(provider) : {}; }
|
|
370
|
+
function allPins() { return Object.fromEntries(CATALOG_PROVIDERS.map((p) => [p, pins(p)])); }
|
|
371
|
+
|
|
372
|
+
function reconcile({ provider, rows = [], source = null } = {}) {
|
|
373
|
+
if (!OWN_PROVIDERS.includes(provider)) return null;
|
|
374
|
+
const at = new Date(now()).toISOString();
|
|
375
|
+
const settle = (ids, pins) => planHiddenPins({ ids, pins, rows, source, at });
|
|
376
|
+
// The look every build takes, from the copy in memory: most have nothing to settle and save nothing.
|
|
377
|
+
let plan = settle(own.list(provider), own.pins(provider));
|
|
378
|
+
if (plan.changed) {
|
|
379
|
+
// Nobody asked for this change, so it is settled again on the document that is on disk when the
|
|
380
|
+
// save happens: an id someone added since this build read the list is part of what gets saved.
|
|
381
|
+
// A list that cannot be saved still applies to this build; the next one settles it again, so
|
|
382
|
+
// a build never waits long for another process.
|
|
383
|
+
try {
|
|
384
|
+
own.change(provider, (doc) => {
|
|
385
|
+
plan = settle(doc.providers[provider], doc.pins?.[provider] || {});
|
|
386
|
+
settling = plan;
|
|
387
|
+
return plan.changed ? { ids: plan.ids, pins: plan.pins } : null;
|
|
388
|
+
}, { reason: 'pins', waitMs: HIDDEN_MIGRATION_WAIT_MS });
|
|
389
|
+
} catch (error) { log('hidden-models:pins-unsaved', error?.message || String(error)); }
|
|
390
|
+
finally { settling = null; }
|
|
391
|
+
for (const m of plan.moved) log('hidden-models:alias-moved', `${provider}: "${m.alias}" now names ${m.to}; ${m.from} stays hidden, "${m.alias}" is shown`);
|
|
392
|
+
}
|
|
393
|
+
return { ids: plan.ids, pins: plan.pins, moved: plan.moved, pinned: plan.pinned };
|
|
394
|
+
}
|
|
116
395
|
|
|
117
|
-
function update(provider, { hide = [], show = [] } = {}) {
|
|
396
|
+
function update(provider, { hide = [], show = [], pins: hidePins = null } = {}) {
|
|
118
397
|
if (!CATALOG_PROVIDERS.includes(provider)) throw hiddenError('PROVIDER_INVALID', `provider must be one of ${CATALOG_PROVIDERS.join(', ')}`);
|
|
119
398
|
if (provider === 'opencode' && !opencodeStore) throw hiddenError('PROVIDER_UNAVAILABLE', 'The OpenCode hidden-model store is not available.', 503);
|
|
120
|
-
const current = list(provider);
|
|
121
399
|
const key = (id) => String(id).trim().toLowerCase();
|
|
122
400
|
const hideIds = arr(hide).map((id) => String(id || '').trim()).filter(Boolean);
|
|
123
401
|
const showKeys = new Set(arr(show).map(key).filter(Boolean));
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
402
|
+
/** This edit on the list it finds: the list after it, and what it added and removed. */
|
|
403
|
+
const edit = (current) => {
|
|
404
|
+
const next = new Map(current.map((id) => [key(id), id]));
|
|
405
|
+
for (const id of hideIds) if (!next.has(key(id))) next.set(key(id), id);
|
|
406
|
+
for (const k of showKeys) next.delete(k);
|
|
407
|
+
const nextList = [...next.values()];
|
|
408
|
+
const before = new Set(current.map(key));
|
|
409
|
+
const after = new Set(nextList.map(key));
|
|
410
|
+
return { current, nextList, added: nextList.filter((id) => !before.has(key(id))), removed: current.filter((id) => !after.has(key(id))) };
|
|
411
|
+
};
|
|
412
|
+
if (provider === 'opencode') {
|
|
413
|
+
const { current, nextList, added, removed } = edit(list(provider));
|
|
414
|
+
if (!added.length && !removed.length) return { provider, hidden: current, changed: false, added, removed };
|
|
415
|
+
const result = opencodeStore.set(nextList);
|
|
416
|
+
return { provider, hidden: result.models, changed: true, added, removed, version: result.version };
|
|
417
|
+
}
|
|
418
|
+
const given = hidePins && typeof hidePins === 'object' ? new Map(Object.entries(hidePins).map(([id, pin]) => [key(id), pin])) : new Map();
|
|
419
|
+
const at = new Date(now()).toISOString();
|
|
420
|
+
// An edit is a difference, so it is made on the list that is on disk when it is saved: what another
|
|
421
|
+
// writer added or removed in the meantime stays as that writer left it.
|
|
422
|
+
let made = null;
|
|
423
|
+
const result = own.change(provider, (doc) => {
|
|
424
|
+
made = edit(doc.providers[provider]);
|
|
425
|
+
if (!made.added.length && !made.removed.length) return null;
|
|
426
|
+
// An alias that is hidden now is pinned to the model it names now; an id already hidden keeps its pin.
|
|
427
|
+
const nextPins = { ...(doc.pins?.[provider] || {}) };
|
|
428
|
+
for (const id of made.added) {
|
|
429
|
+
const pin = normalizePin(given.get(key(id)));
|
|
430
|
+
if (pin) nextPins[key(id)] = { ...pin, at };
|
|
431
|
+
}
|
|
432
|
+
return { ids: made.nextList, pins: nextPins };
|
|
433
|
+
});
|
|
434
|
+
const { added, removed } = made;
|
|
435
|
+
if (!result.changed) return { provider, hidden: made.current, changed: false, added, removed };
|
|
436
|
+
return { provider, hidden: result.providers[provider], pins: result.pins[provider], changed: true, added, removed, version: result.version };
|
|
135
437
|
}
|
|
136
438
|
|
|
137
439
|
function onChange(fn) { listeners.add(fn); return () => listeners.delete(fn); }
|
|
138
|
-
return { list, all, update, onChange, store: own, path: file };
|
|
440
|
+
return { list, all, pins, allPins, update, reconcile, onChange, store: own, path: file };
|
|
139
441
|
}
|
|
140
442
|
|
|
141
443
|
// ── resolving user-typed ids against the unfiltered catalog ─────────────────
|
|
@@ -206,9 +508,13 @@ export function resolveHiddenModelId({ id, provider = null, action = 'hide', row
|
|
|
206
508
|
}
|
|
207
509
|
|
|
208
510
|
/**
|
|
209
|
-
* GET / → { ok, providers: { 'claude-code': [], codex: [], opencode: [] } }
|
|
511
|
+
* GET / → { ok, providers: { 'claude-code': [], codex: [], opencode: [] }, pins }
|
|
210
512
|
* PATCH / { provider?, hide?: [], show?: [] } → the new lists plus what changed.
|
|
211
|
-
* A missing provider is inferred from the unfiltered catalog.
|
|
513
|
+
* A missing provider is inferred from the unfiltered catalog. Hiding an
|
|
514
|
+
* alias ("haiku") records the model it names at that moment (`pins`),
|
|
515
|
+
* when the catalog's list for the provider is a live one; else the
|
|
516
|
+
* next live build records it. 503 HIDDEN_BUSY / HIDDEN_LOCK_UNAVAILABLE
|
|
517
|
+
* and 409 HIDDEN_UNREADABLE: nothing was changed.
|
|
212
518
|
* Owner only (guests get 403). Broadcasts assistant:hidden-models-changed, and
|
|
213
519
|
* opencode:hidden-models-changed when the OpenCode list changed.
|
|
214
520
|
*/
|
|
@@ -220,7 +526,7 @@ export function createAssistantHiddenModelsRouter({ hiddenModels, catalog = null
|
|
|
220
526
|
next();
|
|
221
527
|
});
|
|
222
528
|
router.get('/', (req, res) => {
|
|
223
|
-
try { res.json({ ok: true, providers: hiddenModels.all() }); }
|
|
529
|
+
try { res.json({ ok: true, providers: hiddenModels.all(), pins: hiddenModels.allPins?.() || {} }); }
|
|
224
530
|
catch (error) { res.status(500).json({ ok: false, error: error.message }); }
|
|
225
531
|
});
|
|
226
532
|
router.patch('/', async (req, res) => {
|
|
@@ -233,16 +539,36 @@ export function createAssistantHiddenModelsRouter({ hiddenModels, catalog = null
|
|
|
233
539
|
if (hide.length + show.length > MAX_HIDDEN_PER_PROVIDER) throw hiddenError('TOO_MANY', `At most ${MAX_HIDDEN_PER_PROVIDER} ids per request.`);
|
|
234
540
|
const provider = body.provider ? String(body.provider).trim().toLowerCase() : null;
|
|
235
541
|
if (provider && !CATALOG_PROVIDERS.includes(provider)) throw hiddenError('PROVIDER_INVALID', `provider must be one of ${CATALOG_PROVIDERS.join(', ')}`);
|
|
542
|
+
// The rows and how each list was obtained, from one catalog build: what an alias names is
|
|
543
|
+
// only taken from a list the provider just gave (`live`), never from a remembered one.
|
|
236
544
|
let rowsByProvider = {};
|
|
237
|
-
|
|
545
|
+
let sources = {};
|
|
546
|
+
try {
|
|
547
|
+
if (typeof catalog?.manageSnapshot === 'function') {
|
|
548
|
+
const snapshot = (await catalog.manageSnapshot()) || {};
|
|
549
|
+
rowsByProvider = snapshot.rows || {};
|
|
550
|
+
sources = snapshot.sources || {};
|
|
551
|
+
} else rowsByProvider = (await catalog?.manageRows?.()) || {};
|
|
552
|
+
} catch {}
|
|
238
553
|
const hiddenByProvider = hiddenModels.all();
|
|
239
|
-
const plan = new Map(); // provider → { hide: [], show: [] }
|
|
554
|
+
const plan = new Map(); // provider → { hide: [], show: [], pins: {} }
|
|
240
555
|
const applied = [];
|
|
241
556
|
const add = (action, raw) => {
|
|
242
557
|
const hit = resolveHiddenModelId({ id: raw, provider, action, rowsByProvider, hiddenByProvider });
|
|
243
|
-
if (!plan.has(hit.provider)) plan.set(hit.provider, { hide: [], show: [] });
|
|
558
|
+
if (!plan.has(hit.provider)) plan.set(hit.provider, { hide: [], show: [], pins: {} });
|
|
244
559
|
plan.get(hit.provider)[action].push(hit.id);
|
|
245
|
-
|
|
560
|
+
const entry = { action, provider: hit.provider, id: hit.id, requested: String(raw) };
|
|
561
|
+
if (action === 'hide') {
|
|
562
|
+
// The row being hidden: an alias is hidden as the model it names right now. While the list is
|
|
563
|
+
// not a verified one the entry is saved without a pin, and the next live build pins it to what
|
|
564
|
+
// the alias names then (reconcile): a pin read off a stale list would be undone by that build.
|
|
565
|
+
const row = sources[hit.provider] === 'live'
|
|
566
|
+
? arr(rowsByProvider[hit.provider]).find((r) => String(r?.id || '').toLowerCase() === String(hit.id).toLowerCase())
|
|
567
|
+
: null;
|
|
568
|
+
const pin = row ? pinForRow({ ...row, provider: hit.provider }) : null;
|
|
569
|
+
if (pin) { plan.get(hit.provider).pins[String(hit.id).toLowerCase()] = pin; entry.pinned = pinnedModelId(pin); }
|
|
570
|
+
}
|
|
571
|
+
applied.push(entry);
|
|
246
572
|
};
|
|
247
573
|
for (const id of hide) add('hide', id);
|
|
248
574
|
for (const id of show) add('show', id);
|
|
@@ -250,8 +576,9 @@ export function createAssistantHiddenModelsRouter({ hiddenModels, catalog = null
|
|
|
250
576
|
for (const [p, change] of plan) results.push(hiddenModels.update(p, change));
|
|
251
577
|
const changed = results.filter((r) => r.changed);
|
|
252
578
|
const providers = hiddenModels.all();
|
|
579
|
+
const pins = hiddenModels.allPins?.() || {};
|
|
253
580
|
if (changed.length) {
|
|
254
|
-
broadcastSync({ type: ASSISTANT_HIDDEN_SYNC_TYPE, providers, changed: changed.map((r) => r.provider) });
|
|
581
|
+
broadcastSync({ type: ASSISTANT_HIDDEN_SYNC_TYPE, providers, pins, changed: changed.map((r) => r.provider) });
|
|
255
582
|
const oc = changed.find((r) => r.provider === 'opencode');
|
|
256
583
|
if (oc) {
|
|
257
584
|
let version = oc.version;
|
|
@@ -260,7 +587,7 @@ export function createAssistantHiddenModelsRouter({ hiddenModels, catalog = null
|
|
|
260
587
|
}
|
|
261
588
|
}
|
|
262
589
|
res.json({
|
|
263
|
-
ok: true, providers, changed: changed.length > 0, applied,
|
|
590
|
+
ok: true, providers, pins, changed: changed.length > 0, applied,
|
|
264
591
|
added: Object.fromEntries(changed.map((r) => [r.provider, r.added])),
|
|
265
592
|
removed: Object.fromEntries(changed.map((r) => [r.provider, r.removed])),
|
|
266
593
|
});
|