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
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
// ── The code editor's preferences (DATA_HOME/data/editor-config.json) ──
|
|
2
|
+
// Word wrap, font size, indentation and the AI autocomplete settings of the
|
|
3
|
+
// File Explorer's editor. The defaults and the normaliser are the client's own
|
|
4
|
+
// (public/shared/editor/editor-prefs.js): one definition, so the file on disk
|
|
5
|
+
// and the page can never disagree about what a valid configuration is. Unknown
|
|
6
|
+
// keys are dropped and out-of-range values clamped on the way in and on the
|
|
7
|
+
// way out.
|
|
8
|
+
//
|
|
9
|
+
// The endpoint API key of AI autocomplete is not kept here: it is
|
|
10
|
+
// SYNABUN_EDITOR_AI_KEY in the environment, and only whether one is set
|
|
11
|
+
// (`hasEndpointKey`) ever reaches the page.
|
|
12
|
+
|
|
13
|
+
import { Router } from 'express';
|
|
14
|
+
import { mkdirSync, readFileSync, statSync } from 'node:fs';
|
|
15
|
+
import { dirname } from 'node:path';
|
|
16
|
+
import { replaceFileAtomic } from './confined-fs.js';
|
|
17
|
+
import { defaultEditorConfig, mergeEditorConfig, normalizeEditorConfig } from '../public/shared/editor/editor-prefs.js';
|
|
18
|
+
|
|
19
|
+
export { defaultEditorConfig, normalizeEditorConfig };
|
|
20
|
+
export const EDITOR_CONFIG_SYNC_TYPE = 'editor:config-changed';
|
|
21
|
+
/** What these routes add to the editor (GET /api/editor/capabilities). */
|
|
22
|
+
export const EDITOR_CONFIG_CAPABILITIES = Object.freeze(['config']);
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* The stored configuration. Read again when the file's modification time changes; written atomically.
|
|
26
|
+
* @param {{ path: string, log?: (tag: string, message: string) => void }} o
|
|
27
|
+
*/
|
|
28
|
+
export function createEditorConfigStore({ path, log = () => {} } = {}) {
|
|
29
|
+
if (!path) throw new Error('createEditorConfigStore requires path');
|
|
30
|
+
let cached = null; // { mtimeMs, config }
|
|
31
|
+
|
|
32
|
+
const mtime = () => { try { return statSync(path).mtimeMs; } catch { return 0; } };
|
|
33
|
+
|
|
34
|
+
/** The configuration in effect: the file's, normalised, or the defaults when there is no (readable) file. */
|
|
35
|
+
function read() {
|
|
36
|
+
const m = mtime();
|
|
37
|
+
if (cached && cached.mtimeMs === m) return cached.config;
|
|
38
|
+
let config = defaultEditorConfig();
|
|
39
|
+
if (m) {
|
|
40
|
+
try { config = normalizeEditorConfig(JSON.parse(readFileSync(path, 'utf8'))); }
|
|
41
|
+
catch (error) { log('editor-config:unreadable', error?.message || String(error)); }
|
|
42
|
+
}
|
|
43
|
+
cached = { mtimeMs: m, config };
|
|
44
|
+
return config;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Apply a partial change and save.
|
|
49
|
+
* @returns {{ config: object, changed: boolean }}
|
|
50
|
+
*/
|
|
51
|
+
function update(partial) {
|
|
52
|
+
const current = read();
|
|
53
|
+
const config = mergeEditorConfig(current, partial);
|
|
54
|
+
const changed = JSON.stringify(config) !== JSON.stringify(current);
|
|
55
|
+
if (changed) {
|
|
56
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
57
|
+
replaceFileAtomic(path, `${JSON.stringify(config, null, 2)}\n`);
|
|
58
|
+
cached = { mtimeMs: mtime(), config };
|
|
59
|
+
}
|
|
60
|
+
return { config, changed };
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return { read, update, path };
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export const ENDPOINT_KEY_NAME = 'SYNABUN_EDITOR_AI_KEY';
|
|
67
|
+
// A bearer token as providers issue them. Nothing that could start a new line or a comment in a .env file.
|
|
68
|
+
const ENDPOINT_KEY_FORMAT = /^[A-Za-z0-9._~+/=:@-]{1,500}$/;
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* The API key of AI autocomplete's endpoint: SYNABUN_EDITOR_AI_KEY in the data home's .env, beside the other
|
|
72
|
+
* secrets, and in the environment of the running server. Written the way the TypeSafe key is (read the file,
|
|
73
|
+
* change one name, write the file).
|
|
74
|
+
* @param {object} deps
|
|
75
|
+
* @param {string} deps.envPath the .env file
|
|
76
|
+
* @param {(path: string) => object} deps.parseEnvFile server.js's reader
|
|
77
|
+
* @param {(path: string, vars: object) => void} deps.writeEnvFile server.js's writer
|
|
78
|
+
* @param {object} [deps.env] the running environment (process.env)
|
|
79
|
+
*/
|
|
80
|
+
export function createEndpointKeyStore({ envPath, parseEnvFile, writeEnvFile, env = process.env } = {}) {
|
|
81
|
+
if (!envPath || typeof parseEnvFile !== 'function' || typeof writeEnvFile !== 'function') {
|
|
82
|
+
throw new Error('createEndpointKeyStore requires envPath, parseEnvFile and writeEnvFile');
|
|
83
|
+
}
|
|
84
|
+
const get = () => String(env[ENDPOINT_KEY_NAME] || '').trim();
|
|
85
|
+
return {
|
|
86
|
+
get,
|
|
87
|
+
has: () => !!get(),
|
|
88
|
+
/** Store a key, or remove it with ''. Throws on a value that is not a key. */
|
|
89
|
+
set(value) {
|
|
90
|
+
if (typeof value !== 'string') throw new Error('The endpoint key must be text');
|
|
91
|
+
const key = value.trim();
|
|
92
|
+
if (key && !ENDPOINT_KEY_FORMAT.test(key)) throw new Error('The endpoint key has an unexpected format');
|
|
93
|
+
const vars = parseEnvFile(envPath);
|
|
94
|
+
if (key) { vars[ENDPOINT_KEY_NAME] = key; env[ENDPOINT_KEY_NAME] = key; }
|
|
95
|
+
else { delete vars[ENDPOINT_KEY_NAME]; delete env[ENDPOINT_KEY_NAME]; }
|
|
96
|
+
writeEnvFile(envPath, vars);
|
|
97
|
+
return !!key;
|
|
98
|
+
},
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* GET / → { ok, config, hasEndpointKey }
|
|
104
|
+
* PUT / (a partial configuration) → the same, after the change; other windows are told when it changed anything.
|
|
105
|
+
* `endpointKey` in the body stores the endpoint's API key ('' removes it); it is never part of an answer.
|
|
106
|
+
* Mounted at /api/editor/config. The owner's only: `guard` refuses everybody else on the router itself.
|
|
107
|
+
* @param {object} deps
|
|
108
|
+
* @param {{ read: () => object, update: (partial: object) => { config: object, changed: boolean } }} deps.store
|
|
109
|
+
* @param {(message: object) => void} [deps.broadcastSync]
|
|
110
|
+
* @param {() => boolean} [deps.hasEndpointKey]
|
|
111
|
+
* @param {(key: string) => void} [deps.setEndpointKey] stores or removes the key; without it a key cannot be saved
|
|
112
|
+
* @param {(req: object, res: object, next: () => void) => void} [deps.guard] who may use these routes
|
|
113
|
+
* (lib/http-guards.js ownerRouteGuard); without one, anybody who reaches them
|
|
114
|
+
*/
|
|
115
|
+
export function createEditorConfigRouter({ store, broadcastSync = () => {}, hasEndpointKey = () => false, setEndpointKey = null, guard = null } = {}) {
|
|
116
|
+
if (!store) throw new Error('createEditorConfigRouter requires a store');
|
|
117
|
+
const router = Router();
|
|
118
|
+
if (typeof guard === 'function') router.use(guard);
|
|
119
|
+
const view = (config) => ({ ok: true, config, hasEndpointKey: !!hasEndpointKey() });
|
|
120
|
+
|
|
121
|
+
router.get('/', (req, res) => {
|
|
122
|
+
try { res.json(view(store.read())); }
|
|
123
|
+
catch (error) { res.status(500).json({ ok: false, error: error.message }); }
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
router.put('/', (req, res) => {
|
|
127
|
+
if (!req.body || typeof req.body !== 'object' || Array.isArray(req.body)) {
|
|
128
|
+
return res.status(400).json({ ok: false, error: 'The body must be an object holding the preferences to change' });
|
|
129
|
+
}
|
|
130
|
+
const { endpointKey, ...partial } = req.body;
|
|
131
|
+
try {
|
|
132
|
+
let keyChanged = false;
|
|
133
|
+
if (endpointKey !== undefined) {
|
|
134
|
+
if (typeof setEndpointKey !== 'function') return res.status(400).json({ ok: false, error: 'Key storage is not wired in this server' });
|
|
135
|
+
const had = !!hasEndpointKey();
|
|
136
|
+
try { setEndpointKey(endpointKey); }
|
|
137
|
+
catch (error) { return res.status(400).json({ ok: false, error: error.message }); }
|
|
138
|
+
keyChanged = had !== !!hasEndpointKey();
|
|
139
|
+
}
|
|
140
|
+
const { config, changed } = store.update(partial);
|
|
141
|
+
if (changed || keyChanged) broadcastSync({ type: EDITOR_CONFIG_SYNC_TYPE, config, hasEndpointKey: !!hasEndpointKey() });
|
|
142
|
+
res.json(view(config));
|
|
143
|
+
} catch (error) {
|
|
144
|
+
res.status(500).json({ ok: false, error: error.message });
|
|
145
|
+
}
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
return router;
|
|
149
|
+
}
|
|
@@ -0,0 +1,268 @@
|
|
|
1
|
+
// ── The code editor's file routes ──
|
|
2
|
+
// Reading, writing, creating and removing the files the File Explorer's editor
|
|
3
|
+
// works on, plus what the editor asks the server before it uses anything newer
|
|
4
|
+
// than read and write (GET /api/editor/capabilities).
|
|
5
|
+
//
|
|
6
|
+
// A path is allowed when it is inside one of the roots the server registers:
|
|
7
|
+
// the data home, the registered projects, the plan folders, and one single
|
|
8
|
+
// file, the user's OpenCode configuration. "Inside" is judged as written first
|
|
9
|
+
// (403 "Path outside registered project roots"), then as the filesystem
|
|
10
|
+
// resolves it: every read and write goes through lib/confined-fs.js, so a link
|
|
11
|
+
// that leads out of its root is refused instead of followed.
|
|
12
|
+
//
|
|
13
|
+
// A read reports the file's version ({ mtimeMs, size }); a write that sends it
|
|
14
|
+
// back as `expect` is refused with 409 when the file changed in between. A
|
|
15
|
+
// write of a file that is expected not to be there (`expectAbsent`, with
|
|
16
|
+
// `create`) is refused the same way when it is: it is created exclusively.
|
|
17
|
+
//
|
|
18
|
+
// Who may ask is each route's own business: the `guard` it is handed (server.js:
|
|
19
|
+
// lib/http-guards.js ownerRouteGuard) refuses invite guests, a Host that is not
|
|
20
|
+
// this server's and a write from another site's page, on the route itself.
|
|
21
|
+
// Dependencies are injected: the router is tested without server.js.
|
|
22
|
+
|
|
23
|
+
import { Router } from 'express';
|
|
24
|
+
import { lstatSync, mkdirSync, readdirSync, rmdirSync, statSync } from 'node:fs';
|
|
25
|
+
import { basename, dirname, join, resolve, sep } from 'node:path';
|
|
26
|
+
import { PathConfineError, realPathOf } from './path-confine.js';
|
|
27
|
+
import { MAX_FILE_BYTES, canonicalInside, makeConfinedDir, readConfinedFile, removeConfinedEntry, writeConfinedFile } from './confined-fs.js';
|
|
28
|
+
|
|
29
|
+
/** What these routes add to the editor: the version check on save, and POST /api/file-stat. */
|
|
30
|
+
export const EDITOR_FILE_CAPABILITIES = Object.freeze(['file-version', 'file-stat']);
|
|
31
|
+
export const MAX_STAT_PATHS = 50;
|
|
32
|
+
/**
|
|
33
|
+
* The namespace of the UI blob store (lib/ui-blob-store.js, /api/ui-blobs) that holds the editor's copies of
|
|
34
|
+
* unsaved buffers. server.js allows it and keeps it for the owner; it announces `backups` when it does.
|
|
35
|
+
*/
|
|
36
|
+
export const EDITOR_BACKUP_NAMESPACE = 'editor-backups';
|
|
37
|
+
export const EDITOR_BACKUP_CAPABILITIES = Object.freeze(['backups']);
|
|
38
|
+
|
|
39
|
+
const OUTSIDE_ROOTS = 'Path outside registered project roots';
|
|
40
|
+
const BINARY_PROBE_BYTES = 8192;
|
|
41
|
+
const UTF8_BOM = [0xEF, 0xBB, 0xBF];
|
|
42
|
+
|
|
43
|
+
const outsideRoots = () => new PathConfineError(403, OUTSIDE_ROOTS);
|
|
44
|
+
const slashes = (path) => path.replace(/\\/g, '/');
|
|
45
|
+
const versionOf = (st) => ({ mtimeMs: st.mtimeMs, size: st.size });
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* The roots a path is inside, as written.
|
|
49
|
+
* @param {string} path any path a request sent
|
|
50
|
+
* @param {{ dirs?: string[], files?: string[] }} roots folders, and single files allowed by their exact path
|
|
51
|
+
* @returns {{ target: string, matches: { base: string, path: string, isBase: boolean }[] }} `target` is the path
|
|
52
|
+
* resolved; each match is the folder to confine to and the path to use inside it. A single file is confined to
|
|
53
|
+
* the folder it really is in: it is allowed by name, wherever a link takes it.
|
|
54
|
+
*/
|
|
55
|
+
export function matchFileRoots(path, roots = {}) {
|
|
56
|
+
const target = resolve(String(path));
|
|
57
|
+
const matches = [];
|
|
58
|
+
for (const dir of roots.dirs || []) {
|
|
59
|
+
const base = resolve(dir);
|
|
60
|
+
if (target === base) matches.push({ base, path: target, isBase: true });
|
|
61
|
+
else if (target.startsWith(base.endsWith(sep) ? base : base + sep)) matches.push({ base, path: target, isBase: false });
|
|
62
|
+
}
|
|
63
|
+
for (const file of roots.files || []) {
|
|
64
|
+
if (target !== resolve(file)) continue;
|
|
65
|
+
try {
|
|
66
|
+
const real = realPathOf(target);
|
|
67
|
+
matches.push({ base: dirname(real), path: real, isBase: false });
|
|
68
|
+
} catch { /* a link that cannot be resolved: not a file to open */ }
|
|
69
|
+
}
|
|
70
|
+
return { target, matches };
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Run `use(match)` on the first root that really holds the path. A root the path leaves through a link is passed
|
|
75
|
+
* over; when every root is, the path is refused (403), as a path outside every root is.
|
|
76
|
+
* @param {string} path any path a request sent
|
|
77
|
+
* @param {{ dirs?: string[], files?: string[] }} roots
|
|
78
|
+
* @param {(match: { base: string, path: string, isBase: boolean, target: string }) => any} use
|
|
79
|
+
*/
|
|
80
|
+
export function useFileRoot(path, roots, use) {
|
|
81
|
+
const { target, matches } = matchFileRoots(path, roots);
|
|
82
|
+
if (!matches.length) throw outsideRoots();
|
|
83
|
+
let refused = null;
|
|
84
|
+
for (const match of matches) {
|
|
85
|
+
try { return use({ ...match, target }); }
|
|
86
|
+
catch (error) {
|
|
87
|
+
if (!(error instanceof PathConfineError) || error.status !== 403) throw error;
|
|
88
|
+
refused = error;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
throw refused;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* @param {object} deps
|
|
96
|
+
* @param {() => { dirs: string[], files: string[] }} deps.fileRoots the roots validateProjectPath() allows
|
|
97
|
+
* @param {() => string[]} [deps.capabilities] what other editor routes of this server add (GET /api/editor/capabilities)
|
|
98
|
+
* @param {number} [deps.maxBytes] the largest file that is read
|
|
99
|
+
* @param {(req: object, res: object, next: () => void) => void} [deps.guard] who may use these routes; without
|
|
100
|
+
* one, anybody who reaches them
|
|
101
|
+
*/
|
|
102
|
+
export function createEditorFilesRouter({ fileRoots, capabilities = () => [], maxBytes = MAX_FILE_BYTES, guard = null } = {}) {
|
|
103
|
+
if (typeof fileRoots !== 'function') throw new Error('createEditorFilesRouter requires fileRoots');
|
|
104
|
+
const router = Router();
|
|
105
|
+
// On each route, not on the router: it is mounted at the root, where a check of its own would run for the
|
|
106
|
+
// paths of every route registered after it.
|
|
107
|
+
const owner = typeof guard === 'function' ? guard : (req, res, next) => next();
|
|
108
|
+
|
|
109
|
+
const fail = (res, error) => {
|
|
110
|
+
if (error instanceof PathConfineError) return res.status(error.status).json({ error: error.message });
|
|
111
|
+
return res.status(500).json({ error: error?.message || String(error) });
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
const inRoots = (path, use) => useFileRoot(path, fileRoots(), use);
|
|
115
|
+
|
|
116
|
+
// The canonical path of a match, and what is there (null when nothing is).
|
|
117
|
+
function locate(match) {
|
|
118
|
+
const real = canonicalInside(match.base, match.path, { allowBase: true });
|
|
119
|
+
let st = null;
|
|
120
|
+
try { st = statSync(real); } catch { st = null; }
|
|
121
|
+
return { real, st };
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// GET /api/file-content — read one file for the editor
|
|
125
|
+
router.get('/api/file-content', owner, (req, res) => {
|
|
126
|
+
try {
|
|
127
|
+
const filePath = req.query.path;
|
|
128
|
+
if (!filePath || typeof filePath !== 'string') return res.status(400).json({ error: 'Missing path parameter' });
|
|
129
|
+
const found = inRoots(filePath, (match) => {
|
|
130
|
+
let { real, st } = locate(match);
|
|
131
|
+
if (!st || !st.isFile()) return { status: 400, body: { error: 'Not a valid file' } };
|
|
132
|
+
if (st.size > maxBytes) {
|
|
133
|
+
return { status: 413, body: { error: `File too large (the editor opens up to ${Math.round(maxBytes / (1024 * 1024))} MB)`, maxBytes } };
|
|
134
|
+
}
|
|
135
|
+
let buf = readConfinedFile(match.base, real, { maxBytes });
|
|
136
|
+
// The version must be the one of the bytes that were read: a write in between is read again.
|
|
137
|
+
let after = statSync(real);
|
|
138
|
+
if (after.mtimeMs !== st.mtimeMs || after.size !== st.size) {
|
|
139
|
+
buf = readConfinedFile(match.base, real, { maxBytes });
|
|
140
|
+
after = statSync(real);
|
|
141
|
+
}
|
|
142
|
+
st = after;
|
|
143
|
+
const probe = Math.min(BINARY_PROBE_BYTES, buf.length);
|
|
144
|
+
for (let i = 0; i < probe; i++) {
|
|
145
|
+
if (buf[i] === 0x00) return { status: 415, body: { error: 'Binary file', reason: 'binary' } };
|
|
146
|
+
}
|
|
147
|
+
const bom = buf.length >= 3 && UTF8_BOM.every((byte, i) => buf[i] === byte);
|
|
148
|
+
let content;
|
|
149
|
+
try { content = new TextDecoder('utf-8', { fatal: true, ignoreBOM: true }).decode(bom ? buf.subarray(3) : buf); }
|
|
150
|
+
catch { return { status: 415, body: { error: 'File is not valid UTF-8 text', reason: 'encoding' } }; }
|
|
151
|
+
return {
|
|
152
|
+
status: 200,
|
|
153
|
+
body: { ok: true, content, path: slashes(match.target), name: basename(match.target), size: st.size, version: versionOf(st), bom },
|
|
154
|
+
};
|
|
155
|
+
});
|
|
156
|
+
res.status(found.status).json(found.body);
|
|
157
|
+
} catch (error) { fail(res, error); }
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
// POST /api/file-content — write a file (an existing one, or a new one with `create: true`)
|
|
161
|
+
// expect: { mtimeMs, size } the version that was read: 409 { code: 'changed', current } when the file differs
|
|
162
|
+
// expectAbsent: true with `create`: the file is expected not to be there (the editor saving a tab whose
|
|
163
|
+
// file was deleted): 409 { code: 'changed', current } when it is
|
|
164
|
+
// force: true write whatever is there ("Overwrite")
|
|
165
|
+
router.post('/api/file-content', owner, (req, res) => {
|
|
166
|
+
try {
|
|
167
|
+
const { path: filePath, content, create, expect, expectAbsent, force } = req.body || {};
|
|
168
|
+
if (!filePath || typeof filePath !== 'string' || typeof content !== 'string') {
|
|
169
|
+
return res.status(400).json({ error: 'Missing path or content' });
|
|
170
|
+
}
|
|
171
|
+
const checked = expect !== undefined && expect !== null;
|
|
172
|
+
if (checked && !(Number.isFinite(expect?.mtimeMs) && Number.isFinite(expect?.size))) {
|
|
173
|
+
return res.status(400).json({ error: 'expect must be the version a read returned: { mtimeMs, size }' });
|
|
174
|
+
}
|
|
175
|
+
const absent = expectAbsent === true && !force;
|
|
176
|
+
const changed = (st) => ({ status: 409, body: { error: 'The file changed on disk', code: 'changed', current: versionOf(st) } });
|
|
177
|
+
const done = inRoots(filePath, (match) => {
|
|
178
|
+
const { real, st } = locate(match);
|
|
179
|
+
if (st && !st.isFile()) return { status: 400, body: { error: create ? 'Not a valid file' : 'File does not exist' } };
|
|
180
|
+
if (!st && !create) return { status: 400, body: { error: 'File does not exist' } };
|
|
181
|
+
if (st && absent) return changed(st);
|
|
182
|
+
if (st && checked && !force && (st.mtimeMs !== expect.mtimeMs || st.size !== expect.size)) return changed(st);
|
|
183
|
+
// In place: an existing file keeps its mode and its inode, a link to it stays a link. A file that is
|
|
184
|
+
// expected to be absent is created exclusively: one that appeared since the look above is not written over.
|
|
185
|
+
try {
|
|
186
|
+
writeConfinedFile(match.base, real, content, { exclusive: absent });
|
|
187
|
+
} catch (error) {
|
|
188
|
+
if (!(absent && error instanceof PathConfineError && error.status === 409)) throw error;
|
|
189
|
+
let now = null;
|
|
190
|
+
try { now = statSync(real); } catch { now = null; }
|
|
191
|
+
if (now) return changed(now);
|
|
192
|
+
throw error;
|
|
193
|
+
}
|
|
194
|
+
return { status: 200, body: { ok: true, version: versionOf(statSync(real)) } };
|
|
195
|
+
});
|
|
196
|
+
res.status(done.status).json(done.body);
|
|
197
|
+
} catch (error) { fail(res, error); }
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
// POST /api/file-mkdir — create a directory
|
|
201
|
+
router.post('/api/file-mkdir', owner, (req, res) => {
|
|
202
|
+
try {
|
|
203
|
+
const { path: dirPath } = req.body || {};
|
|
204
|
+
if (!dirPath || typeof dirPath !== 'string') return res.status(400).json({ error: 'Missing path' });
|
|
205
|
+
inRoots(dirPath, (match) => {
|
|
206
|
+
if (match.isBase) mkdirSync(match.path, { recursive: true });
|
|
207
|
+
else makeConfinedDir(match.base, match.path);
|
|
208
|
+
});
|
|
209
|
+
res.json({ ok: true });
|
|
210
|
+
} catch (error) { fail(res, error); }
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
// DELETE /api/file-content — delete a file or an empty directory
|
|
214
|
+
router.delete('/api/file-content', owner, (req, res) => {
|
|
215
|
+
try {
|
|
216
|
+
const filePath = req.query.path;
|
|
217
|
+
if (!filePath || typeof filePath !== 'string') return res.status(400).json({ error: 'Missing path parameter' });
|
|
218
|
+
const done = inRoots(filePath, (match) => {
|
|
219
|
+
if (match.isBase) return { status: 403, body: { error: 'A registered folder is not deleted from here' } };
|
|
220
|
+
// The folder that holds the entry must really be in the root; the entry itself is removed as it is
|
|
221
|
+
// (a link is unlinked, never followed).
|
|
222
|
+
const parent = canonicalInside(match.base, dirname(match.path), { allowBase: true });
|
|
223
|
+
const entry = join(parent, basename(match.path));
|
|
224
|
+
let st;
|
|
225
|
+
try { st = lstatSync(entry); } catch { return { status: 404, body: { error: 'Path does not exist' } }; }
|
|
226
|
+
if (st.isDirectory()) {
|
|
227
|
+
if (readdirSync(entry).length > 0) return { status: 400, body: { error: 'Directory is not empty' } };
|
|
228
|
+
rmdirSync(entry);
|
|
229
|
+
} else {
|
|
230
|
+
removeConfinedEntry(match.base, entry);
|
|
231
|
+
}
|
|
232
|
+
return { status: 200, body: { ok: true } };
|
|
233
|
+
});
|
|
234
|
+
res.status(done.status).json(done.body);
|
|
235
|
+
} catch (error) { fail(res, error); }
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
// POST /api/file-stat — the versions of up to 50 files, to see which changed on disk
|
|
239
|
+
router.post('/api/file-stat', owner, (req, res) => {
|
|
240
|
+
try {
|
|
241
|
+
const paths = req.body?.paths;
|
|
242
|
+
if (!Array.isArray(paths) || paths.length > MAX_STAT_PATHS || paths.some((p) => typeof p !== 'string' || !p)) {
|
|
243
|
+
return res.status(400).json({ error: `paths must be an array of at most ${MAX_STAT_PATHS} file paths` });
|
|
244
|
+
}
|
|
245
|
+
const files = {};
|
|
246
|
+
for (const path of paths) {
|
|
247
|
+
let entry = { exists: false };
|
|
248
|
+
try {
|
|
249
|
+
entry = inRoots(path, (match) => {
|
|
250
|
+
const { st } = locate(match);
|
|
251
|
+
return st && st.isFile() ? { exists: true, version: versionOf(st) } : { exists: false };
|
|
252
|
+
});
|
|
253
|
+
} catch { /* outside the roots, or out of one through a link: not a file the editor has */ }
|
|
254
|
+
files[path] = entry;
|
|
255
|
+
}
|
|
256
|
+
res.json({ ok: true, files });
|
|
257
|
+
} catch (error) { fail(res, error); }
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
// GET /api/editor/capabilities — what this server offers the editor beyond reading and writing
|
|
261
|
+
router.get('/api/editor/capabilities', owner, (req, res) => {
|
|
262
|
+
let extra = [];
|
|
263
|
+
try { extra = (capabilities() || []).filter((name) => typeof name === 'string'); } catch { extra = []; }
|
|
264
|
+
res.json({ ok: true, capabilities: [...new Set([...EDITOR_FILE_CAPABILITIES, ...extra])] });
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
return router;
|
|
268
|
+
}
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
// ── The code editor's view of git: the committed version of a file ──
|
|
2
|
+
// GET /api/file-head?path= answers with the text of a file as the last commit
|
|
3
|
+
// has it (`git show HEAD:<path>`). The editor marks the lines that differ from
|
|
4
|
+
// it and shows the two against each other.
|
|
5
|
+
//
|
|
6
|
+
// The path is checked like every other file path of the editor (inside a
|
|
7
|
+
// registered root as written, and as the filesystem resolves it). git is run
|
|
8
|
+
// with an argument array, never through a shell: a file name is a name,
|
|
9
|
+
// whatever characters it holds. Only commands that read are run here:
|
|
10
|
+
// `rev-parse --show-toplevel` to find the repository, `show` for the text.
|
|
11
|
+
// Nothing in the repository, its index or its working tree is changed, and
|
|
12
|
+
// nothing the repository's own configuration names is started: every call of
|
|
13
|
+
// runGit() is a read as lib/git-read.js runs one (no file system monitor
|
|
14
|
+
// program, no hook, no external diff or textconv, no fetch).
|
|
15
|
+
//
|
|
16
|
+
// A file git does not know, a folder that is not a repository and a machine
|
|
17
|
+
// without git all answer `tracked: false`: for the editor that is "no markers",
|
|
18
|
+
// not an error. Dependencies are injected: the router is tested without server.js.
|
|
19
|
+
|
|
20
|
+
import { Router } from 'express';
|
|
21
|
+
import { execFile } from 'node:child_process';
|
|
22
|
+
import { statSync } from 'node:fs';
|
|
23
|
+
import { dirname, isAbsolute, relative, sep } from 'node:path';
|
|
24
|
+
import { PathConfineError } from './path-confine.js';
|
|
25
|
+
import { MAX_FILE_BYTES, canonicalInside } from './confined-fs.js';
|
|
26
|
+
import { useFileRoot } from './editor-files-api.js';
|
|
27
|
+
import { gitRead } from './git-read.js';
|
|
28
|
+
|
|
29
|
+
/** What this route adds to the editor (GET /api/editor/capabilities). */
|
|
30
|
+
export const EDITOR_GIT_CAPABILITIES = Object.freeze(['git-head']);
|
|
31
|
+
export const GIT_TIMEOUT_MS = 5000;
|
|
32
|
+
|
|
33
|
+
const UTF8_BOM = [0xEF, 0xBB, 0xBF];
|
|
34
|
+
const BINARY_PROBE_BYTES = 8192;
|
|
35
|
+
// Variables that would point git at another repository than the one the file is in.
|
|
36
|
+
const REDIRECTING = ['GIT_DIR', 'GIT_WORK_TREE', 'GIT_INDEX_FILE', 'GIT_NAMESPACE', 'GIT_COMMON_DIR', 'GIT_OBJECT_DIRECTORY', 'GIT_ALTERNATE_OBJECT_DIRECTORIES', 'GIT_CEILING_DIRECTORIES'];
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Run git for a read and collect what it prints. Never throws. The arguments are the command's own: what keeps a
|
|
40
|
+
* read from starting a program of the repository's is added here (lib/git-read.js).
|
|
41
|
+
* @param {string[]} args the command first
|
|
42
|
+
* @param {{ cwd: string, maxBuffer: number, timeoutMs?: number, binary?: string, env?: object }} o
|
|
43
|
+
* @returns {Promise<{ ok: boolean, stdout: Buffer, tooLarge: boolean }>} `tooLarge`: it printed more than `maxBuffer` bytes
|
|
44
|
+
*/
|
|
45
|
+
export function runGit(args, { cwd, maxBuffer, timeoutMs = GIT_TIMEOUT_MS, binary = 'git', env = process.env } = {}) {
|
|
46
|
+
const clean = { ...env };
|
|
47
|
+
for (const name of REDIRECTING) delete clean[name];
|
|
48
|
+
const failed = { ok: false, stdout: Buffer.alloc(0), tooLarge: false };
|
|
49
|
+
return gitRead(cwd, args, { binary, env: clean }).then((read) => new Promise((done) => {
|
|
50
|
+
try {
|
|
51
|
+
execFile(binary, read.args, { cwd, maxBuffer, timeout: timeoutMs, encoding: 'buffer', windowsHide: true, env: read.env, shell: false }, (error, stdout) => {
|
|
52
|
+
const out = Buffer.isBuffer(stdout) ? stdout : Buffer.alloc(0);
|
|
53
|
+
if (!error) done({ ok: true, stdout: out, tooLarge: false });
|
|
54
|
+
else done({ ok: false, stdout: out, tooLarge: error.code === 'ERR_CHILD_PROCESS_STDIO_MAXBUFFER' });
|
|
55
|
+
});
|
|
56
|
+
} catch {
|
|
57
|
+
done(failed);
|
|
58
|
+
}
|
|
59
|
+
})).catch(() => failed);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* The committed text of one file.
|
|
64
|
+
* @param {string} real the file's canonical path (it may be gone from the disk)
|
|
65
|
+
* @param {{ maxBytes: number, git: (args: string[], o: object) => Promise<object> }} o
|
|
66
|
+
* @returns {Promise<{ tracked: boolean, content?: string, binary?: boolean, tooLarge?: boolean }>}
|
|
67
|
+
*/
|
|
68
|
+
export async function readHeadVersion(real, { maxBytes, git }) {
|
|
69
|
+
const folder = dirname(real);
|
|
70
|
+
const top = await git(['rev-parse', '--show-toplevel'], { cwd: folder, maxBuffer: 64 * 1024 });
|
|
71
|
+
if (!top.ok) return { tracked: false };
|
|
72
|
+
const root = top.stdout.toString('utf8').replace(/\r?\n$/, '');
|
|
73
|
+
if (!root || !isAbsolute(root)) return { tracked: false };
|
|
74
|
+
// The file's name inside the repository, as git writes it. Both paths are canonical: git prints its top level
|
|
75
|
+
// resolved, and `real` is.
|
|
76
|
+
const inside = relative(root, real);
|
|
77
|
+
if (!inside || inside.startsWith('..') || isAbsolute(inside)) return { tracked: false };
|
|
78
|
+
const name = sep === '/' ? inside : inside.split(sep).join('/');
|
|
79
|
+
const shown = await git(['show', `HEAD:${name}`], { cwd: root, maxBuffer: maxBytes });
|
|
80
|
+
if (shown.tooLarge) return { tracked: true, tooLarge: true };
|
|
81
|
+
if (!shown.ok) return { tracked: false }; // not in the last commit, or no commit yet
|
|
82
|
+
const buf = shown.stdout;
|
|
83
|
+
const probe = Math.min(BINARY_PROBE_BYTES, buf.length);
|
|
84
|
+
for (let i = 0; i < probe; i++) if (buf[i] === 0x00) return { tracked: true, binary: true };
|
|
85
|
+
const bom = buf.length >= 3 && UTF8_BOM.every((byte, i) => buf[i] === byte);
|
|
86
|
+
try { return { tracked: true, content: new TextDecoder('utf-8', { fatal: true, ignoreBOM: true }).decode(bom ? buf.subarray(3) : buf) }; }
|
|
87
|
+
catch { return { tracked: true, binary: true }; }
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* GET /api/file-head?path=
|
|
92
|
+
* 200 { ok, tracked: false } git does not know the file, the folder is no repository, or there is no git
|
|
93
|
+
* 200 { ok, tracked: true, content } the file as HEAD has it: its own line endings, a byte-order mark taken off
|
|
94
|
+
* 200 { ok, tracked: true, binary: true } HEAD holds something that is not UTF-8 text
|
|
95
|
+
* 400 no path, or a folder · 403 outside the registered roots, or out of one through a link · 413 larger than `maxBytes`
|
|
96
|
+
* @param {object} deps
|
|
97
|
+
* @param {() => { dirs: string[], files: string[] }} deps.fileRoots the roots the editor's file routes allow
|
|
98
|
+
* @param {number} [deps.maxBytes]
|
|
99
|
+
* @param {(args: string[], o: object) => Promise<object>} [deps.git] runGit, or a stand-in
|
|
100
|
+
* @param {(req: object, res: object, next: () => void) => void} [deps.guard] who may use the route (the files
|
|
101
|
+
* router's own: lib/http-guards.js ownerRouteGuard); on the route itself, as the router is mounted at the root
|
|
102
|
+
*/
|
|
103
|
+
export function createEditorGitRouter({ fileRoots, maxBytes = MAX_FILE_BYTES, git = runGit, guard = null } = {}) {
|
|
104
|
+
if (typeof fileRoots !== 'function') throw new Error('createEditorGitRouter requires fileRoots');
|
|
105
|
+
const router = Router();
|
|
106
|
+
const owner = typeof guard === 'function' ? guard : (req, res, next) => next();
|
|
107
|
+
const tooLarge = () => ({ status: 413, body: { error: `File too large (the editor opens up to ${Math.max(1, Math.round(maxBytes / (1024 * 1024)))} MB)`, maxBytes } });
|
|
108
|
+
|
|
109
|
+
router.get('/api/file-head', owner, async (req, res) => {
|
|
110
|
+
try {
|
|
111
|
+
const filePath = req.query.path;
|
|
112
|
+
if (!filePath || typeof filePath !== 'string') return res.status(400).json({ error: 'Missing path parameter' });
|
|
113
|
+
const found = useFileRoot(filePath, fileRoots(), (match) => {
|
|
114
|
+
const real = canonicalInside(match.base, match.path, { allowBase: true });
|
|
115
|
+
let st = null;
|
|
116
|
+
try { st = statSync(real); } catch { st = null; }
|
|
117
|
+
return { real, st };
|
|
118
|
+
});
|
|
119
|
+
// A file that is gone from the disk may still be in the last commit; a folder never is a file.
|
|
120
|
+
if (found.st && !found.st.isFile()) return res.status(400).json({ error: 'Not a valid file' });
|
|
121
|
+
if (found.st && found.st.size > maxBytes) { const refusal = tooLarge(); return res.status(refusal.status).json(refusal.body); }
|
|
122
|
+
const head = await readHeadVersion(found.real, { maxBytes, git });
|
|
123
|
+
if (head.tooLarge) { const refusal = tooLarge(); return res.status(refusal.status).json(refusal.body); }
|
|
124
|
+
if (!head.tracked) return res.json({ ok: true, tracked: false });
|
|
125
|
+
if (head.binary) return res.json({ ok: true, tracked: true, binary: true });
|
|
126
|
+
res.json({ ok: true, tracked: true, content: head.content });
|
|
127
|
+
} catch (error) {
|
|
128
|
+
if (error instanceof PathConfineError) return res.status(error.status).json({ error: error.message });
|
|
129
|
+
res.status(500).json({ error: error?.message || String(error) });
|
|
130
|
+
}
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
return router;
|
|
134
|
+
}
|