synabun 2.0.0-beta → 2.0.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +65 -0
- package/NOTICE +5 -0
- package/README.md +1 -1
- package/THIRD-PARTY-LICENSES.md +51 -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/launcher.mjs +40 -0
- package/lib/claude-hooks.js +61 -8
- package/lib/client-config-repair.js +4 -3
- package/lib/data-home-migration.js +4 -0
- package/lib/desktop-pwa.js +206 -0
- package/lib/external-tools.js +221 -0
- package/lib/first-launch-protection.js +5 -0
- package/lib/packaged-runtime.js +110 -0
- package/lib/start-launcher.js +1396 -0
- package/mcp-server/package-lock.json +2 -2
- package/mcp-server/package.json +1 -3
- package/neural-interface/i18n/en.json +1540 -154
- package/neural-interface/i18n/pt-BR.json +1538 -153
- 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-envelope.js +9 -6
- 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/augmented-path.js +23 -3
- package/neural-interface/lib/browser-background-tab.js +134 -0
- package/neural-interface/lib/browser-language.js +83 -0
- package/neural-interface/lib/browser-screenshot.js +678 -61
- package/neural-interface/lib/claude-agent-bridge.js +87 -153
- package/neural-interface/lib/claude-executable.js +241 -0
- package/neural-interface/lib/claude-model-catalog.js +82 -93
- 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-binary-runtime.js +69 -337
- package/neural-interface/lib/native-loop-providers.js +40 -49
- package/neural-interface/lib/offline-page.js +54 -0
- 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/session-title-generator.js +5 -0
- package/neural-interface/lib/synabun-update-plan.js +19 -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 -253
- package/neural-interface/package.json +13 -2
- package/neural-interface/public/install-app.html +34 -0
- package/neural-interface/public/manifest.json +2 -0
- package/neural-interface/public/offline.html +402 -66
- package/neural-interface/public/onboarding.html +264 -147
- 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 +26 -64
- 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-rack-styles.js +0 -3
- package/neural-interface/public/shared/assistant/asst-render.js +219 -67
- 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 +162 -6
- 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/command-runner-model.js +30 -0
- 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 +169 -170
- 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 +62 -0
- package/neural-interface/public/shared/pwa-install.css +12 -0
- package/neural-interface/public/shared/pwa-install.js +127 -0
- 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-rules.js +160 -0
- package/neural-interface/public/shared/settings/settings-user-context.js +154 -0
- package/neural-interface/public/shared/start-bridge.js +802 -0
- package/neural-interface/public/shared/styleguide/sg-colors.js +5 -2
- package/neural-interface/public/shared/styleguide/sg-import-export.js +4 -3
- package/neural-interface/public/shared/styleguide/sg-kit.js +4 -1
- package/neural-interface/public/shared/styleguide/sg-panel.js +9 -1
- package/neural-interface/public/shared/styleguide/sg-typography.js +1 -0
- package/neural-interface/public/shared/styles.css +2219 -1117
- package/neural-interface/public/shared/synabun-mascot.js +20 -56
- package/neural-interface/public/shared/term-ready.js +307 -0
- package/neural-interface/public/shared/tunnel-view.js +167 -0
- package/neural-interface/public/shared/ui-claude-panel.js +160 -19
- package/neural-interface/public/shared/ui-command-runner.js +251 -468
- 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-judgments.js +1 -1
- package/neural-interface/public/shared/ui-keybinds.js +4 -0
- package/neural-interface/public/shared/ui-loading.js +367 -12
- package/neural-interface/public/shared/ui-menubar.js +21 -55
- 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 +491 -547
- 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 +153 -149
- 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 +3 -1
- package/neural-interface/public/variant/2d/main.js +2 -1
- package/neural-interface/public/variant/3d/main.js +2 -1
- 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/rebuild-pty.js +15 -12
- package/neural-interface/scripts/vendor-codemirror-entry.js +121 -0
- package/neural-interface/server.js +1092 -1212
- package/neural-interface/templates/rulesets/README.md +1 -1
- package/package.json +13 -7
- package/postinstall.js +11 -81
- package/setup.js +111 -6
- package/benchmarks/browser-live-results.json +0 -79
- package/benchmarks/browser-v2-results.json +0 -258
- package/docs/browser-v2.md +0 -109
- package/docs/hook-integrity.md +0 -89
- package/docs/judgments.md +0 -182
- package/docs/style-guide.md +0 -246
- 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/scripts/audit-claude-hook-usage.mjs +0 -12
- package/scripts/audit-memory-duplicates.mjs +0 -44
- package/scripts/measure-claude-hook-overhead.mjs +0 -77
- package/scripts/recover-user-config.mjs +0 -254
- package/skills/synabun/synabunicon.png +0 -0
|
@@ -0,0 +1,589 @@
|
|
|
1
|
+
// ═══════════════════════════════════════════
|
|
2
|
+
// SynaBun — who may ask: the checks in front of every route
|
|
3
|
+
// ═══════════════════════════════════════════
|
|
4
|
+
//
|
|
5
|
+
// Four middlewares that server.js mounts before any route (createRequestGates):
|
|
6
|
+
//
|
|
7
|
+
// host A request for the owner's data, or one that acts for them, must
|
|
8
|
+
// name this server: 403 { ok: false, code: 'BAD_HOST' } otherwise.
|
|
9
|
+
// A web page whose own name was re-pointed at this machine (DNS
|
|
10
|
+
// rebinding) is same-origin with the server and sends that name as
|
|
11
|
+
// its Host. The rule is lib/http-guards.js isAllowedHost: loopback,
|
|
12
|
+
// an IP address, this computer's names, the tunnel and the invite
|
|
13
|
+
// proxy in use, and what SYNABUN_ALLOWED_HOSTS adds. It covers
|
|
14
|
+
// everything under HOST_CHECKED_PREFIXES and the images the owner
|
|
15
|
+
// uploaded for their categories; the app's own static files stay
|
|
16
|
+
// open.
|
|
17
|
+
// remote A request that comes in from outside is nobody's until it shows
|
|
18
|
+
// an invite session: 403 { error: 'Forbidden' } for everything but
|
|
19
|
+
// the MCP endpoint and the invite sign-in. From outside is what
|
|
20
|
+
// Cloudflare's tunnel marks (cf-connecting-ip) and what arrives
|
|
21
|
+
// under a public name, the tunnel's or the invite proxy's,
|
|
22
|
+
// whatever headers it carries (isRemote). With a session it is a
|
|
23
|
+
// guest; it is never the owner. This computer's own names and
|
|
24
|
+
// addresses are not public names, whatever the invite address is
|
|
25
|
+
// set to.
|
|
26
|
+
// adminOnly An invite guest never reaches an admin-only route.
|
|
27
|
+
// features An invite guest changes nothing of a feature the owner has not
|
|
28
|
+
// opened to guests, and without the terminal reads neither of
|
|
29
|
+
// its lists of a folder's files and branches.
|
|
30
|
+
//
|
|
31
|
+
// And one for the plain /mcp mount (mcpWithoutKey): what comes in from outside
|
|
32
|
+
// needs the key (/mcp/<key>), and is refused while none is set. So does what
|
|
33
|
+
// comes from another machine on the network (fromAnotherMachine: the socket's
|
|
34
|
+
// peer is neither loopback nor one of this computer's own addresses), which
|
|
35
|
+
// server.js also asks before the tunnel is started or stopped. Those two only:
|
|
36
|
+
// what else a machine on the network may do is the owner's decision.
|
|
37
|
+
//
|
|
38
|
+
// A WebSocket upgrade passes no middleware: server.js asks ownHost(req) for the
|
|
39
|
+
// Host rule (every upgrade is under it, whatever its path) and isRemote(req)
|
|
40
|
+
// for where it comes from, before anything else looks at the request. A name
|
|
41
|
+
// that is not known while this computer's names are being read waits for
|
|
42
|
+
// them, as a route's request does (ownHostAfterNames).
|
|
43
|
+
//
|
|
44
|
+
// The two guest checks decide on the path as the router will match it, not on
|
|
45
|
+
// the path as it was written. Express matches a route whatever the case of its
|
|
46
|
+
// letters, takes one more slash after the path a router is mounted at, and
|
|
47
|
+
// decodes the parameters of a route (a handler may decode one again). So
|
|
48
|
+
// `/API/system/backup`, `/api//hook-recall` and `/api/ui-blobs/editor%2Dbackups`
|
|
49
|
+
// reach routes that a comparison of the raw path with a list of prefixes does
|
|
50
|
+
// not see. routedForms() gives every form a path can be read as; a prefix that
|
|
51
|
+
// matches any of them decides. A path that has no safe reading (a "." or ".."
|
|
52
|
+
// segment, a control character, more layers of escapes than anybody's own page
|
|
53
|
+
// writes) is refused outright: for a guest, and for a Host that is not ours.
|
|
54
|
+
//
|
|
55
|
+
// The owner's own requests are not read this way at all: with a Host of ours
|
|
56
|
+
// and no invite session, none of this looks at the path.
|
|
57
|
+
|
|
58
|
+
import { timingSafeEqual } from 'node:crypto';
|
|
59
|
+
import { readFileSync } from 'node:fs';
|
|
60
|
+
import { isIP } from 'node:net';
|
|
61
|
+
import { parse as parseDotenv } from 'dotenv';
|
|
62
|
+
import { hostNameOf, isAllowedHost, isLoopbackAddress, sendGuardError, thisMachineAddresses } from './http-guards.js';
|
|
63
|
+
import { machineNames as thisMachine } from './machine-names.js';
|
|
64
|
+
|
|
65
|
+
/** Routes an invite guest never reaches: the path, in any spelling, starts with one of these. */
|
|
66
|
+
export const ADMIN_ONLY_PREFIXES = Object.freeze([
|
|
67
|
+
'/api/settings', '/api/display-settings',
|
|
68
|
+
'/api/connections', '/api/setup', '/api/system',
|
|
69
|
+
'/api/invite/key', '/api/invite/sessions', '/api/invite/proxy', '/api/invite/permissions',
|
|
70
|
+
'/api/claude-code', '/api/mcp-key', '/api/mcp/runtime-profile',
|
|
71
|
+
// MCP servers and plugins as the owner set them up: their credentials (GET /api/mcp/env/<name>?reveal=1
|
|
72
|
+
// answers with them), the registry of the commands that are started, installs, what is written into the
|
|
73
|
+
// CLIs' own configuration, and the tool profile. Everything under /api/mcp, the two entries above included.
|
|
74
|
+
'/api/mcp',
|
|
75
|
+
'/api/plugins',
|
|
76
|
+
// Installing a skill into one of the owner's runtimes, or removing it (Settings; its older address is under
|
|
77
|
+
// /api/claude-code above). The "Skills Studio" permission opens /api/skills-studio, not this.
|
|
78
|
+
'/api/skills/install',
|
|
79
|
+
'/api/tunnel/start', '/api/tunnel/stop',
|
|
80
|
+
'/api/bridges', '/api/keybinds',
|
|
81
|
+
'/api/cli',
|
|
82
|
+
// The code editor: reading, writing and creating files, their versions, and its preferences.
|
|
83
|
+
'/api/file-content',
|
|
84
|
+
'/api/file-mkdir',
|
|
85
|
+
'/api/file-stat',
|
|
86
|
+
'/api/file-head',
|
|
87
|
+
'/api/editor',
|
|
88
|
+
// The editor's copies of unsaved buffers hold file content: their place in the UI blob store is the owner's.
|
|
89
|
+
'/api/ui-blobs/editor-backups',
|
|
90
|
+
// The File Explorer: its own routes (lib/file-explorer-api.js), the older listing of a project's files it
|
|
91
|
+
// falls back to, and the folder picker of "Add project", which opens a dialog on the owner's machine.
|
|
92
|
+
'/api/explorer',
|
|
93
|
+
'/api/project-files',
|
|
94
|
+
'/api/browse-folder',
|
|
95
|
+
'/api/codex/plans',
|
|
96
|
+
'/api/sidepanel',
|
|
97
|
+
'/api/sidepanel-runs',
|
|
98
|
+
'/api/loop',
|
|
99
|
+
'/api/schedules',
|
|
100
|
+
'/api/schedule-groups',
|
|
101
|
+
'/api/quick-timer',
|
|
102
|
+
'/api/diagnostics',
|
|
103
|
+
'/api/desktop',
|
|
104
|
+
// WhatsApp Link: linking, the owner and the permission level (its router
|
|
105
|
+
// refuses guests and remote callers on its own too: lib/http-guards.js).
|
|
106
|
+
'/api/whatsapp',
|
|
107
|
+
// TypeSafe key/config/backfill, and the hook routes: /hook-recall returns
|
|
108
|
+
// memory content and /hook-judge spends the host's judgment budget.
|
|
109
|
+
'/api/typesafe',
|
|
110
|
+
'/api/hook-',
|
|
111
|
+
'/api/map/rebuild',
|
|
112
|
+
]);
|
|
113
|
+
|
|
114
|
+
/** A guest's request that is not a GET, under one of these, needs the owner to have opened that feature to guests. */
|
|
115
|
+
export const FEATURE_PERMISSION_MAP = Object.freeze([
|
|
116
|
+
{ prefix: '/api/terminal', perm: 'terminal' },
|
|
117
|
+
{ prefix: '/api/whiteboard', perm: 'whiteboard' },
|
|
118
|
+
{ prefix: '/api/memory', perm: 'memories' },
|
|
119
|
+
{ prefix: '/api/categories', perm: 'memories' },
|
|
120
|
+
{ prefix: '/api/trash', perm: 'memories' },
|
|
121
|
+
{ prefix: '/api/skills-studio', perm: 'skills' },
|
|
122
|
+
{ prefix: '/api/browser', perm: 'browser' },
|
|
123
|
+
{ prefix: '/api/cards', perm: 'cards' },
|
|
124
|
+
{ prefix: '/api/style-guide', perm: 'styleGuide' },
|
|
125
|
+
].map(Object.freeze));
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* The GETs a guest does not get for the asking. The terminal's lists of a folder's files and of its branches take
|
|
129
|
+
* any folder of the disk from the request, so they are the terminal's: a guest reads them with the permission
|
|
130
|
+
* that gives a guest the terminal, and is refused without it. A whole segment each.
|
|
131
|
+
*/
|
|
132
|
+
export const FEATURE_READ_PATHS = Object.freeze([
|
|
133
|
+
{ path: '/api/terminal/files', perm: 'terminal' },
|
|
134
|
+
{ path: '/api/terminal/branches', perm: 'terminal' },
|
|
135
|
+
].map(Object.freeze));
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* What the Host rule covers: every route that returns the owner's data or acts for them. Everything under /api;
|
|
139
|
+
* the MCP endpoint (memory, the browser, every tool); the invite sign-in (the way in for a guest); the offline
|
|
140
|
+
* page as the server renders it (it carries the install path and the launcher's state); the owner's uploaded file
|
|
141
|
+
* icons. A whole segment each: /api and /api/…, never /apix.
|
|
142
|
+
*/
|
|
143
|
+
export const HOST_CHECKED_PREFIXES = Object.freeze(['/api', '/mcp', '/invite/auth', '/offline.html', '/custom-icons']);
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* The Host rule also covers the images the owner uploaded for their categories. They are written beside the
|
|
147
|
+
* app's own files (server.js: public/logo-<category>.<png|jpg|webp|gif>) and served with them, so they are known
|
|
148
|
+
* by their name: a file of that family at the root. The app's own files of the family (BUNDLED_LOGO_FILES) stay
|
|
149
|
+
* open, unless a category has taken one of those names for its upload (`claimed`).
|
|
150
|
+
*/
|
|
151
|
+
export const UPLOADED_IMAGE_PATH = /^\/logo-[^/]+\.(?:png|jpe?g|webp|gif)$/;
|
|
152
|
+
export const BUNDLED_LOGO_FILES = Object.freeze(['/logo-synabun.png']);
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* What a request from outside reaches without an invite session: the MCP endpoint (it has its key) and the invite
|
|
156
|
+
* sign-in. As the tunnel block of server.js always read it: the path as written starts with one of these.
|
|
157
|
+
*/
|
|
158
|
+
export const REMOTE_OPEN_PREFIXES = Object.freeze(['/mcp', '/invite']);
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Headers that speak for an agent of this computer (the Assistant's brain, a computer-use grant). From outside
|
|
162
|
+
* they count for nothing: they are taken off a remote request before anything reads them.
|
|
163
|
+
*/
|
|
164
|
+
export const REMOTE_STRIPPED_HEADERS = Object.freeze(['x-synabun-role', 'x-synabun-desktop-grant']);
|
|
165
|
+
|
|
166
|
+
/** The environment variable that adds names this server answers to (docs/api-reference.md → Who may call the server). */
|
|
167
|
+
export const ALLOWED_HOSTS_VARIABLE = 'SYNABUN_ALLOWED_HOSTS';
|
|
168
|
+
|
|
169
|
+
const MAX_DECODE_ROUNDS = 3;
|
|
170
|
+
const ESCAPE_RUNS = /(?:%[0-9a-f]{2})+/gi;
|
|
171
|
+
const ASCII_ESCAPE = /%[0-7][0-9a-f]/gi;
|
|
172
|
+
const CONTROL = /[\u0000-\u001f\u007f]/;
|
|
173
|
+
const DOTS_ONLY = /^[. ]+$/;
|
|
174
|
+
const MAX_REFUSED_NAMES = 100;
|
|
175
|
+
|
|
176
|
+
/** One layer of percent-escapes taken off. A run that is not UTF-8 keeps its bytes above 0x7F as they are written. */
|
|
177
|
+
function decodeOnce(path) {
|
|
178
|
+
return path.replace(ESCAPE_RUNS, (run) => {
|
|
179
|
+
try { return decodeURIComponent(run); }
|
|
180
|
+
catch { return run.replace(ASCII_ESCAPE, (one) => String.fromCharCode(parseInt(one.slice(1), 16))); }
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* One reading of a path, in the form the prefixes are written in: lower-case, "\" as "/", a segment without its
|
|
186
|
+
* `;parameters` and without dots or spaces at its end, one slash between segments. null when a segment is "." or
|
|
187
|
+
* ".." (or dots and spaces only): the router does not resolve those, and nothing that sends them is a page of ours.
|
|
188
|
+
*/
|
|
189
|
+
function canonical(path) {
|
|
190
|
+
const segments = [];
|
|
191
|
+
for (const written of path.toLowerCase().replace(/\\/g, '/').split('/')) {
|
|
192
|
+
const semicolon = written.indexOf(';');
|
|
193
|
+
const segment = semicolon === -1 ? written : written.slice(0, semicolon);
|
|
194
|
+
if (DOTS_ONLY.test(segment)) return null;
|
|
195
|
+
const trimmed = segment.replace(/[. ]+$/, '');
|
|
196
|
+
if (trimmed) segments.push(trimmed);
|
|
197
|
+
}
|
|
198
|
+
return `/${segments.join('/')}`;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* Every form a request path can be read as by the router or by a handler behind it: as written, and with one, two
|
|
203
|
+
* and three layers of percent-escapes taken off, each made canonical. A check that compares prefixes takes a match
|
|
204
|
+
* in any of them.
|
|
205
|
+
* @param {string} path the path as the router takes it (req.path: no query, no fragment)
|
|
206
|
+
* @returns {string[] | null} null: the path has no safe reading
|
|
207
|
+
*/
|
|
208
|
+
export function routedForms(path) {
|
|
209
|
+
if (typeof path !== 'string' || path[0] !== '/') return null;
|
|
210
|
+
const forms = [];
|
|
211
|
+
let current = path;
|
|
212
|
+
for (let round = 0; ; round++) {
|
|
213
|
+
if (CONTROL.test(current)) return null;
|
|
214
|
+
const form = canonical(current);
|
|
215
|
+
if (form === null) return null;
|
|
216
|
+
if (!forms.includes(form)) forms.push(form);
|
|
217
|
+
const next = decodeOnce(current);
|
|
218
|
+
if (next === current) return forms;
|
|
219
|
+
if (round === MAX_DECODE_ROUNDS) return null;
|
|
220
|
+
current = next;
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
const startsWithSegment = (form, prefix) => form === prefix || form.startsWith(`${prefix}/`);
|
|
225
|
+
|
|
226
|
+
/** Is this path, in any of its forms, one the Host rule covers? Also true for a path without a safe reading. */
|
|
227
|
+
export function isHostCheckedPath(path) {
|
|
228
|
+
const forms = routedForms(path);
|
|
229
|
+
return !forms || forms.some((form) => HOST_CHECKED_PREFIXES.some((prefix) => startsWithSegment(form, prefix)));
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* Is this path, in any of its forms, an image the owner uploaded for a category (UPLOADED_IMAGE_PATH)? One of the
|
|
234
|
+
* app's own files of that family is not, unless `claimed` says a category's upload now has that name.
|
|
235
|
+
* @param {string} path the path as the router takes it
|
|
236
|
+
* @param {(form: string) => boolean} [claimed] is this path (lower-case, as a category records its image) an upload?
|
|
237
|
+
*/
|
|
238
|
+
export function isUploadedImagePath(path, claimed = () => false) {
|
|
239
|
+
const forms = routedForms(path);
|
|
240
|
+
if (!forms) return false; // no safe reading: isHostCheckedPath has that case
|
|
241
|
+
return forms.some((form) => {
|
|
242
|
+
if (!UPLOADED_IMAGE_PATH.test(form)) return false;
|
|
243
|
+
if (!BUNDLED_LOGO_FILES.includes(form)) return true;
|
|
244
|
+
try { return !!claimed(form); } catch { return true; }
|
|
245
|
+
});
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/**
|
|
249
|
+
* The names a value of SYNABUN_ALLOWED_HOSTS adds: separated by commas or blanks, each a host name, host:port or
|
|
250
|
+
* an http(s) URL (isAllowedHost reads the host name of each; anything else counts for nothing). A value counts
|
|
251
|
+
* up to its first "#": no host is written with one, so what follows it is a comment, and a comment adds no name.
|
|
252
|
+
* @param {...(string | undefined | null)} values the variable as the environment has it, as the .env file has it
|
|
253
|
+
*/
|
|
254
|
+
export function extraHostNames(...values) {
|
|
255
|
+
const names = [];
|
|
256
|
+
for (const value of values) {
|
|
257
|
+
if (typeof value !== 'string') continue;
|
|
258
|
+
for (const each of value.split('#', 1)[0].replace(/^\s*(['"])(.*)\1\s*$/, '$2').split(/[\s,]+/)) {
|
|
259
|
+
if (each && !names.includes(each)) names.push(each);
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
return names;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/**
|
|
266
|
+
* SYNABUN_ALLOWED_HOSTS as the data home's .env has it now, read the way the server read that file when it
|
|
267
|
+
* started: with dotenv's own parser. What counts is what dotenv gives the line (`export NAME=…`, blanks around
|
|
268
|
+
* the "=", a quoted value, a comment after it), so a line means the same before and after a restart. Read when a
|
|
269
|
+
* request asks: a name added to the file is answered at the next request, one taken out is refused at the next.
|
|
270
|
+
* @param {string} path the .env file
|
|
271
|
+
* @param {string} [variable]
|
|
272
|
+
* @returns {string | undefined} undefined: the file has no such line, or there is no file
|
|
273
|
+
*/
|
|
274
|
+
export function savedValueOf(path, variable = ALLOWED_HOSTS_VARIABLE) {
|
|
275
|
+
try {
|
|
276
|
+
const value = parseDotenv(readFileSync(path, 'utf8'))[variable];
|
|
277
|
+
return typeof value === 'string' ? value : undefined;
|
|
278
|
+
} catch {
|
|
279
|
+
return undefined;
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
/**
|
|
284
|
+
* SYNABUN_ALLOWED_HOSTS as the process was started with it, kept apart from what the data home's .env says.
|
|
285
|
+
* Loading that file into the environment copies the file's value into it, and there it stays when the line is
|
|
286
|
+
* taken out of the file again: a name could be added without a restart and never removed. So the value the
|
|
287
|
+
* environment has by itself is noted before the file is loaded (`value`), and put back afterwards (`restore`);
|
|
288
|
+
* the file's own value is read from the file when a request asks. Nothing the server starts inherits the file's.
|
|
289
|
+
* @param {object} [env] process.env
|
|
290
|
+
* @param {string} [variable]
|
|
291
|
+
* @returns {{ value: string | undefined, restore: () => void }}
|
|
292
|
+
*/
|
|
293
|
+
export function startedValueOf(env = process.env, variable = ALLOWED_HOSTS_VARIABLE) {
|
|
294
|
+
const had = Object.prototype.hasOwnProperty.call(env, variable);
|
|
295
|
+
const value = had ? env[variable] : undefined;
|
|
296
|
+
return {
|
|
297
|
+
value,
|
|
298
|
+
restore() {
|
|
299
|
+
if (had) env[variable] = value;
|
|
300
|
+
else delete env[variable];
|
|
301
|
+
},
|
|
302
|
+
};
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
/** The host names and addresses of a list of http(s) URLs, host names or host:port entries, as hostNameOf writes them. */
|
|
306
|
+
export function hostsOf(list) {
|
|
307
|
+
let entries;
|
|
308
|
+
try { entries = typeof list === 'function' ? list() : list; } catch { return []; }
|
|
309
|
+
const names = [];
|
|
310
|
+
for (const entry of Array.isArray(entries) ? entries : []) {
|
|
311
|
+
if (!entry) continue;
|
|
312
|
+
let host = String(entry);
|
|
313
|
+
if (host.includes('://')) {
|
|
314
|
+
try {
|
|
315
|
+
const url = new URL(host);
|
|
316
|
+
if (url.protocol !== 'http:' && url.protocol !== 'https:') continue;
|
|
317
|
+
host = url.host;
|
|
318
|
+
} catch { continue; }
|
|
319
|
+
}
|
|
320
|
+
const name = hostNameOf(host);
|
|
321
|
+
if (name) names.push(name);
|
|
322
|
+
}
|
|
323
|
+
return names;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
/**
|
|
327
|
+
* A socket's peer address as this computer's own addresses are written (thisMachineAddresses): IPv4 without the
|
|
328
|
+
* "::ffff:" of a dual-stack socket, IPv6 in brackets without its zone. null when there is none.
|
|
329
|
+
*/
|
|
330
|
+
function peerName(address) {
|
|
331
|
+
if (typeof address !== 'string' || !address) return null;
|
|
332
|
+
const bare = address.replace(/%.*$/, '');
|
|
333
|
+
const v4 = /^::ffff:/i.test(bare) && isIP(bare.slice(7)) === 4 ? bare.slice(7) : bare;
|
|
334
|
+
if (isIP(v4) === 4) return v4;
|
|
335
|
+
return isIP(v4) === 6 ? hostNameOf(`[${v4}]`) : null;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
function sameText(a, b) {
|
|
339
|
+
const left = Buffer.from(String(a));
|
|
340
|
+
const right = Buffer.from(String(b));
|
|
341
|
+
return left.length === right.length && timingSafeEqual(left, right);
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
/** `/mcp/<key>` with the key that is set: the caller's proof is in the path, whatever name it used to get here. */
|
|
345
|
+
function carriesMcpKey(path, key) {
|
|
346
|
+
if (!key || typeof key !== 'string' || typeof path !== 'string' || !path.startsWith('/mcp/')) return false;
|
|
347
|
+
const end = path.indexOf('/', 5);
|
|
348
|
+
return sameText(end === -1 ? path.slice(5) : path.slice(5, end), key);
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
/**
|
|
352
|
+
* The checks in front of the routes.
|
|
353
|
+
* @param {object} deps
|
|
354
|
+
* @param {(req: object) => boolean} deps.isGuestRequest an invite guest's request?
|
|
355
|
+
* @param {() => object} [deps.permissions] what the owner opened to guests ({ terminal, whiteboard, memories, … })
|
|
356
|
+
* @param {string[] | (() => string[])} [deps.allowedHosts] the public names of this server: the tunnel's and the
|
|
357
|
+
* invite proxy's URLs, and the names of SYNABUN_ALLOWED_HOSTS (asked only when no local rule matched)
|
|
358
|
+
* @param {() => (string | null)} [deps.mcpKey] the key of /mcp/<key>, when one is set
|
|
359
|
+
* @param {string[] | (() => string[])} [deps.publicHosts] the names requests from outside arrive under: the
|
|
360
|
+
* tunnel's URL and the invite proxy's when one is in use. Never the names of SYNABUN_ALLOWED_HOSTS, which are
|
|
361
|
+
* the owner's own for this computer
|
|
362
|
+
* @param {(req: object) => boolean} [deps.hasInviteSession] does this request carry an invite session that is
|
|
363
|
+
* still good? (nobody has one when this is left out)
|
|
364
|
+
* @param {(path: string) => boolean} [deps.uploadedImages] is this root file (lower-case: "/logo-<name>.<ext>")
|
|
365
|
+
* the image a category recorded as its upload? Asked only for the app's own files of that family
|
|
366
|
+
* @param {() => string[]} [deps.localAddresses] this computer's own addresses, as a Host header names them
|
|
367
|
+
* (lib/http-guards.js thisMachineAddresses when it is left out)
|
|
368
|
+
* @param {{ names: () => string[], refresh: () => Promise<any>, stale: () => boolean }} [deps.machine]
|
|
369
|
+
* lib/machine-names.js; a name that is not known is checked again after the system was asked anew
|
|
370
|
+
* @param {() => string} [deps.settingsHint] where SYNABUN_ALLOWED_HOSTS is set, for the log line (the .env's path)
|
|
371
|
+
* @param {(line: string) => void} [deps.log]
|
|
372
|
+
* @returns {{ host: Function, remote: Function, adminOnly: Function, features: Function, mcpWithoutKey: Function,
|
|
373
|
+
* ownHost: (req: object) => boolean, ownHostAfterNames: (req: object, socket: object, again: () => void) => boolean,
|
|
374
|
+
* isRemote: (req: object) => boolean, fromAnotherMachine: (req: object) => boolean, refusedHosts: () => string[] }}
|
|
375
|
+
*/
|
|
376
|
+
export function createRequestGates({
|
|
377
|
+
isGuestRequest, permissions = () => ({}), allowedHosts = [], mcpKey = () => null, machine = thisMachine,
|
|
378
|
+
settingsHint = () => '', log = (line) => console.warn(line),
|
|
379
|
+
publicHosts = [], hasInviteSession = () => false, uploadedImages = () => false, localAddresses = thisMachineAddresses,
|
|
380
|
+
} = {}) {
|
|
381
|
+
if (typeof isGuestRequest !== 'function') throw new Error('createRequestGates requires isGuestRequest');
|
|
382
|
+
const refused = new Set(); // names already written to the log
|
|
383
|
+
const forms = new WeakMap(); // req → routedForms(req.path), read once for the two guest checks
|
|
384
|
+
const formsOf = (req) => {
|
|
385
|
+
if (!forms.has(req)) forms.set(req, routedForms(req.path));
|
|
386
|
+
return forms.get(req);
|
|
387
|
+
};
|
|
388
|
+
const ours = (host) => isAllowedHost(host, { allowedHosts, machineName: machine.names });
|
|
389
|
+
|
|
390
|
+
/** One line per name: what was refused, and what to do when the name is the owner's. */
|
|
391
|
+
function note(req) {
|
|
392
|
+
const written = req.headers?.host;
|
|
393
|
+
const name = hostNameOf(written);
|
|
394
|
+
const label = name ?? (typeof written === 'string' ? JSON.stringify(written.slice(0, 80)) : 'no Host header');
|
|
395
|
+
if (refused.has(label)) return;
|
|
396
|
+
if (refused.size >= MAX_REFUSED_NAMES) {
|
|
397
|
+
if (!refused.has('…')) { refused.add('…'); log('[host] More names were refused; they are no longer listed one by one.'); }
|
|
398
|
+
return;
|
|
399
|
+
}
|
|
400
|
+
refused.add(label);
|
|
401
|
+
if (!name) {
|
|
402
|
+
log(`[host] Refused a request whose Host header is not a name a browser sends (${label}).`);
|
|
403
|
+
return;
|
|
404
|
+
}
|
|
405
|
+
let where = '';
|
|
406
|
+
try { where = String(settingsHint() || ''); } catch { where = ''; }
|
|
407
|
+
log(`[host] Refused a request for "${name}": this server does not answer to that name. It answers to localhost, `
|
|
408
|
+
+ 'an IP address, this computer\'s own names and the tunnel or invite proxy in use. If the name is this '
|
|
409
|
+
+ `computer's too, add ${ALLOWED_HOSTS_VARIABLE}=${name}${where ? ` to ${where}` : ''} (several names: `
|
|
410
|
+
+ 'separated by commas; it is read again without a restart). If it is not, a web page tried to reach this '
|
|
411
|
+
+ 'server under a name of its own and was stopped.');
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
function refuseHost(req, res) {
|
|
415
|
+
note(req);
|
|
416
|
+
return sendGuardError(res, 403, 'BAD_HOST', `This server does not answer to the name in the request's Host header. Open it at localhost or an IP address, or add the name to ${ALLOWED_HOSTS_VARIABLE}.`);
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
function host(req, res, next) {
|
|
420
|
+
const written = req.headers?.host;
|
|
421
|
+
if (ours(written)) return next();
|
|
422
|
+
let key = null;
|
|
423
|
+
try { key = mcpKey(); } catch { key = null; }
|
|
424
|
+
if (carriesMcpKey(req.path, key)) return next();
|
|
425
|
+
if (!isHostCheckedPath(req.path) && !isUploadedImagePath(req.path, uploadedImages)) return next();
|
|
426
|
+
// The computer may have been renamed since its names were read: they are read again (once a minute at most)
|
|
427
|
+
// before a name is refused.
|
|
428
|
+
if (machine.stale()) {
|
|
429
|
+
machine.refresh().then(() => (ours(written) ? next() : refuseHost(req, res)), () => refuseHost(req, res));
|
|
430
|
+
return undefined;
|
|
431
|
+
}
|
|
432
|
+
return refuseHost(req, res);
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
const waited = new WeakSet(); // upgrades that have waited for this computer's names: each waits once
|
|
436
|
+
const twoHosts = (req) => { const named = req?.headersDistinct?.host; return Array.isArray(named) && named.length > 1; };
|
|
437
|
+
/**
|
|
438
|
+
* Could a reading of this computer's names still make this upgrade's Host ours? Only a name can become one
|
|
439
|
+
* (what is no name, and two Hosts, stay refused), only while the answers are old or on their way
|
|
440
|
+
* (`machine.stale()`: never where the system is not asked), and only once per upgrade.
|
|
441
|
+
*/
|
|
442
|
+
function namesDue(req) {
|
|
443
|
+
if (!req || typeof req !== 'object' || waited.has(req) || twoHosts(req) || hostNameOf(req.headers?.host) === null) return false;
|
|
444
|
+
try { return machine.stale() === true; } catch { return false; }
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
/**
|
|
448
|
+
* The Host rule for a WebSocket upgrade: is its Host one of this server's names? Every upgrade is under the
|
|
449
|
+
* rule, whatever its path and whether it carries an Origin or not (no file of the app is served over one, and
|
|
450
|
+
* a client without an Origin passes the Origin check). It answers from what is known now, at once: a caller
|
|
451
|
+
* that got `false` asks ownHostAfterNames() whether the system's names, which `host` waits for, are worth
|
|
452
|
+
* waiting for here too. A refused name is logged once, when the refusal is the last word.
|
|
453
|
+
* An upgrade that names two Hosts is refused whatever they are: Node reads the first, and a proxy in front may
|
|
454
|
+
* have gone by the other.
|
|
455
|
+
*/
|
|
456
|
+
function ownHost(req) {
|
|
457
|
+
if (twoHosts(req)) {
|
|
458
|
+
if (!refused.has('two Host headers')) { refused.add('two Host headers'); log('[host] Refused a WebSocket upgrade that names two Hosts: nothing of ours sends that.'); }
|
|
459
|
+
return false;
|
|
460
|
+
}
|
|
461
|
+
if (ours(req?.headers?.host)) return true;
|
|
462
|
+
if (!namesDue(req)) note({ headers: req?.headers || {} });
|
|
463
|
+
return false;
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
/**
|
|
467
|
+
* For an upgrade ownHost() has just said no to: the computer's names are read when the server starts and again
|
|
468
|
+
* when the answers are old, and until a reading has ended the Bonjour name is not known. `host` waits for the
|
|
469
|
+
* reading before it refuses a request; an upgrade does the same here, so a page left open under that name, or
|
|
470
|
+
* a client that connects directly, is not answered 403 right after a start.
|
|
471
|
+
* → false: nothing to wait for (see namesDue). The caller refuses now.
|
|
472
|
+
* → true: `again()` is called when the reading has ended, and the caller asks ownHost() once more, which then
|
|
473
|
+
* gives the last word. Not called when the socket went away meanwhile.
|
|
474
|
+
* The reading is the one everybody shares (`machine.refresh()`, asked only while `machine.stale()`): names that
|
|
475
|
+
* are not known cannot have the system asked more often than its cache allows. Node has let go of an upgrade's
|
|
476
|
+
* socket, error listener included, so one is kept on it for as long as it waits here: a connection reset in
|
|
477
|
+
* that time would otherwise be an uncaught exception.
|
|
478
|
+
*/
|
|
479
|
+
function ownHostAfterNames(req, socket, again) {
|
|
480
|
+
if (typeof again !== 'function' || !namesDue(req)) return false;
|
|
481
|
+
waited.add(req);
|
|
482
|
+
const quiet = () => {};
|
|
483
|
+
try { socket?.on?.('error', quiet); } catch { /* not a socket */ }
|
|
484
|
+
const ask = () => {
|
|
485
|
+
try { socket?.off?.('error', quiet); } catch { /* not a socket */ }
|
|
486
|
+
if (!socket?.destroyed) again();
|
|
487
|
+
};
|
|
488
|
+
let read;
|
|
489
|
+
try { read = Promise.resolve(machine.refresh()); } catch { read = Promise.resolve(); }
|
|
490
|
+
read.catch(() => {}).then(ask);
|
|
491
|
+
return true;
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
/** Does this Host (or forwarded Host) say the request came in under a public name: the tunnel's, the invite proxy's? */
|
|
495
|
+
function underPublicName(written) {
|
|
496
|
+
const name = hostNameOf(written);
|
|
497
|
+
if (!name) return false;
|
|
498
|
+
const address = name.startsWith('[') ? name.slice(1, -1) : (isIP(name) === 4 ? name : null);
|
|
499
|
+
// This computer itself is nobody else's way in, whatever the invite address was set to: under loopback, one
|
|
500
|
+
// of its own names or one of its own addresses the owner's requests arrive, as they always did. (What is
|
|
501
|
+
// cheap to know is asked first: the public names are read from a file.)
|
|
502
|
+
if (address ? isLoopbackAddress(address) : isAllowedHost(name, { allowedHosts: [], machineName: machine.names })) return false;
|
|
503
|
+
if (!hostsOf(publicHosts).includes(name)) return false;
|
|
504
|
+
if (!address) return true;
|
|
505
|
+
let own = [];
|
|
506
|
+
try { own = localAddresses() || []; } catch { own = []; }
|
|
507
|
+
return !own.includes(name);
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
/**
|
|
511
|
+
* Did this request come in from outside? Cloudflare's tunnel says so with a header; a request under a public
|
|
512
|
+
* name (the Host it names, or the one a proxy says it was sent to) is from outside whatever it carries.
|
|
513
|
+
* Reads headers only: a WebSocket upgrade is asked the same way.
|
|
514
|
+
*/
|
|
515
|
+
function isRemote(req) {
|
|
516
|
+
const headers = req?.headers || {};
|
|
517
|
+
if (headers['cf-connecting-ip'] !== undefined) return true;
|
|
518
|
+
if (underPublicName(headers.host)) return true;
|
|
519
|
+
const forwarded = headers['x-forwarded-host'];
|
|
520
|
+
return typeof forwarded === 'string' && forwarded.split(',').some((each) => underPublicName(each.trim()));
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
function remote(req, res, next) {
|
|
524
|
+
if (!isRemote(req)) return next();
|
|
525
|
+
for (const name of REMOTE_STRIPPED_HEADERS) delete req.headers[name];
|
|
526
|
+
if (REMOTE_OPEN_PREFIXES.some((prefix) => req.path.startsWith(prefix))) return next();
|
|
527
|
+
let session = false;
|
|
528
|
+
try { session = !!hasInviteSession(req); } catch { session = false; }
|
|
529
|
+
if (session) return next();
|
|
530
|
+
return res.status(403).json({ error: 'Forbidden' });
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
/**
|
|
534
|
+
* Did this request's connection come from another machine? Its socket's peer (the socket's own address, never a
|
|
535
|
+
* header) is neither a loopback address nor one of this computer's own addresses; a request without one counts
|
|
536
|
+
* as another machine's. Asked for two things only: the plain /mcp mount (mcpWithoutKey) and the tunnel's start
|
|
537
|
+
* and stop (server.js). What else a machine on the network may do is the owner's decision, not this check's.
|
|
538
|
+
* The tunnel, and a proxy on this computer, connect from loopback; a proxy on another machine is that machine.
|
|
539
|
+
*/
|
|
540
|
+
function fromAnotherMachine(req) {
|
|
541
|
+
const peer = req?.socket?.remoteAddress;
|
|
542
|
+
if (isLoopbackAddress(peer)) return false;
|
|
543
|
+
const name = peerName(peer);
|
|
544
|
+
if (!name) return true;
|
|
545
|
+
let own = [];
|
|
546
|
+
try { own = localAddresses() || []; } catch { own = []; }
|
|
547
|
+
return !own.includes(name);
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
/**
|
|
551
|
+
* In front of the plain /mcp mount: from outside, and from another machine on the network, the endpoint is
|
|
552
|
+
* reached with its key (/mcp/<key>) and no other way. While no key is set there is no way in from there at all:
|
|
553
|
+
* the tunnel's or the invite proxy's address, or this computer's address on the network, by itself opens the
|
|
554
|
+
* memory, the browser and every other tool to nobody. On this computer the plain endpoint answers as it always
|
|
555
|
+
* did, key or no key. (The same answer with a key set and without: it does not say which.)
|
|
556
|
+
*/
|
|
557
|
+
function mcpWithoutKey(req, res, next) {
|
|
558
|
+
if (isRemote(req) || fromAnotherMachine(req)) return res.status(403).json({ error: 'Forbidden' });
|
|
559
|
+
return next();
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
function adminOnly(req, res, next) {
|
|
563
|
+
if (!isGuestRequest(req)) return next();
|
|
564
|
+
const read = formsOf(req);
|
|
565
|
+
if (!read) return res.status(400).json({ error: 'Unsupported path' });
|
|
566
|
+
if (read.some((form) => ADMIN_ONLY_PREFIXES.some((prefix) => form.startsWith(prefix)))) {
|
|
567
|
+
return res.status(403).json({ error: 'Admin only' });
|
|
568
|
+
}
|
|
569
|
+
return next();
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
function features(req, res, next) {
|
|
573
|
+
if (!isGuestRequest(req)) return next();
|
|
574
|
+
const read = formsOf(req);
|
|
575
|
+
if (!read) return res.status(400).json({ error: 'Unsupported path' });
|
|
576
|
+
// A guest's GET is free but for the few that are a feature's own (FEATURE_READ_PATHS); anything else a guest
|
|
577
|
+
// sends under a feature needs that feature.
|
|
578
|
+
const match = req.method === 'GET'
|
|
579
|
+
? FEATURE_READ_PATHS.find((entry) => read.some((form) => startsWithSegment(form, entry.path)))
|
|
580
|
+
: FEATURE_PERMISSION_MAP.find((entry) => read.some((form) => form.startsWith(entry.prefix)));
|
|
581
|
+
if (match && !permissions()?.[match.perm]) return res.status(403).json({ error: 'Feature not enabled' });
|
|
582
|
+
return next();
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
// The names are read now, so the first request under the Bonjour name finds them known.
|
|
586
|
+
try { Promise.resolve(machine.refresh()).catch(() => {}); } catch { /* a machine that cannot be asked adds no name */ }
|
|
587
|
+
|
|
588
|
+
return { host, ownHost, ownHostAfterNames, remote, adminOnly, features, mcpWithoutKey, isRemote, fromAnotherMachine, refusedHosts: () => [...refused] };
|
|
589
|
+
}
|
|
@@ -70,6 +70,10 @@ export function extractCodexText(stdout) {
|
|
|
70
70
|
}
|
|
71
71
|
|
|
72
72
|
async function generateClaudeTitle(input, deps) {
|
|
73
|
+
// The user's Claude Code, as every other SDK call (lib/claude-executable.js).
|
|
74
|
+
// Without one there is no title to ask for: the caller falls back to its own.
|
|
75
|
+
const executable = typeof deps.claudeExecutable === 'function' ? deps.claudeExecutable() : null;
|
|
76
|
+
if (!executable?.path && !deps.claudeQuery) throw new Error(executable?.reason || 'Claude CLI not found');
|
|
73
77
|
const abortController = new AbortController();
|
|
74
78
|
const timer = setTimeout(() => abortController.abort(new Error('Session title request timed out')), SESSION_TITLE_TIMEOUT_MS);
|
|
75
79
|
timer.unref?.();
|
|
@@ -89,6 +93,7 @@ async function generateClaudeTitle(input, deps) {
|
|
|
89
93
|
persistSession: false,
|
|
90
94
|
includePartialMessages: false,
|
|
91
95
|
abortController,
|
|
96
|
+
...(executable?.path ? { pathToClaudeCodeExecutable: executable.path } : {}),
|
|
92
97
|
env: cleanClaudeEnv(deps.env || process.env),
|
|
93
98
|
canUseTool: async () => ({ behavior: 'deny', message: 'Session title generation cannot use tools.' }),
|
|
94
99
|
},
|
|
@@ -246,6 +246,25 @@ export function buildSynabunInstallPlan({
|
|
|
246
246
|
const version = formatSynabunVersion(latest);
|
|
247
247
|
const target = version || latest;
|
|
248
248
|
|
|
249
|
+
if (kind === 'packaged-app') {
|
|
250
|
+
// A packaged application carries its own runtime and dependencies: npm and
|
|
251
|
+
// git never write inside it. A newer build is installed over it instead.
|
|
252
|
+
const releasesUrl = `${repoUrl}/releases`;
|
|
253
|
+
return {
|
|
254
|
+
source: 'github',
|
|
255
|
+
installSource: kind,
|
|
256
|
+
canAutoUpdate: false,
|
|
257
|
+
reason: 'packaged-app',
|
|
258
|
+
current,
|
|
259
|
+
target,
|
|
260
|
+
channel: installedChannel,
|
|
261
|
+
displayCommand: releasesUrl,
|
|
262
|
+
openUrl: releasesUrl,
|
|
263
|
+
manualCommand: null,
|
|
264
|
+
manualHint: `This copy of SynaBun is a packaged application, so v${target} is installed by replacing it with the newer build, not from inside the app. Nothing was installed. Get the build for your system from the releases page and install it over this one; your data stays where it is.`,
|
|
265
|
+
};
|
|
266
|
+
}
|
|
267
|
+
|
|
249
268
|
if (kind !== 'npm-global') {
|
|
250
269
|
const clonedHint = kind === 'github-clone'
|
|
251
270
|
? 'This SynaBun install is a GitHub checkout. Open the repository to pull or reinstall from the correct source.'
|