opencami 1.0.0
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/LICENSE +22 -0
- package/README.md +111 -0
- package/bin/opencami.js +155 -0
- package/dist/client/apple-touch-icon-180x180.png +0 -0
- package/dist/client/assets/_sessionKey-B-MBYHgD.js +95 -0
- package/dist/client/assets/abap-BdImnpbu.js +1 -0
- package/dist/client/assets/actionscript-3-CfeIJUat.js +1 -0
- package/dist/client/assets/ada-bCR0ucgS.js +1 -0
- package/dist/client/assets/andromeeda-C-Jbm3Hp.js +1 -0
- package/dist/client/assets/angular-html-CU67Zn6k.js +1 -0
- package/dist/client/assets/angular-ts-BwZT4LLn.js +1 -0
- package/dist/client/assets/apache-Pmp26Uib.js +1 -0
- package/dist/client/assets/apex-D8_7TLub.js +1 -0
- package/dist/client/assets/apl-dKokRX4l.js +1 -0
- package/dist/client/assets/applescript-Co6uUVPk.js +1 -0
- package/dist/client/assets/ara-BRHolxvo.js +1 -0
- package/dist/client/assets/asciidoc-Dv7Oe6Be.js +1 -0
- package/dist/client/assets/asm-D_Q5rh1f.js +1 -0
- package/dist/client/assets/astro-CbQHKStN.js +1 -0
- package/dist/client/assets/aurora-x-D-2ljcwZ.js +1 -0
- package/dist/client/assets/awk-DMzUqQB5.js +1 -0
- package/dist/client/assets/ayu-dark-CmMr59Fi.js +1 -0
- package/dist/client/assets/ballerina-BFfxhgS-.js +1 -0
- package/dist/client/assets/bat-BkioyH1T.js +1 -0
- package/dist/client/assets/beancount-k_qm7-4y.js +1 -0
- package/dist/client/assets/berry-uYugtg8r.js +1 -0
- package/dist/client/assets/bibtex-CHM0blh-.js +1 -0
- package/dist/client/assets/bicep-Bmn6On1c.js +1 -0
- package/dist/client/assets/blade-D4QpJJKB.js +1 -0
- package/dist/client/assets/bsl-BO_Y6i37.js +1 -0
- package/dist/client/assets/button-D_9OBT_f.js +1 -0
- package/dist/client/assets/c-BIGW1oBm.js +1 -0
- package/dist/client/assets/c3-VCDPK7BO.js +1 -0
- package/dist/client/assets/cadence-Bv_4Rxtq.js +1 -0
- package/dist/client/assets/cairo-KRGpt6FW.js +1 -0
- package/dist/client/assets/catppuccin-frappe-DFWUc33u.js +1 -0
- package/dist/client/assets/catppuccin-latte-C9dUb6Cb.js +1 -0
- package/dist/client/assets/catppuccin-macchiato-DQyhUUbL.js +1 -0
- package/dist/client/assets/catppuccin-mocha-D87Tk5Gz.js +1 -0
- package/dist/client/assets/clarity-D53aC0YG.js +1 -0
- package/dist/client/assets/clojure-P80f7IUj.js +1 -0
- package/dist/client/assets/cmake-D1j8_8rp.js +1 -0
- package/dist/client/assets/cobol-nwyudZeR.js +1 -0
- package/dist/client/assets/codeowners-Bp6g37R7.js +1 -0
- package/dist/client/assets/codeql-DsOJ9woJ.js +1 -0
- package/dist/client/assets/coffee-Ch7k5sss.js +1 -0
- package/dist/client/assets/common-lisp-Cg-RD9OK.js +1 -0
- package/dist/client/assets/connect-BRasuRbB.js +2 -0
- package/dist/client/assets/coq-DkFqJrB1.js +1 -0
- package/dist/client/assets/cpp-CofmeUqb.js +1 -0
- package/dist/client/assets/crystal-tKQVLTB8.js +1 -0
- package/dist/client/assets/csharp-K5feNrxe.js +1 -0
- package/dist/client/assets/css-DPfMkruS.js +1 -0
- package/dist/client/assets/csv-fuZLfV_i.js +1 -0
- package/dist/client/assets/cue-D82EKSYY.js +1 -0
- package/dist/client/assets/cypher-COkxafJQ.js +1 -0
- package/dist/client/assets/d-85-TOEBH.js +1 -0
- package/dist/client/assets/dark-plus-C3mMm8J8.js +1 -0
- package/dist/client/assets/dart-CF10PKvl.js +1 -0
- package/dist/client/assets/dax-CEL-wOlO.js +1 -0
- package/dist/client/assets/desktop-BmXAJ9_W.js +1 -0
- package/dist/client/assets/diff-D97Zzqfu.js +1 -0
- package/dist/client/assets/docker-BcOcwvcX.js +1 -0
- package/dist/client/assets/dotenv-Da5cRb03.js +1 -0
- package/dist/client/assets/dracula-BzJJZx-M.js +1 -0
- package/dist/client/assets/dracula-soft-BXkSAIEj.js +1 -0
- package/dist/client/assets/dream-maker-BtqSS_iP.js +1 -0
- package/dist/client/assets/edge-BkV0erSs.js +1 -0
- package/dist/client/assets/elixir-CDX3lj18.js +1 -0
- package/dist/client/assets/elm-DbKCFpqz.js +1 -0
- package/dist/client/assets/emacs-lisp-C9XAeP06.js +1 -0
- package/dist/client/assets/erb-BOJIQeun.js +1 -0
- package/dist/client/assets/erlang-DsQrWhSR.js +1 -0
- package/dist/client/assets/everforest-dark-BgDCqdQA.js +1 -0
- package/dist/client/assets/everforest-light-C8M2exoo.js +1 -0
- package/dist/client/assets/fennel-BYunw83y.js +1 -0
- package/dist/client/assets/file-explorer-screen-Daf4rIq9.js +1 -0
- package/dist/client/assets/files-ByeIMLfw.js +2 -0
- package/dist/client/assets/fish-BvzEVeQv.js +1 -0
- package/dist/client/assets/fluent-C4IJs8-o.js +1 -0
- package/dist/client/assets/fortran-fixed-form-CkoXwp7k.js +1 -0
- package/dist/client/assets/fortran-free-form-BxgE0vQu.js +1 -0
- package/dist/client/assets/fsharp-CXgrBDvD.js +1 -0
- package/dist/client/assets/gdresource-B7Tvp0Sc.js +1 -0
- package/dist/client/assets/gdscript-DTMYz4Jt.js +1 -0
- package/dist/client/assets/gdshader-DkwncUOv.js +1 -0
- package/dist/client/assets/genie-D0YGMca9.js +1 -0
- package/dist/client/assets/gherkin-DyxjwDmM.js +1 -0
- package/dist/client/assets/git-commit-F4YmCXRG.js +1 -0
- package/dist/client/assets/git-rebase-r7XF79zn.js +1 -0
- package/dist/client/assets/github-dark-DHJKELXO.js +1 -0
- package/dist/client/assets/github-dark-default-Cuk6v7N8.js +1 -0
- package/dist/client/assets/github-dark-dimmed-DH5Ifo-i.js +1 -0
- package/dist/client/assets/github-dark-high-contrast-E3gJ1_iC.js +1 -0
- package/dist/client/assets/github-light-DAi9KRSo.js +1 -0
- package/dist/client/assets/github-light-default-D7oLnXFd.js +1 -0
- package/dist/client/assets/github-light-high-contrast-BfjtVDDH.js +1 -0
- package/dist/client/assets/gleam-BspZqrRM.js +1 -0
- package/dist/client/assets/glimmer-js-Rg0-pVw9.js +1 -0
- package/dist/client/assets/glimmer-ts-U6CK756n.js +1 -0
- package/dist/client/assets/glsl-DplSGwfg.js +1 -0
- package/dist/client/assets/gn-n2N0HUVH.js +1 -0
- package/dist/client/assets/gnuplot-DdkO51Og.js +1 -0
- package/dist/client/assets/go-Dn2_MT6a.js +1 -0
- package/dist/client/assets/graphql-ChdNCCLP.js +1 -0
- package/dist/client/assets/groovy-gcz8RCvz.js +1 -0
- package/dist/client/assets/gruvbox-dark-hard-CFHQjOhq.js +1 -0
- package/dist/client/assets/gruvbox-dark-medium-GsRaNv29.js +1 -0
- package/dist/client/assets/gruvbox-dark-soft-CVdnzihN.js +1 -0
- package/dist/client/assets/gruvbox-light-hard-CH1njM8p.js +1 -0
- package/dist/client/assets/gruvbox-light-medium-DRw_LuNl.js +1 -0
- package/dist/client/assets/gruvbox-light-soft-hJgmCMqR.js +1 -0
- package/dist/client/assets/hack-CaT9iCJl.js +1 -0
- package/dist/client/assets/haml-B8DHNrY2.js +1 -0
- package/dist/client/assets/handlebars-BL8al0AC.js +1 -0
- package/dist/client/assets/haskell-Df6bDoY_.js +1 -0
- package/dist/client/assets/haxe-CzTSHFRz.js +1 -0
- package/dist/client/assets/hcl-BWvSN4gD.js +1 -0
- package/dist/client/assets/hjson-D5-asLiD.js +1 -0
- package/dist/client/assets/hlsl-D3lLCCz7.js +1 -0
- package/dist/client/assets/houston-DnULxvSX.js +1 -0
- package/dist/client/assets/html-GMplVEZG.js +1 -0
- package/dist/client/assets/html-derivative-BFtXZ54Q.js +1 -0
- package/dist/client/assets/http-jrhK8wxY.js +1 -0
- package/dist/client/assets/hurl-irOxFIW8.js +1 -0
- package/dist/client/assets/hxml-Bvhsp5Yf.js +1 -0
- package/dist/client/assets/hy-DFXneXwc.js +1 -0
- package/dist/client/assets/imba-DGztddWO.js +1 -0
- package/dist/client/assets/index-ByUDBI-n.js +14 -0
- package/dist/client/assets/index-DHGnKfAU.js +1 -0
- package/dist/client/assets/ini-BEwlwnbL.js +1 -0
- package/dist/client/assets/java-CylS5w8V.js +1 -0
- package/dist/client/assets/javascript-wDzz0qaB.js +1 -0
- package/dist/client/assets/jinja-4LBKfQ-Z.js +1 -0
- package/dist/client/assets/jison-wvAkD_A8.js +1 -0
- package/dist/client/assets/json-Cp-IABpG.js +1 -0
- package/dist/client/assets/json5-C9tS-k6U.js +1 -0
- package/dist/client/assets/jsonc-Des-eS-w.js +1 -0
- package/dist/client/assets/jsonl-DcaNXYhu.js +1 -0
- package/dist/client/assets/jsonnet-DFQXde-d.js +1 -0
- package/dist/client/assets/jssm-C2t-YnRu.js +1 -0
- package/dist/client/assets/jsx-g9-lgVsj.js +1 -0
- package/dist/client/assets/julia-CxzCAyBv.js +1 -0
- package/dist/client/assets/kanagawa-dragon-CkXjmgJE.js +1 -0
- package/dist/client/assets/kanagawa-lotus-CfQXZHmo.js +1 -0
- package/dist/client/assets/kanagawa-wave-DWedfzmr.js +1 -0
- package/dist/client/assets/kdl-DV7GczEv.js +1 -0
- package/dist/client/assets/keyboard-shortcuts-dialog-agsWJ36q.js +1 -0
- package/dist/client/assets/kotlin-BdnUsdx6.js +1 -0
- package/dist/client/assets/kusto-DZf3V79B.js +1 -0
- package/dist/client/assets/laserwave-DUszq2jm.js +1 -0
- package/dist/client/assets/latex-B4uzh10-.js +1 -0
- package/dist/client/assets/lean-BZvkOJ9d.js +1 -0
- package/dist/client/assets/less-B1dDrJ26.js +1 -0
- package/dist/client/assets/light-plus-B7mTdjB0.js +1 -0
- package/dist/client/assets/liquid-DYVedYrR.js +1 -0
- package/dist/client/assets/llvm-BtvRca6l.js +1 -0
- package/dist/client/assets/log-2UxHyX5q.js +1 -0
- package/dist/client/assets/logo-BtOb2qkB.js +1 -0
- package/dist/client/assets/lua-BbnMAYS6.js +1 -0
- package/dist/client/assets/luau-C-HG3fhB.js +1 -0
- package/dist/client/assets/main-BAHT5yqU.js +81 -0
- package/dist/client/assets/make-CHLpvVh8.js +1 -0
- package/dist/client/assets/markdown-Cvjx9yec.js +1 -0
- package/dist/client/assets/marko-DZsq8hO1.js +1 -0
- package/dist/client/assets/material-theme-D5KoaKCx.js +1 -0
- package/dist/client/assets/material-theme-darker-BfHTSMKl.js +1 -0
- package/dist/client/assets/material-theme-lighter-B0m2ddpp.js +1 -0
- package/dist/client/assets/material-theme-ocean-CyktbL80.js +1 -0
- package/dist/client/assets/material-theme-palenight-Csfq5Kiy.js +1 -0
- package/dist/client/assets/matlab-D7o27uSR.js +1 -0
- package/dist/client/assets/mdc-DUICxH0z.js +1 -0
- package/dist/client/assets/mdx-Cmh6b_Ma.js +1 -0
- package/dist/client/assets/menu-BhVaz8Ly.js +20 -0
- package/dist/client/assets/mermaid-mWjccvbQ.js +1 -0
- package/dist/client/assets/min-dark-CafNBF8u.js +1 -0
- package/dist/client/assets/min-light-CTRr51gU.js +1 -0
- package/dist/client/assets/mipsasm-CKIfxQSi.js +1 -0
- package/dist/client/assets/mojo-B93PlW-d.js +1 -0
- package/dist/client/assets/monokai-D4h5O-jR.js +1 -0
- package/dist/client/assets/moonbit-Ba13S78F.js +1 -0
- package/dist/client/assets/move-Bu9oaDYs.js +1 -0
- package/dist/client/assets/narrat-DRg8JJMk.js +1 -0
- package/dist/client/assets/new-DLlBm66g.js +1 -0
- package/dist/client/assets/nextflow-BrzmwbiE.js +1 -0
- package/dist/client/assets/nginx-DknmC5AR.js +1 -0
- package/dist/client/assets/night-owl-C39BiMTA.js +1 -0
- package/dist/client/assets/nim-CVrawwO9.js +1 -0
- package/dist/client/assets/nix-CwoSXNpI.js +1 -0
- package/dist/client/assets/nord-Ddv68eIx.js +1 -0
- package/dist/client/assets/nushell-C-sUppwS.js +1 -0
- package/dist/client/assets/objective-c-DXmwc3jG.js +1 -0
- package/dist/client/assets/objective-cpp-CLxacb5B.js +1 -0
- package/dist/client/assets/ocaml-C0hk2d4L.js +1 -0
- package/dist/client/assets/one-dark-pro-DVMEJ2y_.js +1 -0
- package/dist/client/assets/one-light-PoHY5YXO.js +1 -0
- package/dist/client/assets/openscad-C4EeE6gA.js +1 -0
- package/dist/client/assets/pascal-D93ZcfNL.js +1 -0
- package/dist/client/assets/perl-C0TMdlhV.js +1 -0
- package/dist/client/assets/php-CDn_0X-4.js +1 -0
- package/dist/client/assets/pkl-u5AG7uiY.js +1 -0
- package/dist/client/assets/plastic-3e1v2bzS.js +1 -0
- package/dist/client/assets/plsql-ChMvpjG-.js +1 -0
- package/dist/client/assets/po-BTJTHyun.js +1 -0
- package/dist/client/assets/poimandres-CS3Unz2-.js +1 -0
- package/dist/client/assets/polar-C0HS_06l.js +1 -0
- package/dist/client/assets/postcss-CXtECtnM.js +1 -0
- package/dist/client/assets/powerquery-CEu0bR-o.js +1 -0
- package/dist/client/assets/powershell-Dpen1YoG.js +1 -0
- package/dist/client/assets/prisma-Dd19v3D-.js +1 -0
- package/dist/client/assets/prolog-CbFg5uaA.js +1 -0
- package/dist/client/assets/proto-C7zT0LnQ.js +1 -0
- package/dist/client/assets/pug-CGlum2m_.js +1 -0
- package/dist/client/assets/puppet-BMWR74SV.js +1 -0
- package/dist/client/assets/purescript-CklMAg4u.js +1 -0
- package/dist/client/assets/python-B6aJPvgy.js +1 -0
- package/dist/client/assets/qml-3beO22l8.js +1 -0
- package/dist/client/assets/qmldir-C8lEn-DE.js +1 -0
- package/dist/client/assets/qss-IeuSbFQv.js +1 -0
- package/dist/client/assets/r-Dspwwk_N.js +1 -0
- package/dist/client/assets/racket-BqYA7rlc.js +1 -0
- package/dist/client/assets/raku-DXvB9xmW.js +1 -0
- package/dist/client/assets/razor-C1TweQQi.js +1 -0
- package/dist/client/assets/red-bN70gL4F.js +1 -0
- package/dist/client/assets/reg-C-SQnVFl.js +1 -0
- package/dist/client/assets/regexp-CDVJQ6XC.js +1 -0
- package/dist/client/assets/rel-C3B-1QV4.js +1 -0
- package/dist/client/assets/riscv-BM1_JUlF.js +1 -0
- package/dist/client/assets/rose-pine-dawn-DHQR4-dF.js +1 -0
- package/dist/client/assets/rose-pine-moon-D4_iv3hh.js +1 -0
- package/dist/client/assets/rose-pine-qdsjHGoJ.js +1 -0
- package/dist/client/assets/rosmsg-BJDFO7_C.js +1 -0
- package/dist/client/assets/rst-B0xPkSld.js +1 -0
- package/dist/client/assets/ruby-BvKwtOVI.js +1 -0
- package/dist/client/assets/rust-B1yitclQ.js +1 -0
- package/dist/client/assets/sas-cz2c8ADy.js +1 -0
- package/dist/client/assets/sass-Cj5Yp3dK.js +1 -0
- package/dist/client/assets/scala-C151Ov-r.js +1 -0
- package/dist/client/assets/scheme-C98Dy4si.js +1 -0
- package/dist/client/assets/scss-OYdSNvt2.js +1 -0
- package/dist/client/assets/sdbl-DVxCFoDh.js +1 -0
- package/dist/client/assets/search-dialog-CHwnOyPS.js +1 -0
- package/dist/client/assets/session-export-dialog-uOlDUzgX.js +1 -0
- package/dist/client/assets/settings-dialog-D0aYDkAM.js +1 -0
- package/dist/client/assets/shaderlab-Dg9Lc6iA.js +1 -0
- package/dist/client/assets/shellscript-Yzrsuije.js +1 -0
- package/dist/client/assets/shellsession-BADoaaVG.js +1 -0
- package/dist/client/assets/slack-dark-BthQWCQV.js +1 -0
- package/dist/client/assets/slack-ochin-DqwNpetd.js +1 -0
- package/dist/client/assets/smalltalk-BERRCDM3.js +1 -0
- package/dist/client/assets/snazzy-light-Bw305WKR.js +1 -0
- package/dist/client/assets/solarized-dark-DXbdFlpD.js +1 -0
- package/dist/client/assets/solarized-light-L9t79GZl.js +1 -0
- package/dist/client/assets/solidity-rGO070M0.js +1 -0
- package/dist/client/assets/soy-Brmx7dQM.js +1 -0
- package/dist/client/assets/sparql-rVzFXLq3.js +1 -0
- package/dist/client/assets/splunk-BtCnVYZw.js +1 -0
- package/dist/client/assets/sql-BLtJtn59.js +1 -0
- package/dist/client/assets/ssh-config-_ykCGR6B.js +1 -0
- package/dist/client/assets/stata-BH5u7GGu.js +1 -0
- package/dist/client/assets/styles-7aVSlb6l.css +1 -0
- package/dist/client/assets/stylus-BEDo0Tqx.js +1 -0
- package/dist/client/assets/svelte-zxCyuUbr.js +1 -0
- package/dist/client/assets/swift-Dg5xB15N.js +1 -0
- package/dist/client/assets/synthwave-84-CbfX1IO0.js +1 -0
- package/dist/client/assets/system-verilog-CnnmHF94.js +1 -0
- package/dist/client/assets/systemd-4A_iFExJ.js +1 -0
- package/dist/client/assets/talonscript-CkByrt1z.js +1 -0
- package/dist/client/assets/tasl-QIJgUcNo.js +1 -0
- package/dist/client/assets/tcl-dwOrl1Do.js +1 -0
- package/dist/client/assets/templ-W15q3VgB.js +1 -0
- package/dist/client/assets/terraform-BETggiCN.js +1 -0
- package/dist/client/assets/tex-CvyZ59Mk.js +1 -0
- package/dist/client/assets/tokyo-night-hegEt444.js +1 -0
- package/dist/client/assets/toml-vGWfd6FD.js +1 -0
- package/dist/client/assets/ts-tags-zn1MmPIZ.js +1 -0
- package/dist/client/assets/tsv-B_m7g4N7.js +1 -0
- package/dist/client/assets/tsx-COt5Ahok.js +1 -0
- package/dist/client/assets/turtle-BsS91CYL.js +1 -0
- package/dist/client/assets/twig-CO9l9SDP.js +1 -0
- package/dist/client/assets/typescript-BPQ3VLAy.js +1 -0
- package/dist/client/assets/typespec-BGHnOYBU.js +1 -0
- package/dist/client/assets/typst-DHCkPAjA.js +1 -0
- package/dist/client/assets/v-BcVCzyr7.js +1 -0
- package/dist/client/assets/vala-CsfeWuGM.js +1 -0
- package/dist/client/assets/vb-D17OF-Vu.js +1 -0
- package/dist/client/assets/verilog-BQ8w6xss.js +1 -0
- package/dist/client/assets/vesper-DU1UobuO.js +1 -0
- package/dist/client/assets/vhdl-CeAyd5Ju.js +1 -0
- package/dist/client/assets/viml-CJc9bBzg.js +1 -0
- package/dist/client/assets/vitesse-black-Bkuqu6BP.js +1 -0
- package/dist/client/assets/vitesse-dark-D0r3Knsf.js +1 -0
- package/dist/client/assets/vitesse-light-CVO1_9PV.js +1 -0
- package/dist/client/assets/vue-DN_0RTcg.js +1 -0
- package/dist/client/assets/vue-html-AaS7Mt5G.js +1 -0
- package/dist/client/assets/vue-vine-CQOfvN7w.js +1 -0
- package/dist/client/assets/vyper-CDx5xZoG.js +1 -0
- package/dist/client/assets/wasm-CG6Dc4jp.js +1 -0
- package/dist/client/assets/wasm-MzD3tlZU.js +1 -0
- package/dist/client/assets/wenyan-BV7otONQ.js +1 -0
- package/dist/client/assets/wgsl-Dx-B1_4e.js +1 -0
- package/dist/client/assets/wikitext-BhOHFoWU.js +1 -0
- package/dist/client/assets/wit-5i3qLPDT.js +1 -0
- package/dist/client/assets/wolfram-lXgVvXCa.js +1 -0
- package/dist/client/assets/xml-sdJ4AIDG.js +1 -0
- package/dist/client/assets/xsl-CtQFsRM5.js +1 -0
- package/dist/client/assets/yaml-Buea-lGh.js +1 -0
- package/dist/client/assets/zenscript-DVFEvuxE.js +1 -0
- package/dist/client/assets/zig-VOosw3JB.js +1 -0
- package/dist/client/cover.jpg +0 -0
- package/dist/client/cover.webp +0 -0
- package/dist/client/favicon.svg +4 -0
- package/dist/client/manifest.json +40 -0
- package/dist/client/pwa-192x192.png +0 -0
- package/dist/client/pwa-512x512.png +0 -0
- package/dist/client/pwa-maskable-512x512.png +0 -0
- package/dist/client/robots.txt +3 -0
- package/dist/client/screenshot-file-explorer.png +0 -0
- package/dist/client/sw.js +165 -0
- package/dist/server/assets/_sessionKey-qlAPF_Ft.js +6130 -0
- package/dist/server/assets/_tanstack-start-manifest_v-El8F-kd-.js +4 -0
- package/dist/server/assets/button-DtQ3rV1m.js +53 -0
- package/dist/server/assets/connect-BWI_6rCm.js +80 -0
- package/dist/server/assets/file-explorer-screen-DJXPEG_J.js +2450 -0
- package/dist/server/assets/files-CONoVTGD.js +11 -0
- package/dist/server/assets/index-Bp8QskbI.js +28 -0
- package/dist/server/assets/index-COu2idHm.js +226 -0
- package/dist/server/assets/keyboard-shortcuts-dialog-D5hqVX2v.js +69 -0
- package/dist/server/assets/menu-D6n4DB0U.js +156 -0
- package/dist/server/assets/new-Dzk5YxE9.js +6 -0
- package/dist/server/assets/router-DbxyvprK.js +2749 -0
- package/dist/server/assets/search-dialog-JY8C3mqa.js +443 -0
- package/dist/server/assets/session-export-dialog-D87uafPD.js +80 -0
- package/dist/server/assets/settings-dialog-CBj7njLM.js +568 -0
- package/dist/server/assets/start-HYkvq4Ni.js +4 -0
- package/dist/server/server.js +1013 -0
- package/package.json +93 -0
|
@@ -0,0 +1,2749 @@
|
|
|
1
|
+
import { createRootRoute, Outlet, HeadContent, Scripts, createFileRoute, lazyRouteComponent, redirect, createRouter } from "@tanstack/react-router";
|
|
2
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { QueryClientProvider, QueryClient } from "@tanstack/react-query";
|
|
4
|
+
import { randomUUID } from "node:crypto";
|
|
5
|
+
import WebSocket from "ws";
|
|
6
|
+
import { json } from "@tanstack/router-core/ssr/client";
|
|
7
|
+
import { readFile, mkdir, writeFile, rename, stat, readdir, rm, realpath, lstat } from "node:fs/promises";
|
|
8
|
+
import os from "node:os";
|
|
9
|
+
import path, { join, resolve, relative, extname } from "node:path";
|
|
10
|
+
import { posix } from "path";
|
|
11
|
+
const appCss = "/assets/styles-7aVSlb6l.css";
|
|
12
|
+
const swRegisterScript = `
|
|
13
|
+
(() => {
|
|
14
|
+
// Skip PWA service worker inside Capacitor native shell — they conflict
|
|
15
|
+
// with the native networking layer and caching.
|
|
16
|
+
if (window.Capacitor && window.Capacitor.isNativePlatform && window.Capacitor.isNativePlatform()) return;
|
|
17
|
+
if ('serviceWorker' in navigator) {
|
|
18
|
+
window.addEventListener('load', () => {
|
|
19
|
+
navigator.serviceWorker.register('/sw.js', { scope: '/' })
|
|
20
|
+
.then((reg) => {
|
|
21
|
+
// Auto-update: check for updates periodically
|
|
22
|
+
setInterval(() => reg.update(), 60 * 60 * 1000); // every hour
|
|
23
|
+
reg.addEventListener('updatefound', () => {
|
|
24
|
+
const newWorker = reg.installing;
|
|
25
|
+
if (newWorker) {
|
|
26
|
+
newWorker.addEventListener('statechange', () => {
|
|
27
|
+
if (newWorker.state === 'activated') {
|
|
28
|
+
// New version activated, could notify user here
|
|
29
|
+
console.log('[SW] New version activated');
|
|
30
|
+
}
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
});
|
|
34
|
+
})
|
|
35
|
+
.catch((err) => console.warn('[SW] Registration failed:', err));
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
})()
|
|
39
|
+
`;
|
|
40
|
+
const themeScript = `
|
|
41
|
+
(() => {
|
|
42
|
+
try {
|
|
43
|
+
const stored = localStorage.getItem('chat-settings')
|
|
44
|
+
let theme = 'system'
|
|
45
|
+
if (stored) {
|
|
46
|
+
const parsed = JSON.parse(stored)
|
|
47
|
+
const storedTheme = parsed?.state?.settings?.theme
|
|
48
|
+
if (storedTheme === 'light' || storedTheme === 'dark' || storedTheme === 'system' || storedTheme === 'chameleon') {
|
|
49
|
+
theme = storedTheme
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
const root = document.documentElement
|
|
53
|
+
const media = window.matchMedia('(prefers-color-scheme: dark)')
|
|
54
|
+
const apply = () => {
|
|
55
|
+
root.classList.remove('light', 'dark', 'system', 'chameleon')
|
|
56
|
+
root.classList.add(theme)
|
|
57
|
+
if (theme === 'system' && media.matches) {
|
|
58
|
+
root.classList.add('dark')
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
apply()
|
|
62
|
+
media.addEventListener('change', () => {
|
|
63
|
+
if (theme === 'system') apply()
|
|
64
|
+
})
|
|
65
|
+
} catch {}
|
|
66
|
+
})()
|
|
67
|
+
`;
|
|
68
|
+
const textSizeScript = `
|
|
69
|
+
(() => {
|
|
70
|
+
try {
|
|
71
|
+
const stored = localStorage.getItem('opencami-text-size')
|
|
72
|
+
const allowed = new Set(['14px', '16px', '18px', '20px'])
|
|
73
|
+
const value = allowed.has(stored) ? stored : '16px'
|
|
74
|
+
document.documentElement.style.setProperty('--opencami-text-size', value)
|
|
75
|
+
} catch {}
|
|
76
|
+
})()
|
|
77
|
+
`;
|
|
78
|
+
const Route$p = createRootRoute({
|
|
79
|
+
head: () => ({
|
|
80
|
+
meta: [
|
|
81
|
+
{
|
|
82
|
+
charSet: "utf-8"
|
|
83
|
+
},
|
|
84
|
+
{
|
|
85
|
+
name: "viewport",
|
|
86
|
+
content: "width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no, viewport-fit=cover"
|
|
87
|
+
},
|
|
88
|
+
{
|
|
89
|
+
title: "OpenCami"
|
|
90
|
+
},
|
|
91
|
+
{
|
|
92
|
+
name: "description",
|
|
93
|
+
content: "a fast web client for OpenClaw"
|
|
94
|
+
},
|
|
95
|
+
{
|
|
96
|
+
property: "og:image",
|
|
97
|
+
content: "/cover.webp"
|
|
98
|
+
},
|
|
99
|
+
{
|
|
100
|
+
property: "og:image:type",
|
|
101
|
+
content: "image/webp"
|
|
102
|
+
},
|
|
103
|
+
{
|
|
104
|
+
name: "twitter:card",
|
|
105
|
+
content: "summary_large_image"
|
|
106
|
+
},
|
|
107
|
+
{
|
|
108
|
+
name: "twitter:image",
|
|
109
|
+
content: "/cover.webp"
|
|
110
|
+
},
|
|
111
|
+
// PWA - Theme color
|
|
112
|
+
{
|
|
113
|
+
name: "theme-color",
|
|
114
|
+
content: "#0a0a0a"
|
|
115
|
+
},
|
|
116
|
+
// PWA - Apple iOS meta tags
|
|
117
|
+
{
|
|
118
|
+
name: "apple-mobile-web-app-capable",
|
|
119
|
+
content: "yes"
|
|
120
|
+
},
|
|
121
|
+
{
|
|
122
|
+
name: "apple-mobile-web-app-status-bar-style",
|
|
123
|
+
content: "black-translucent"
|
|
124
|
+
},
|
|
125
|
+
{
|
|
126
|
+
name: "apple-mobile-web-app-title",
|
|
127
|
+
content: "OpenCami"
|
|
128
|
+
},
|
|
129
|
+
// PWA - Android meta tags
|
|
130
|
+
{
|
|
131
|
+
name: "mobile-web-app-capable",
|
|
132
|
+
content: "yes"
|
|
133
|
+
}
|
|
134
|
+
],
|
|
135
|
+
links: [
|
|
136
|
+
{
|
|
137
|
+
rel: "stylesheet",
|
|
138
|
+
href: appCss
|
|
139
|
+
},
|
|
140
|
+
{
|
|
141
|
+
rel: "icon",
|
|
142
|
+
type: "image/svg+xml",
|
|
143
|
+
href: "/favicon.svg"
|
|
144
|
+
},
|
|
145
|
+
// PWA - Apple touch icon
|
|
146
|
+
{
|
|
147
|
+
rel: "apple-touch-icon",
|
|
148
|
+
href: "/apple-touch-icon-180x180.png"
|
|
149
|
+
},
|
|
150
|
+
// PWA - Manifest
|
|
151
|
+
{
|
|
152
|
+
rel: "manifest",
|
|
153
|
+
href: "/manifest.json"
|
|
154
|
+
}
|
|
155
|
+
]
|
|
156
|
+
}),
|
|
157
|
+
shellComponent: RootDocument,
|
|
158
|
+
component: RootLayout
|
|
159
|
+
});
|
|
160
|
+
const queryClient = new QueryClient();
|
|
161
|
+
function RootLayout() {
|
|
162
|
+
return /* @__PURE__ */ jsx(QueryClientProvider, { client: queryClient, children: /* @__PURE__ */ jsx(Outlet, {}) });
|
|
163
|
+
}
|
|
164
|
+
function RootDocument({ children }) {
|
|
165
|
+
return /* @__PURE__ */ jsxs("html", { lang: "en", suppressHydrationWarning: true, children: [
|
|
166
|
+
/* @__PURE__ */ jsxs("head", { children: [
|
|
167
|
+
/* @__PURE__ */ jsx("script", { dangerouslySetInnerHTML: { __html: textSizeScript } }),
|
|
168
|
+
/* @__PURE__ */ jsx("script", { dangerouslySetInnerHTML: { __html: themeScript } }),
|
|
169
|
+
/* @__PURE__ */ jsx("script", { dangerouslySetInnerHTML: { __html: swRegisterScript } }),
|
|
170
|
+
/* @__PURE__ */ jsx(HeadContent, {})
|
|
171
|
+
] }),
|
|
172
|
+
/* @__PURE__ */ jsxs("body", { children: [
|
|
173
|
+
/* @__PURE__ */ jsx("div", { className: "root", children }),
|
|
174
|
+
/* @__PURE__ */ jsx(Scripts, {})
|
|
175
|
+
] })
|
|
176
|
+
] });
|
|
177
|
+
}
|
|
178
|
+
const $$splitComponentImporter$4 = () => import("./new-Dzk5YxE9.js");
|
|
179
|
+
const Route$o = createFileRoute("/new")({
|
|
180
|
+
beforeLoad: function redirectToNewChat() {
|
|
181
|
+
throw redirect({
|
|
182
|
+
to: "/chat/$sessionKey",
|
|
183
|
+
params: {
|
|
184
|
+
sessionKey: "new"
|
|
185
|
+
},
|
|
186
|
+
replace: true
|
|
187
|
+
});
|
|
188
|
+
},
|
|
189
|
+
component: lazyRouteComponent($$splitComponentImporter$4, "component")
|
|
190
|
+
});
|
|
191
|
+
const $$splitComponentImporter$3 = () => import("./files-CONoVTGD.js");
|
|
192
|
+
const Route$n = createFileRoute("/files")({
|
|
193
|
+
component: lazyRouteComponent($$splitComponentImporter$3, "component")
|
|
194
|
+
});
|
|
195
|
+
const $$splitComponentImporter$2 = () => import("./connect-BWI_6rCm.js");
|
|
196
|
+
const Route$m = createFileRoute("/connect")({
|
|
197
|
+
component: lazyRouteComponent($$splitComponentImporter$2, "component")
|
|
198
|
+
});
|
|
199
|
+
const $$splitComponentImporter$1 = () => import("./index-Bp8QskbI.js");
|
|
200
|
+
const Route$l = createFileRoute("/")({
|
|
201
|
+
component: lazyRouteComponent($$splitComponentImporter$1, "component")
|
|
202
|
+
});
|
|
203
|
+
const $$splitComponentImporter = () => import("./_sessionKey-qlAPF_Ft.js").then((n) => n.$);
|
|
204
|
+
const Route$k = createFileRoute("/chat/$sessionKey")({
|
|
205
|
+
component: lazyRouteComponent($$splitComponentImporter, "component")
|
|
206
|
+
});
|
|
207
|
+
function getGatewayConfig() {
|
|
208
|
+
const url = process.env.CLAWDBOT_GATEWAY_URL?.trim() || "ws://127.0.0.1:18789";
|
|
209
|
+
const token = process.env.CLAWDBOT_GATEWAY_TOKEN?.trim() || "";
|
|
210
|
+
const password = process.env.CLAWDBOT_GATEWAY_PASSWORD?.trim() || "";
|
|
211
|
+
if (!token && !password) {
|
|
212
|
+
throw new Error(
|
|
213
|
+
"Missing gateway auth. Set CLAWDBOT_GATEWAY_TOKEN (recommended) or CLAWDBOT_GATEWAY_PASSWORD in the server environment."
|
|
214
|
+
);
|
|
215
|
+
}
|
|
216
|
+
return { url, token, password };
|
|
217
|
+
}
|
|
218
|
+
function buildConnectParams(token, password) {
|
|
219
|
+
return {
|
|
220
|
+
minProtocol: 3,
|
|
221
|
+
maxProtocol: 3,
|
|
222
|
+
client: {
|
|
223
|
+
id: "gateway-client",
|
|
224
|
+
displayName: "webclaw",
|
|
225
|
+
version: "dev",
|
|
226
|
+
platform: process.platform,
|
|
227
|
+
mode: "ui",
|
|
228
|
+
instanceId: randomUUID()
|
|
229
|
+
},
|
|
230
|
+
auth: {
|
|
231
|
+
token: token || void 0,
|
|
232
|
+
password: password || void 0
|
|
233
|
+
},
|
|
234
|
+
role: "operator",
|
|
235
|
+
scopes: ["operator.admin"]
|
|
236
|
+
};
|
|
237
|
+
}
|
|
238
|
+
class PersistentGatewayConnection {
|
|
239
|
+
ws = null;
|
|
240
|
+
connected = false;
|
|
241
|
+
connectPromise = null;
|
|
242
|
+
pendingRpcs = /* @__PURE__ */ new Map();
|
|
243
|
+
reconnectTimer = null;
|
|
244
|
+
reconnectDelay = 1e3;
|
|
245
|
+
maxReconnectDelay = 3e4;
|
|
246
|
+
destroyed = false;
|
|
247
|
+
// Event listeners keyed by sessionKey — each sessionKey can have multiple listeners
|
|
248
|
+
sessionListeners = /* @__PURE__ */ new Map();
|
|
249
|
+
// Listeners that receive ALL events (for debugging or global subscriptions)
|
|
250
|
+
globalListeners = /* @__PURE__ */ new Set();
|
|
251
|
+
// Event buffer: store recent events per sessionKey so late subscribers don't miss them
|
|
252
|
+
eventBuffer = /* @__PURE__ */ new Map();
|
|
253
|
+
get isConnected() {
|
|
254
|
+
return this.connected && this.ws?.readyState === WebSocket.OPEN;
|
|
255
|
+
}
|
|
256
|
+
/** Ensure the persistent connection is up and authenticated. */
|
|
257
|
+
async ensureConnected() {
|
|
258
|
+
if (this.isConnected) return;
|
|
259
|
+
if (this.connectPromise) return this.connectPromise;
|
|
260
|
+
this.connectPromise = this._connect();
|
|
261
|
+
try {
|
|
262
|
+
await this.connectPromise;
|
|
263
|
+
} finally {
|
|
264
|
+
this.connectPromise = null;
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
async _connect() {
|
|
268
|
+
if (this.destroyed) throw new Error("Connection destroyed");
|
|
269
|
+
const { url, token, password } = getGatewayConfig();
|
|
270
|
+
const ws = new WebSocket(url);
|
|
271
|
+
this.ws = ws;
|
|
272
|
+
await new Promise((resolve2, reject) => {
|
|
273
|
+
const onOpen = () => {
|
|
274
|
+
cleanup();
|
|
275
|
+
resolve2();
|
|
276
|
+
};
|
|
277
|
+
const onError = (err) => {
|
|
278
|
+
cleanup();
|
|
279
|
+
reject(new Error(`WS open error: ${err.message}`));
|
|
280
|
+
};
|
|
281
|
+
const cleanup = () => {
|
|
282
|
+
ws.off("open", onOpen);
|
|
283
|
+
ws.off("error", onError);
|
|
284
|
+
};
|
|
285
|
+
ws.on("open", onOpen);
|
|
286
|
+
ws.on("error", onError);
|
|
287
|
+
});
|
|
288
|
+
ws.on("message", (data) => this._onMessage(data));
|
|
289
|
+
ws.on("close", () => this._onClose());
|
|
290
|
+
ws.on("error", () => {
|
|
291
|
+
});
|
|
292
|
+
const connectId = randomUUID();
|
|
293
|
+
const connectParams = buildConnectParams(token, password);
|
|
294
|
+
ws.send(JSON.stringify({
|
|
295
|
+
type: "req",
|
|
296
|
+
id: connectId,
|
|
297
|
+
method: "connect",
|
|
298
|
+
params: connectParams
|
|
299
|
+
}));
|
|
300
|
+
await this._waitForRes(connectId, 1e4);
|
|
301
|
+
this.connected = true;
|
|
302
|
+
this.reconnectDelay = 1e3;
|
|
303
|
+
console.log("[gateway-ws] Persistent connection established");
|
|
304
|
+
}
|
|
305
|
+
_onMessage(data) {
|
|
306
|
+
try {
|
|
307
|
+
const str = typeof data === "string" ? data : data.toString();
|
|
308
|
+
const parsed = JSON.parse(str);
|
|
309
|
+
if (parsed.type === "res") {
|
|
310
|
+
const pending = this.pendingRpcs.get(parsed.id);
|
|
311
|
+
if (pending) {
|
|
312
|
+
this.pendingRpcs.delete(parsed.id);
|
|
313
|
+
clearTimeout(pending.timer);
|
|
314
|
+
if (parsed.ok) {
|
|
315
|
+
pending.resolve(parsed.payload);
|
|
316
|
+
} else {
|
|
317
|
+
pending.reject(new Error(parsed.error?.message ?? "gateway error"));
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
return;
|
|
321
|
+
}
|
|
322
|
+
if (parsed.type === "event") {
|
|
323
|
+
const event = {
|
|
324
|
+
event: parsed.event,
|
|
325
|
+
payload: parsed.payload ?? {},
|
|
326
|
+
seq: parsed.seq
|
|
327
|
+
};
|
|
328
|
+
const sessionKey = this._extractSessionKey(event);
|
|
329
|
+
for (const listener of this.globalListeners) {
|
|
330
|
+
try {
|
|
331
|
+
listener(event);
|
|
332
|
+
} catch {
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
if (sessionKey) {
|
|
336
|
+
const listeners = this.sessionListeners.get(sessionKey);
|
|
337
|
+
if (listeners && listeners.size > 0) {
|
|
338
|
+
for (const listener of listeners) {
|
|
339
|
+
try {
|
|
340
|
+
listener(event);
|
|
341
|
+
} catch {
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
} else {
|
|
345
|
+
let buf = this.eventBuffer.get(sessionKey);
|
|
346
|
+
if (!buf) {
|
|
347
|
+
const timer = setTimeout(() => {
|
|
348
|
+
this.eventBuffer.delete(sessionKey);
|
|
349
|
+
}, 1e4);
|
|
350
|
+
buf = { events: [], timer };
|
|
351
|
+
this.eventBuffer.set(sessionKey, buf);
|
|
352
|
+
}
|
|
353
|
+
buf.events.push(event);
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
} catch {
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
_extractSessionKey(event) {
|
|
361
|
+
const payload = event.payload;
|
|
362
|
+
if (typeof payload?.sessionKey === "string") return payload.sessionKey;
|
|
363
|
+
if (typeof payload?.session === "string") return payload.session;
|
|
364
|
+
if (payload?.data && typeof payload.data?.sessionKey === "string") {
|
|
365
|
+
return payload.data.sessionKey;
|
|
366
|
+
}
|
|
367
|
+
return null;
|
|
368
|
+
}
|
|
369
|
+
_onClose() {
|
|
370
|
+
const wasConnected = this.connected;
|
|
371
|
+
this.connected = false;
|
|
372
|
+
this.ws = null;
|
|
373
|
+
for (const [, pending] of this.pendingRpcs) {
|
|
374
|
+
clearTimeout(pending.timer);
|
|
375
|
+
pending.reject(new Error("Connection closed"));
|
|
376
|
+
}
|
|
377
|
+
this.pendingRpcs.clear();
|
|
378
|
+
if (wasConnected) {
|
|
379
|
+
console.log("[gateway-ws] Connection lost, scheduling reconnect...");
|
|
380
|
+
}
|
|
381
|
+
if (!this.destroyed) {
|
|
382
|
+
this._scheduleReconnect();
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
_scheduleReconnect() {
|
|
386
|
+
if (this.reconnectTimer) return;
|
|
387
|
+
const delay = this.reconnectDelay;
|
|
388
|
+
this.reconnectDelay = Math.min(this.reconnectDelay * 2, this.maxReconnectDelay);
|
|
389
|
+
console.log(`[gateway-ws] Reconnecting in ${delay}ms...`);
|
|
390
|
+
this.reconnectTimer = setTimeout(async () => {
|
|
391
|
+
this.reconnectTimer = null;
|
|
392
|
+
try {
|
|
393
|
+
await this.ensureConnected();
|
|
394
|
+
} catch (err) {
|
|
395
|
+
console.error("[gateway-ws] Reconnect failed:", err instanceof Error ? err.message : err);
|
|
396
|
+
}
|
|
397
|
+
}, delay);
|
|
398
|
+
}
|
|
399
|
+
_waitForRes(id, timeoutMs = 3e4) {
|
|
400
|
+
return new Promise((resolve2, reject) => {
|
|
401
|
+
const timer = setTimeout(() => {
|
|
402
|
+
this.pendingRpcs.delete(id);
|
|
403
|
+
reject(new Error(`RPC timeout waiting for ${id}`));
|
|
404
|
+
}, timeoutMs);
|
|
405
|
+
this.pendingRpcs.set(id, { resolve: resolve2, reject, timer });
|
|
406
|
+
});
|
|
407
|
+
}
|
|
408
|
+
/** Send an RPC request over the persistent connection. */
|
|
409
|
+
async rpc(method, params, timeoutMs = 3e4) {
|
|
410
|
+
await this.ensureConnected();
|
|
411
|
+
const id = randomUUID();
|
|
412
|
+
const frame = { type: "req", id, method, params };
|
|
413
|
+
this.ws.send(JSON.stringify(frame));
|
|
414
|
+
const payload = await this._waitForRes(id, timeoutMs);
|
|
415
|
+
return payload;
|
|
416
|
+
}
|
|
417
|
+
/** Subscribe to events for a specific sessionKey. Returns an unsubscribe function. */
|
|
418
|
+
subscribe(sessionKey, listener) {
|
|
419
|
+
let listeners = this.sessionListeners.get(sessionKey);
|
|
420
|
+
if (!listeners) {
|
|
421
|
+
listeners = /* @__PURE__ */ new Set();
|
|
422
|
+
this.sessionListeners.set(sessionKey, listeners);
|
|
423
|
+
}
|
|
424
|
+
listeners.add(listener);
|
|
425
|
+
const buf = this.eventBuffer.get(sessionKey);
|
|
426
|
+
if (buf) {
|
|
427
|
+
this.eventBuffer.delete(sessionKey);
|
|
428
|
+
clearTimeout(buf.timer);
|
|
429
|
+
for (const event of buf.events) {
|
|
430
|
+
try {
|
|
431
|
+
listener(event);
|
|
432
|
+
} catch {
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
return () => {
|
|
437
|
+
listeners.delete(listener);
|
|
438
|
+
if (listeners.size === 0) {
|
|
439
|
+
this.sessionListeners.delete(sessionKey);
|
|
440
|
+
}
|
|
441
|
+
};
|
|
442
|
+
}
|
|
443
|
+
/** Subscribe to ALL events. Returns an unsubscribe function. */
|
|
444
|
+
subscribeAll(listener) {
|
|
445
|
+
this.globalListeners.add(listener);
|
|
446
|
+
return () => {
|
|
447
|
+
this.globalListeners.delete(listener);
|
|
448
|
+
};
|
|
449
|
+
}
|
|
450
|
+
destroy() {
|
|
451
|
+
this.destroyed = true;
|
|
452
|
+
if (this.reconnectTimer) {
|
|
453
|
+
clearTimeout(this.reconnectTimer);
|
|
454
|
+
this.reconnectTimer = null;
|
|
455
|
+
}
|
|
456
|
+
if (this.ws) {
|
|
457
|
+
this.ws.close();
|
|
458
|
+
this.ws = null;
|
|
459
|
+
}
|
|
460
|
+
this.connected = false;
|
|
461
|
+
this.sessionListeners.clear();
|
|
462
|
+
this.globalListeners.clear();
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
let _instance = null;
|
|
466
|
+
function getPersistentConnection() {
|
|
467
|
+
if (!_instance) {
|
|
468
|
+
_instance = new PersistentGatewayConnection();
|
|
469
|
+
}
|
|
470
|
+
return _instance;
|
|
471
|
+
}
|
|
472
|
+
async function gatewayRpc(method, params) {
|
|
473
|
+
const conn = getPersistentConnection();
|
|
474
|
+
return conn.rpc(method, params);
|
|
475
|
+
}
|
|
476
|
+
function subscribeGatewayEvents(sessionKey, listener) {
|
|
477
|
+
const conn = getPersistentConnection();
|
|
478
|
+
return conn.subscribe(sessionKey, listener);
|
|
479
|
+
}
|
|
480
|
+
async function gatewayConnectCheck() {
|
|
481
|
+
const conn = getPersistentConnection();
|
|
482
|
+
await conn.ensureConnected();
|
|
483
|
+
}
|
|
484
|
+
async function ttsElevenLabs(text, apiKey, voice) {
|
|
485
|
+
const voiceId = voice || "21m00Tcm4TlvDq8ikWAM";
|
|
486
|
+
const res = await fetch(
|
|
487
|
+
`https://api.elevenlabs.io/v1/text-to-speech/${voiceId}`,
|
|
488
|
+
{
|
|
489
|
+
method: "POST",
|
|
490
|
+
headers: {
|
|
491
|
+
"xi-api-key": apiKey,
|
|
492
|
+
"Content-Type": "application/json",
|
|
493
|
+
Accept: "audio/mpeg"
|
|
494
|
+
},
|
|
495
|
+
body: JSON.stringify({
|
|
496
|
+
text: text.substring(0, 5e3),
|
|
497
|
+
model_id: "eleven_multilingual_v2",
|
|
498
|
+
voice_settings: {
|
|
499
|
+
stability: 0.5,
|
|
500
|
+
similarity_boost: 0.75
|
|
501
|
+
}
|
|
502
|
+
})
|
|
503
|
+
}
|
|
504
|
+
);
|
|
505
|
+
if (!res.ok) {
|
|
506
|
+
const err = await res.text();
|
|
507
|
+
throw new Error(`ElevenLabs error: ${err}`);
|
|
508
|
+
}
|
|
509
|
+
const audioBuffer = await res.arrayBuffer();
|
|
510
|
+
return new Response(audioBuffer, {
|
|
511
|
+
headers: {
|
|
512
|
+
"Content-Type": "audio/mpeg",
|
|
513
|
+
"Cache-Control": "public, max-age=3600"
|
|
514
|
+
}
|
|
515
|
+
});
|
|
516
|
+
}
|
|
517
|
+
async function ttsOpenAI(text, apiKey, voice) {
|
|
518
|
+
const res = await fetch("https://api.openai.com/v1/audio/speech", {
|
|
519
|
+
method: "POST",
|
|
520
|
+
headers: {
|
|
521
|
+
Authorization: `Bearer ${apiKey}`,
|
|
522
|
+
"Content-Type": "application/json"
|
|
523
|
+
},
|
|
524
|
+
body: JSON.stringify({
|
|
525
|
+
model: "tts-1",
|
|
526
|
+
input: text.substring(0, 4096),
|
|
527
|
+
voice: voice || "nova",
|
|
528
|
+
response_format: "mp3"
|
|
529
|
+
})
|
|
530
|
+
});
|
|
531
|
+
if (!res.ok) {
|
|
532
|
+
const err = await res.text();
|
|
533
|
+
throw new Error(`OpenAI TTS error: ${err}`);
|
|
534
|
+
}
|
|
535
|
+
const audioBuffer = await res.arrayBuffer();
|
|
536
|
+
return new Response(audioBuffer, {
|
|
537
|
+
headers: {
|
|
538
|
+
"Content-Type": "audio/mpeg",
|
|
539
|
+
"Cache-Control": "public, max-age=3600"
|
|
540
|
+
}
|
|
541
|
+
});
|
|
542
|
+
}
|
|
543
|
+
async function ttsEdge(text, voice) {
|
|
544
|
+
const { EdgeTTS } = await import("node-edge-tts");
|
|
545
|
+
const tts = new EdgeTTS();
|
|
546
|
+
await tts.synthesize(
|
|
547
|
+
text.substring(0, 5e3),
|
|
548
|
+
voice || "en-US-AriaNeural",
|
|
549
|
+
{}
|
|
550
|
+
);
|
|
551
|
+
const audioBuffer = await tts.toBuffer();
|
|
552
|
+
return new Response(audioBuffer, {
|
|
553
|
+
headers: {
|
|
554
|
+
"Content-Type": "audio/mpeg",
|
|
555
|
+
"Cache-Control": "public, max-age=3600"
|
|
556
|
+
}
|
|
557
|
+
});
|
|
558
|
+
}
|
|
559
|
+
const Route$j = createFileRoute("/api/tts")({
|
|
560
|
+
server: {
|
|
561
|
+
handlers: {
|
|
562
|
+
POST: async ({ request }) => {
|
|
563
|
+
try {
|
|
564
|
+
const body = await request.json().catch(() => ({}));
|
|
565
|
+
const text = typeof body.text === "string" ? body.text.trim() : "";
|
|
566
|
+
const voice = typeof body.voice === "string" ? body.voice.trim() : "";
|
|
567
|
+
const provider = typeof body.provider === "string" ? body.provider.trim() : "";
|
|
568
|
+
if (!text) {
|
|
569
|
+
return json(
|
|
570
|
+
{ ok: false, error: "No text provided" },
|
|
571
|
+
{ status: 400 }
|
|
572
|
+
);
|
|
573
|
+
}
|
|
574
|
+
const configRes = await gatewayRpc("config.get", {});
|
|
575
|
+
const ttsConfig = configRes?.config?.messages?.tts;
|
|
576
|
+
const env = configRes?.config?.env || {};
|
|
577
|
+
const elevenLabsKey = ttsConfig?.elevenlabs?.apiKey || env.ELEVENLABS_API_KEY || env.XI_API_KEY;
|
|
578
|
+
const openaiKey = ttsConfig?.openai?.apiKey || env.OPENAI_API_KEY;
|
|
579
|
+
const preferredProvider = provider || ttsConfig?.provider || "auto";
|
|
580
|
+
const errors = [];
|
|
581
|
+
if (preferredProvider === "elevenlabs" && elevenLabsKey) {
|
|
582
|
+
try {
|
|
583
|
+
return await ttsElevenLabs(text, elevenLabsKey, voice);
|
|
584
|
+
} catch (e) {
|
|
585
|
+
errors.push(
|
|
586
|
+
e instanceof Error ? e.message : String(e)
|
|
587
|
+
);
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
if (preferredProvider === "openai" && openaiKey) {
|
|
591
|
+
try {
|
|
592
|
+
return await ttsOpenAI(text, openaiKey, voice);
|
|
593
|
+
} catch (e) {
|
|
594
|
+
errors.push(
|
|
595
|
+
e instanceof Error ? e.message : String(e)
|
|
596
|
+
);
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
if (preferredProvider === "edge") {
|
|
600
|
+
try {
|
|
601
|
+
return await ttsEdge(text, voice);
|
|
602
|
+
} catch (e) {
|
|
603
|
+
errors.push(
|
|
604
|
+
e instanceof Error ? e.message : String(e)
|
|
605
|
+
);
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
if (preferredProvider === "auto" || errors.length > 0) {
|
|
609
|
+
if (elevenLabsKey) {
|
|
610
|
+
try {
|
|
611
|
+
return await ttsElevenLabs(
|
|
612
|
+
text,
|
|
613
|
+
elevenLabsKey,
|
|
614
|
+
voice
|
|
615
|
+
);
|
|
616
|
+
} catch (e) {
|
|
617
|
+
errors.push(
|
|
618
|
+
e instanceof Error ? e.message : String(e)
|
|
619
|
+
);
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
if (openaiKey) {
|
|
623
|
+
try {
|
|
624
|
+
return await ttsOpenAI(text, openaiKey, voice);
|
|
625
|
+
} catch (e) {
|
|
626
|
+
errors.push(
|
|
627
|
+
e instanceof Error ? e.message : String(e)
|
|
628
|
+
);
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
try {
|
|
632
|
+
return await ttsEdge(text, voice);
|
|
633
|
+
} catch (e) {
|
|
634
|
+
errors.push(
|
|
635
|
+
e instanceof Error ? e.message : String(e)
|
|
636
|
+
);
|
|
637
|
+
}
|
|
638
|
+
}
|
|
639
|
+
return json(
|
|
640
|
+
{
|
|
641
|
+
ok: false,
|
|
642
|
+
error: `All TTS providers failed: ${errors.join("; ")}`
|
|
643
|
+
},
|
|
644
|
+
{ status: 502 }
|
|
645
|
+
);
|
|
646
|
+
} catch (err) {
|
|
647
|
+
return json(
|
|
648
|
+
{
|
|
649
|
+
ok: false,
|
|
650
|
+
error: err instanceof Error ? err.message : String(err)
|
|
651
|
+
},
|
|
652
|
+
{ status: 500 }
|
|
653
|
+
);
|
|
654
|
+
}
|
|
655
|
+
}
|
|
656
|
+
}
|
|
657
|
+
}
|
|
658
|
+
});
|
|
659
|
+
const Route$i = createFileRoute("/api/stream")({
|
|
660
|
+
server: {
|
|
661
|
+
handlers: {
|
|
662
|
+
GET: async ({ request }) => {
|
|
663
|
+
const url = new URL(request.url);
|
|
664
|
+
const sessionKey = url.searchParams.get("sessionKey");
|
|
665
|
+
if (!sessionKey) {
|
|
666
|
+
return new Response(
|
|
667
|
+
JSON.stringify({ ok: false, error: "sessionKey required" }),
|
|
668
|
+
{ status: 400, headers: { "content-type": "application/json" } }
|
|
669
|
+
);
|
|
670
|
+
}
|
|
671
|
+
const encoder = new TextEncoder();
|
|
672
|
+
let unsubscribe = null;
|
|
673
|
+
let closed = false;
|
|
674
|
+
const stream = new ReadableStream({
|
|
675
|
+
start(controller) {
|
|
676
|
+
controller.enqueue(encoder.encode(": connected\n\n"));
|
|
677
|
+
function sendSSE(event, data) {
|
|
678
|
+
if (closed) return;
|
|
679
|
+
try {
|
|
680
|
+
controller.enqueue(
|
|
681
|
+
encoder.encode(`event: ${event}
|
|
682
|
+
data: ${JSON.stringify(data)}
|
|
683
|
+
|
|
684
|
+
`)
|
|
685
|
+
);
|
|
686
|
+
} catch {
|
|
687
|
+
}
|
|
688
|
+
}
|
|
689
|
+
let gotAgentStream = false;
|
|
690
|
+
unsubscribe = subscribeGatewayEvents(sessionKey, (evt) => {
|
|
691
|
+
if (evt.event === "agent") {
|
|
692
|
+
const payload = evt.payload;
|
|
693
|
+
const agentStream = payload.stream;
|
|
694
|
+
if (agentStream === "assistant") {
|
|
695
|
+
gotAgentStream = true;
|
|
696
|
+
const data = payload.data ?? payload;
|
|
697
|
+
const text = typeof data.delta === "string" ? data.delta : typeof data.text === "string" ? data.text : typeof payload.text === "string" ? payload.text : typeof payload.delta === "string" ? payload.delta : "";
|
|
698
|
+
if (text) {
|
|
699
|
+
sendSSE("delta", { text, sessionKey });
|
|
700
|
+
}
|
|
701
|
+
} else if (agentStream === "tool") {
|
|
702
|
+
gotAgentStream = true;
|
|
703
|
+
const tdata = payload.data ?? payload;
|
|
704
|
+
sendSSE("tool", {
|
|
705
|
+
name: tdata.name ?? tdata.toolName ?? payload.name ?? "",
|
|
706
|
+
status: tdata.phase ?? tdata.status ?? payload.phase ?? "running",
|
|
707
|
+
id: tdata.id ?? tdata.toolCallId ?? payload.id ?? "",
|
|
708
|
+
sessionKey
|
|
709
|
+
});
|
|
710
|
+
} else if (agentStream === "lifecycle") {
|
|
711
|
+
const ldata = payload.data ?? payload;
|
|
712
|
+
const phase = ldata.phase ?? payload.phase;
|
|
713
|
+
if (phase === "end" || phase === "error") {
|
|
714
|
+
sendSSE("done", {
|
|
715
|
+
sessionKey,
|
|
716
|
+
status: phase,
|
|
717
|
+
error: phase === "error" ? payload.error : void 0
|
|
718
|
+
});
|
|
719
|
+
}
|
|
720
|
+
}
|
|
721
|
+
} else if (evt.event === "chat") {
|
|
722
|
+
const payload = evt.payload;
|
|
723
|
+
const kind = payload.kind;
|
|
724
|
+
if (kind === "delta" && !gotAgentStream) {
|
|
725
|
+
const text = typeof payload.text === "string" ? payload.text : typeof payload.delta === "string" ? payload.delta : "";
|
|
726
|
+
if (text) {
|
|
727
|
+
sendSSE("delta", { text, sessionKey });
|
|
728
|
+
}
|
|
729
|
+
} else if (kind === "final") {
|
|
730
|
+
sendSSE("done", { sessionKey, status: "end" });
|
|
731
|
+
}
|
|
732
|
+
}
|
|
733
|
+
});
|
|
734
|
+
},
|
|
735
|
+
cancel() {
|
|
736
|
+
closed = true;
|
|
737
|
+
if (unsubscribe) {
|
|
738
|
+
unsubscribe();
|
|
739
|
+
unsubscribe = null;
|
|
740
|
+
}
|
|
741
|
+
}
|
|
742
|
+
});
|
|
743
|
+
if (request.signal) {
|
|
744
|
+
request.signal.addEventListener("abort", () => {
|
|
745
|
+
closed = true;
|
|
746
|
+
if (unsubscribe) {
|
|
747
|
+
unsubscribe();
|
|
748
|
+
unsubscribe = null;
|
|
749
|
+
}
|
|
750
|
+
});
|
|
751
|
+
}
|
|
752
|
+
return new Response(stream, {
|
|
753
|
+
headers: {
|
|
754
|
+
"content-type": "text/event-stream",
|
|
755
|
+
"cache-control": "no-cache, no-transform",
|
|
756
|
+
connection: "keep-alive",
|
|
757
|
+
"x-accel-buffering": "no"
|
|
758
|
+
}
|
|
759
|
+
});
|
|
760
|
+
}
|
|
761
|
+
}
|
|
762
|
+
}
|
|
763
|
+
});
|
|
764
|
+
function deriveFriendlyIdFromKey(key) {
|
|
765
|
+
if (typeof key !== "string" || key.trim().length === 0) return "main";
|
|
766
|
+
const parts = key.split(":");
|
|
767
|
+
const tail = parts[parts.length - 1];
|
|
768
|
+
return tail && tail.trim().length > 0 ? tail.trim() : key;
|
|
769
|
+
}
|
|
770
|
+
function normalizeSessions(payload) {
|
|
771
|
+
const sessions = Array.isArray(
|
|
772
|
+
payload.sessions
|
|
773
|
+
) ? payload.sessions : [];
|
|
774
|
+
const normalized = sessions.map((session) => {
|
|
775
|
+
const rawKey = session.key;
|
|
776
|
+
const key = typeof rawKey === "string" ? rawKey : "";
|
|
777
|
+
const rawFriendly = session.friendlyId;
|
|
778
|
+
const friendlyIdFromPayload = typeof rawFriendly === "string" ? rawFriendly.trim() : "";
|
|
779
|
+
const friendlyId = friendlyIdFromPayload.length > 0 ? friendlyIdFromPayload : deriveFriendlyIdFromKey(key);
|
|
780
|
+
return {
|
|
781
|
+
...session,
|
|
782
|
+
key,
|
|
783
|
+
friendlyId
|
|
784
|
+
};
|
|
785
|
+
});
|
|
786
|
+
return { sessions: normalized };
|
|
787
|
+
}
|
|
788
|
+
const Route$h = createFileRoute("/api/sessions")({
|
|
789
|
+
server: {
|
|
790
|
+
handlers: {
|
|
791
|
+
GET: async () => {
|
|
792
|
+
try {
|
|
793
|
+
const payload = await gatewayRpc(
|
|
794
|
+
"sessions.list",
|
|
795
|
+
{
|
|
796
|
+
limit: 50,
|
|
797
|
+
includeLastMessage: true,
|
|
798
|
+
includeDerivedTitles: true
|
|
799
|
+
}
|
|
800
|
+
);
|
|
801
|
+
return json(normalizeSessions(payload));
|
|
802
|
+
} catch (err) {
|
|
803
|
+
return json(
|
|
804
|
+
{
|
|
805
|
+
error: err instanceof Error ? err.message : String(err)
|
|
806
|
+
},
|
|
807
|
+
{ status: 500 }
|
|
808
|
+
);
|
|
809
|
+
}
|
|
810
|
+
},
|
|
811
|
+
POST: async ({ request }) => {
|
|
812
|
+
try {
|
|
813
|
+
const body = await request.json().catch(() => ({}));
|
|
814
|
+
const requestedLabel = typeof body.label === "string" ? body.label.trim() : "";
|
|
815
|
+
const label = requestedLabel || void 0;
|
|
816
|
+
const friendlyId = randomUUID();
|
|
817
|
+
const params = { key: friendlyId };
|
|
818
|
+
if (label) params.label = label;
|
|
819
|
+
const payload = await gatewayRpc(
|
|
820
|
+
"sessions.patch",
|
|
821
|
+
params
|
|
822
|
+
);
|
|
823
|
+
const sessionKeyRaw = payload.key;
|
|
824
|
+
const sessionKey = typeof sessionKeyRaw === "string" && sessionKeyRaw.trim().length > 0 ? sessionKeyRaw.trim() : "";
|
|
825
|
+
if (sessionKey.length === 0) {
|
|
826
|
+
throw new Error("gateway returned an invalid response");
|
|
827
|
+
}
|
|
828
|
+
await gatewayRpc("sessions.resolve", {
|
|
829
|
+
key: friendlyId,
|
|
830
|
+
includeUnknown: true,
|
|
831
|
+
includeGlobal: true
|
|
832
|
+
}).catch(() => ({ ok: false }));
|
|
833
|
+
return json({
|
|
834
|
+
ok: true,
|
|
835
|
+
sessionKey,
|
|
836
|
+
friendlyId,
|
|
837
|
+
entry: payload.entry
|
|
838
|
+
});
|
|
839
|
+
} catch (err) {
|
|
840
|
+
return json(
|
|
841
|
+
{
|
|
842
|
+
ok: false,
|
|
843
|
+
error: err instanceof Error ? err.message : String(err)
|
|
844
|
+
},
|
|
845
|
+
{ status: 500 }
|
|
846
|
+
);
|
|
847
|
+
}
|
|
848
|
+
},
|
|
849
|
+
PATCH: async ({ request }) => {
|
|
850
|
+
try {
|
|
851
|
+
const body = await request.json().catch(() => ({}));
|
|
852
|
+
const rawSessionKey = typeof body.sessionKey === "string" ? body.sessionKey.trim() : "";
|
|
853
|
+
const rawFriendlyId = typeof body.friendlyId === "string" ? body.friendlyId.trim() : "";
|
|
854
|
+
const label = typeof body.label === "string" ? body.label.trim() : void 0;
|
|
855
|
+
let sessionKey = rawSessionKey;
|
|
856
|
+
const friendlyId = rawFriendlyId;
|
|
857
|
+
if (friendlyId) {
|
|
858
|
+
const resolved = await gatewayRpc(
|
|
859
|
+
"sessions.resolve",
|
|
860
|
+
{
|
|
861
|
+
key: friendlyId,
|
|
862
|
+
includeUnknown: true,
|
|
863
|
+
includeGlobal: true
|
|
864
|
+
}
|
|
865
|
+
);
|
|
866
|
+
const resolvedKey = typeof resolved.key === "string" ? resolved.key.trim() : "";
|
|
867
|
+
if (resolvedKey.length > 0) sessionKey = resolvedKey;
|
|
868
|
+
}
|
|
869
|
+
if (!sessionKey) {
|
|
870
|
+
return json(
|
|
871
|
+
{ ok: false, error: "sessionKey required" },
|
|
872
|
+
{ status: 400 }
|
|
873
|
+
);
|
|
874
|
+
}
|
|
875
|
+
const params = { key: sessionKey };
|
|
876
|
+
if (label) params.label = label;
|
|
877
|
+
const payload = await gatewayRpc(
|
|
878
|
+
"sessions.patch",
|
|
879
|
+
params
|
|
880
|
+
);
|
|
881
|
+
return json({
|
|
882
|
+
ok: true,
|
|
883
|
+
sessionKey,
|
|
884
|
+
entry: payload.entry
|
|
885
|
+
});
|
|
886
|
+
} catch (err) {
|
|
887
|
+
return json(
|
|
888
|
+
{
|
|
889
|
+
ok: false,
|
|
890
|
+
error: err instanceof Error ? err.message : String(err)
|
|
891
|
+
},
|
|
892
|
+
{ status: 500 }
|
|
893
|
+
);
|
|
894
|
+
}
|
|
895
|
+
},
|
|
896
|
+
DELETE: async ({ request }) => {
|
|
897
|
+
try {
|
|
898
|
+
const url = new URL(request.url);
|
|
899
|
+
const rawSessionKey = url.searchParams.get("sessionKey") ?? "";
|
|
900
|
+
const rawFriendlyId = url.searchParams.get("friendlyId") ?? "";
|
|
901
|
+
let sessionKey = rawSessionKey.trim();
|
|
902
|
+
const friendlyId = rawFriendlyId.trim();
|
|
903
|
+
if (friendlyId) {
|
|
904
|
+
const resolved = await gatewayRpc(
|
|
905
|
+
"sessions.resolve",
|
|
906
|
+
{
|
|
907
|
+
key: friendlyId,
|
|
908
|
+
includeUnknown: true,
|
|
909
|
+
includeGlobal: true
|
|
910
|
+
}
|
|
911
|
+
);
|
|
912
|
+
const resolvedKey = typeof resolved.key === "string" ? resolved.key.trim() : "";
|
|
913
|
+
if (resolvedKey.length > 0) sessionKey = resolvedKey;
|
|
914
|
+
}
|
|
915
|
+
if (!sessionKey) {
|
|
916
|
+
return json(
|
|
917
|
+
{ ok: false, error: "sessionKey required" },
|
|
918
|
+
{ status: 400 }
|
|
919
|
+
);
|
|
920
|
+
}
|
|
921
|
+
await gatewayRpc("sessions.delete", { key: sessionKey });
|
|
922
|
+
if (friendlyId && friendlyId !== sessionKey) {
|
|
923
|
+
await gatewayRpc("sessions.delete", { key: friendlyId }).catch(
|
|
924
|
+
() => ({})
|
|
925
|
+
);
|
|
926
|
+
}
|
|
927
|
+
return json({ ok: true, sessionKey });
|
|
928
|
+
} catch (err) {
|
|
929
|
+
return json(
|
|
930
|
+
{
|
|
931
|
+
ok: false,
|
|
932
|
+
error: err instanceof Error ? err.message : String(err)
|
|
933
|
+
},
|
|
934
|
+
{ status: 500 }
|
|
935
|
+
);
|
|
936
|
+
}
|
|
937
|
+
}
|
|
938
|
+
}
|
|
939
|
+
}
|
|
940
|
+
});
|
|
941
|
+
const Route$g = createFileRoute("/api/send")({
|
|
942
|
+
server: {
|
|
943
|
+
handlers: {
|
|
944
|
+
POST: async ({ request }) => {
|
|
945
|
+
try {
|
|
946
|
+
const body = await request.json().catch(() => ({}));
|
|
947
|
+
const rawSessionKey = typeof body.sessionKey === "string" ? body.sessionKey.trim() : "";
|
|
948
|
+
const friendlyId = typeof body.friendlyId === "string" ? body.friendlyId.trim() : "";
|
|
949
|
+
const message = String(body.message ?? "");
|
|
950
|
+
const thinking = typeof body.thinking === "string" ? body.thinking : void 0;
|
|
951
|
+
const rawAttachments = body.attachments;
|
|
952
|
+
const attachments = Array.isArray(rawAttachments) ? rawAttachments.filter(
|
|
953
|
+
(a) => typeof a === "object" && a !== null && typeof a.mimeType === "string" && typeof a.content === "string"
|
|
954
|
+
) : void 0;
|
|
955
|
+
if (!message.trim() && (!attachments || attachments.length === 0)) {
|
|
956
|
+
return json(
|
|
957
|
+
{ ok: false, error: "message required" },
|
|
958
|
+
{ status: 400 }
|
|
959
|
+
);
|
|
960
|
+
}
|
|
961
|
+
let sessionKey = rawSessionKey.length > 0 ? rawSessionKey : "";
|
|
962
|
+
if (!sessionKey && friendlyId) {
|
|
963
|
+
const resolved = await gatewayRpc(
|
|
964
|
+
"sessions.resolve",
|
|
965
|
+
{
|
|
966
|
+
key: friendlyId,
|
|
967
|
+
includeUnknown: true,
|
|
968
|
+
includeGlobal: true
|
|
969
|
+
}
|
|
970
|
+
);
|
|
971
|
+
const resolvedKey = typeof resolved.key === "string" ? resolved.key.trim() : "";
|
|
972
|
+
if (resolvedKey.length === 0) {
|
|
973
|
+
return json(
|
|
974
|
+
{ ok: false, error: "session not found" },
|
|
975
|
+
{ status: 404 }
|
|
976
|
+
);
|
|
977
|
+
}
|
|
978
|
+
sessionKey = resolvedKey;
|
|
979
|
+
}
|
|
980
|
+
if (sessionKey.length === 0) {
|
|
981
|
+
sessionKey = "main";
|
|
982
|
+
}
|
|
983
|
+
const res = await gatewayRpc("chat.send", {
|
|
984
|
+
sessionKey,
|
|
985
|
+
message,
|
|
986
|
+
thinking,
|
|
987
|
+
// model, // Gateway doesn't support model override yet
|
|
988
|
+
attachments,
|
|
989
|
+
deliver: false,
|
|
990
|
+
timeoutMs: 12e4,
|
|
991
|
+
idempotencyKey: typeof body.idempotencyKey === "string" ? body.idempotencyKey : randomUUID()
|
|
992
|
+
});
|
|
993
|
+
return json({ ok: true, ...res, sessionKey });
|
|
994
|
+
} catch (err) {
|
|
995
|
+
return json(
|
|
996
|
+
{
|
|
997
|
+
ok: false,
|
|
998
|
+
error: err instanceof Error ? err.message : String(err)
|
|
999
|
+
},
|
|
1000
|
+
{ status: 500 }
|
|
1001
|
+
);
|
|
1002
|
+
}
|
|
1003
|
+
}
|
|
1004
|
+
}
|
|
1005
|
+
}
|
|
1006
|
+
});
|
|
1007
|
+
const Route$f = createFileRoute("/api/ping")({
|
|
1008
|
+
server: {
|
|
1009
|
+
handlers: {
|
|
1010
|
+
GET: async () => {
|
|
1011
|
+
try {
|
|
1012
|
+
await gatewayConnectCheck();
|
|
1013
|
+
return json({ ok: true });
|
|
1014
|
+
} catch (err) {
|
|
1015
|
+
return json(
|
|
1016
|
+
{
|
|
1017
|
+
ok: false,
|
|
1018
|
+
error: err instanceof Error ? err.message : String(err)
|
|
1019
|
+
},
|
|
1020
|
+
{ status: 503 }
|
|
1021
|
+
);
|
|
1022
|
+
}
|
|
1023
|
+
}
|
|
1024
|
+
}
|
|
1025
|
+
}
|
|
1026
|
+
});
|
|
1027
|
+
const categories = { "core": [{ "id": "cami", "name": "Cami", "emoji": "🦎", "description": "Adaptive AI chameleon companion" }, { "id": "chameleon-agent", "name": "Chameleon Agent", "emoji": "🌀", "description": "Meta-adaptive agent" }, { "id": "professor-stein", "name": "Professor Stein", "emoji": "🎓", "description": "Academic educator & researcher" }, { "id": "dev", "name": "Dev", "emoji": "💻", "description": "Senior programmer & code expert" }, { "id": "flash", "name": "Flash", "emoji": "⚡", "description": "Quick answers & fast problem solver" }], "creative": [{ "id": "luna", "name": "Luna", "emoji": "🌙", "description": "Creative writer & storyteller" }], "curator": [{ "id": "vibe", "name": "Vibe", "emoji": "🎵", "description": "Music curator & playlist expert" }], "learning": [{ "id": "herr-mueller", "name": "Herr Müller", "emoji": "🇩🇪", "description": "German language teacher" }, { "id": "scholar", "name": "Scholar", "emoji": "📚", "description": "Research assistant & academic guide" }, { "id": "lingua", "name": "Lingua", "emoji": "🌍", "description": "Multilingual language coach" }], "lifestyle": [{ "id": "chef-marco", "name": "Chef Marco", "emoji": "👨🍳", "description": "Italian cooking expert" }, { "id": "fit", "name": "Fit", "emoji": "💪", "description": "Fitness trainer & health coach" }, { "id": "zen", "name": "Zen", "emoji": "🧘", "description": "Mindfulness & meditation guide" }], "professional": [{ "id": "cyberguard", "name": "Cyberguard", "emoji": "🛡️", "description": "Cybersecurity expert" }, { "id": "dataviz", "name": "DataViz", "emoji": "📊", "description": "Data visualization specialist" }, { "id": "career-coach", "name": "Career Coach", "emoji": "🎯", "description": "Career guidance & interview prep" }, { "id": "legal-guide", "name": "Legal Guide", "emoji": "⚖️", "description": "Legal information assistant" }, { "id": "startup-sam", "name": "Startup Sam", "emoji": "🚀", "description": "Startup advisor & entrepreneur" }, { "id": "dr-med", "name": "Dr. Med", "emoji": "🩺", "description": "Medical information educator" }, { "id": "wordsmith", "name": "Wordsmith", "emoji": "✍️", "description": "Writing coach & editor" }] };
|
|
1028
|
+
const personasData = {
|
|
1029
|
+
categories
|
|
1030
|
+
};
|
|
1031
|
+
const Route$e = createFileRoute("/api/personas")({
|
|
1032
|
+
server: {
|
|
1033
|
+
handlers: {
|
|
1034
|
+
GET: async () => {
|
|
1035
|
+
try {
|
|
1036
|
+
const res = await gatewayRpc("config.get", {});
|
|
1037
|
+
const workspace = res.config?.agents?.defaults?.workspace || "/root/clawd";
|
|
1038
|
+
const skillJson = await readFile(
|
|
1039
|
+
`${workspace}/skills/personas/skill.json`,
|
|
1040
|
+
"utf-8"
|
|
1041
|
+
);
|
|
1042
|
+
const skill = JSON.parse(skillJson);
|
|
1043
|
+
return json({
|
|
1044
|
+
ok: true,
|
|
1045
|
+
personas: personasData.categories,
|
|
1046
|
+
available: true,
|
|
1047
|
+
skillVersion: skill.version
|
|
1048
|
+
});
|
|
1049
|
+
} catch {
|
|
1050
|
+
return json({
|
|
1051
|
+
ok: true,
|
|
1052
|
+
personas: {},
|
|
1053
|
+
available: false
|
|
1054
|
+
});
|
|
1055
|
+
}
|
|
1056
|
+
}
|
|
1057
|
+
}
|
|
1058
|
+
}
|
|
1059
|
+
});
|
|
1060
|
+
function resolveSessionsDir() {
|
|
1061
|
+
const agentId = (process.env.CLAWDBOT_AGENT_ID || "main").trim() || "main";
|
|
1062
|
+
const stateDir = (process.env.CLAWDBOT_STATE_DIR || path.join(os.homedir(), ".clawdbot")).trim();
|
|
1063
|
+
return {
|
|
1064
|
+
agentId,
|
|
1065
|
+
stateDir,
|
|
1066
|
+
sessionsDir: path.join(stateDir, "agents", agentId, "sessions"),
|
|
1067
|
+
storePath: path.join(
|
|
1068
|
+
stateDir,
|
|
1069
|
+
"agents",
|
|
1070
|
+
agentId,
|
|
1071
|
+
"sessions",
|
|
1072
|
+
"sessions.json"
|
|
1073
|
+
)
|
|
1074
|
+
};
|
|
1075
|
+
}
|
|
1076
|
+
const Route$d = createFileRoute("/api/paths")({
|
|
1077
|
+
server: {
|
|
1078
|
+
handlers: {
|
|
1079
|
+
GET: () => {
|
|
1080
|
+
return json(resolveSessionsDir());
|
|
1081
|
+
}
|
|
1082
|
+
}
|
|
1083
|
+
}
|
|
1084
|
+
});
|
|
1085
|
+
function parseModelName(modelId) {
|
|
1086
|
+
if (modelId.includes("hf:")) {
|
|
1087
|
+
const match = modelId.match(/hf:[^/]+\/(.+)$/);
|
|
1088
|
+
if (match) {
|
|
1089
|
+
return match[1].replace(/-/g, " ");
|
|
1090
|
+
}
|
|
1091
|
+
}
|
|
1092
|
+
const segments = modelId.split("/");
|
|
1093
|
+
const model = segments[segments.length - 1];
|
|
1094
|
+
return model.split("-").map((word) => {
|
|
1095
|
+
if (["gpt", "ai", "api", "glm"].includes(word.toLowerCase())) {
|
|
1096
|
+
return word.toUpperCase();
|
|
1097
|
+
}
|
|
1098
|
+
return word.charAt(0).toUpperCase() + word.slice(1);
|
|
1099
|
+
}).join(" ");
|
|
1100
|
+
}
|
|
1101
|
+
const Route$c = createFileRoute("/api/models")({
|
|
1102
|
+
server: {
|
|
1103
|
+
handlers: {
|
|
1104
|
+
GET: async () => {
|
|
1105
|
+
try {
|
|
1106
|
+
const res = await gatewayRpc("config.get", {});
|
|
1107
|
+
const models = [];
|
|
1108
|
+
if (res.config?.agents?.defaults?.models) {
|
|
1109
|
+
const agentModels = res.config.agents.defaults.models;
|
|
1110
|
+
for (const [modelId, modelConfig] of Object.entries(agentModels)) {
|
|
1111
|
+
const parsedName = parseModelName(modelId);
|
|
1112
|
+
models.push({
|
|
1113
|
+
id: modelId,
|
|
1114
|
+
name: modelConfig.alias ? `${parsedName} (${modelConfig.alias})` : parsedName,
|
|
1115
|
+
provider: modelId.split("/")[0]
|
|
1116
|
+
});
|
|
1117
|
+
}
|
|
1118
|
+
}
|
|
1119
|
+
if (models.length === 0 && res.config?.chat?.models && Array.isArray(res.config.chat.models)) {
|
|
1120
|
+
for (const model of res.config.chat.models) {
|
|
1121
|
+
if (model.enabled !== false && model.id) {
|
|
1122
|
+
models.push({
|
|
1123
|
+
id: model.id,
|
|
1124
|
+
name: model.name || parseModelName(model.id),
|
|
1125
|
+
provider: model.provider
|
|
1126
|
+
});
|
|
1127
|
+
}
|
|
1128
|
+
}
|
|
1129
|
+
}
|
|
1130
|
+
if (models.length === 0 && res.config?.model?.allowed) {
|
|
1131
|
+
for (const modelId of res.config.model.allowed) {
|
|
1132
|
+
models.push({
|
|
1133
|
+
id: modelId,
|
|
1134
|
+
name: parseModelName(modelId)
|
|
1135
|
+
});
|
|
1136
|
+
}
|
|
1137
|
+
}
|
|
1138
|
+
const defaultModel = res.config?.agents?.defaults?.model?.primary || res.config?.chat?.defaultModel || res.config?.model?.defaultModel || models[0]?.id || "";
|
|
1139
|
+
return json({
|
|
1140
|
+
ok: true,
|
|
1141
|
+
models,
|
|
1142
|
+
defaultModel
|
|
1143
|
+
});
|
|
1144
|
+
} catch (err) {
|
|
1145
|
+
console.error("[models] Error fetching models:", err);
|
|
1146
|
+
return json({
|
|
1147
|
+
ok: true,
|
|
1148
|
+
models: [
|
|
1149
|
+
{
|
|
1150
|
+
id: "default",
|
|
1151
|
+
name: "Default Model"
|
|
1152
|
+
}
|
|
1153
|
+
],
|
|
1154
|
+
defaultModel: "default"
|
|
1155
|
+
});
|
|
1156
|
+
}
|
|
1157
|
+
}
|
|
1158
|
+
}
|
|
1159
|
+
}
|
|
1160
|
+
});
|
|
1161
|
+
const DEFAULT_BASE_URL = "https://api.openai.com/v1";
|
|
1162
|
+
const DEFAULT_TIMEOUT_MS = 1e4;
|
|
1163
|
+
const MODEL_FALLBACK_CHAIN = [
|
|
1164
|
+
"gpt-4.1-nano",
|
|
1165
|
+
"gpt-4o-mini",
|
|
1166
|
+
"gpt-3.5-turbo"
|
|
1167
|
+
];
|
|
1168
|
+
async function chatCompletion(messages, options) {
|
|
1169
|
+
const {
|
|
1170
|
+
apiKey,
|
|
1171
|
+
baseUrl = DEFAULT_BASE_URL,
|
|
1172
|
+
model,
|
|
1173
|
+
timeoutMs = DEFAULT_TIMEOUT_MS,
|
|
1174
|
+
maxTokens = 100
|
|
1175
|
+
} = options;
|
|
1176
|
+
const modelsToTry = model ? [model] : MODEL_FALLBACK_CHAIN;
|
|
1177
|
+
let lastError = null;
|
|
1178
|
+
for (const currentModel of modelsToTry) {
|
|
1179
|
+
const controller = new AbortController();
|
|
1180
|
+
const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
|
|
1181
|
+
try {
|
|
1182
|
+
const response = await fetch(`${baseUrl}/chat/completions`, {
|
|
1183
|
+
method: "POST",
|
|
1184
|
+
headers: {
|
|
1185
|
+
"Content-Type": "application/json",
|
|
1186
|
+
"Authorization": `Bearer ${apiKey}`
|
|
1187
|
+
},
|
|
1188
|
+
body: JSON.stringify({
|
|
1189
|
+
model: currentModel,
|
|
1190
|
+
messages,
|
|
1191
|
+
max_tokens: maxTokens,
|
|
1192
|
+
temperature: 0.7
|
|
1193
|
+
}),
|
|
1194
|
+
signal: controller.signal
|
|
1195
|
+
});
|
|
1196
|
+
clearTimeout(timeoutId);
|
|
1197
|
+
if (!response.ok) {
|
|
1198
|
+
const errorText = await response.text();
|
|
1199
|
+
if (response.status === 404 || errorText.includes("model_not_found")) {
|
|
1200
|
+
console.log(`[llm-client] Model ${currentModel} not available, trying next...`);
|
|
1201
|
+
lastError = new Error(`Model ${currentModel} not found`);
|
|
1202
|
+
continue;
|
|
1203
|
+
}
|
|
1204
|
+
throw new Error(`OpenAI API error: ${response.status} ${errorText}`);
|
|
1205
|
+
}
|
|
1206
|
+
const data = await response.json();
|
|
1207
|
+
const content = data.choices[0]?.message?.content?.trim() || "";
|
|
1208
|
+
if (content) {
|
|
1209
|
+
return content;
|
|
1210
|
+
}
|
|
1211
|
+
lastError = new Error(`Model ${currentModel} returned empty response`);
|
|
1212
|
+
continue;
|
|
1213
|
+
} catch (error) {
|
|
1214
|
+
clearTimeout(timeoutId);
|
|
1215
|
+
if (error instanceof Error && error.name === "AbortError") {
|
|
1216
|
+
lastError = new Error("OpenAI API request timed out");
|
|
1217
|
+
continue;
|
|
1218
|
+
}
|
|
1219
|
+
lastError = error instanceof Error ? error : new Error(String(error));
|
|
1220
|
+
continue;
|
|
1221
|
+
}
|
|
1222
|
+
}
|
|
1223
|
+
throw lastError || new Error("All models failed");
|
|
1224
|
+
}
|
|
1225
|
+
async function generateSessionTitle(message, options) {
|
|
1226
|
+
const systemPrompt = `Generate a concise 3-6 word title for this conversation.
|
|
1227
|
+
Rules:
|
|
1228
|
+
- No quotes or punctuation at the end
|
|
1229
|
+
- Capture the main topic/intent
|
|
1230
|
+
- Be specific, not generic
|
|
1231
|
+
- Use title case`;
|
|
1232
|
+
return chatCompletion(
|
|
1233
|
+
[
|
|
1234
|
+
{ role: "system", content: systemPrompt },
|
|
1235
|
+
{ role: "user", content: message }
|
|
1236
|
+
],
|
|
1237
|
+
{ ...options, maxTokens: 25 }
|
|
1238
|
+
);
|
|
1239
|
+
}
|
|
1240
|
+
async function generateFollowUps(conversationContext, options) {
|
|
1241
|
+
const systemPrompt = `Based on this conversation, suggest 3 natural follow-up questions the user might ask.
|
|
1242
|
+
Rules:
|
|
1243
|
+
- Each question max 10 words
|
|
1244
|
+
- Make them specific to the conversation context
|
|
1245
|
+
- Vary the types: clarification, deeper dive, related topic
|
|
1246
|
+
- Return ONLY a JSON array of 3 strings, nothing else`;
|
|
1247
|
+
const response = await chatCompletion(
|
|
1248
|
+
[
|
|
1249
|
+
{ role: "system", content: systemPrompt },
|
|
1250
|
+
{ role: "user", content: conversationContext }
|
|
1251
|
+
],
|
|
1252
|
+
{ ...options, maxTokens: 150 }
|
|
1253
|
+
);
|
|
1254
|
+
try {
|
|
1255
|
+
let jsonStr = response.trim();
|
|
1256
|
+
if (jsonStr.startsWith("```")) {
|
|
1257
|
+
jsonStr = jsonStr.replace(/^```(?:json)?\n?/, "").replace(/\n?```$/, "");
|
|
1258
|
+
}
|
|
1259
|
+
const parsed = JSON.parse(jsonStr);
|
|
1260
|
+
if (Array.isArray(parsed) && parsed.length > 0) {
|
|
1261
|
+
return parsed.slice(0, 3).map(String);
|
|
1262
|
+
}
|
|
1263
|
+
} catch {
|
|
1264
|
+
}
|
|
1265
|
+
return [];
|
|
1266
|
+
}
|
|
1267
|
+
async function testApiKey(apiKey) {
|
|
1268
|
+
try {
|
|
1269
|
+
await chatCompletion(
|
|
1270
|
+
[{ role: "user", content: "Hi" }],
|
|
1271
|
+
{ apiKey, maxTokens: 1, timeoutMs: 5e3 }
|
|
1272
|
+
);
|
|
1273
|
+
return true;
|
|
1274
|
+
} catch {
|
|
1275
|
+
return false;
|
|
1276
|
+
}
|
|
1277
|
+
}
|
|
1278
|
+
function getApiKey(request) {
|
|
1279
|
+
const headerKey = request.headers.get("X-OpenAI-API-Key");
|
|
1280
|
+
if (headerKey?.trim()) {
|
|
1281
|
+
return headerKey.trim();
|
|
1282
|
+
}
|
|
1283
|
+
const envKey = process.env.OPENAI_API_KEY?.trim();
|
|
1284
|
+
if (envKey) {
|
|
1285
|
+
return envKey;
|
|
1286
|
+
}
|
|
1287
|
+
return null;
|
|
1288
|
+
}
|
|
1289
|
+
function generateHeuristicTitle(message) {
|
|
1290
|
+
let text = message.replace(/```[\s\S]*?```/g, " ");
|
|
1291
|
+
text = text.replace(/`[^`]+`/g, " ");
|
|
1292
|
+
text = text.replace(/https?:\/\/[^\s]+/g, " ");
|
|
1293
|
+
text = text.replace(/[^\w\s.,!?'-]/g, " ");
|
|
1294
|
+
text = text.replace(/\s+/g, " ").trim();
|
|
1295
|
+
const words = text.split(/\s+/).filter((word) => {
|
|
1296
|
+
if (word.length <= 2 && !["AI", "ML", "UI", "UX", "API", "CSS", "JS"].includes(word.toUpperCase())) {
|
|
1297
|
+
return false;
|
|
1298
|
+
}
|
|
1299
|
+
return true;
|
|
1300
|
+
});
|
|
1301
|
+
const titleWords = words.slice(0, 6);
|
|
1302
|
+
let title = titleWords.join(" ");
|
|
1303
|
+
title = title.replace(/[.,!?]+$/, "");
|
|
1304
|
+
if (title.length > 60) {
|
|
1305
|
+
title = title.slice(0, 57) + "...";
|
|
1306
|
+
}
|
|
1307
|
+
return title || message.slice(0, 50);
|
|
1308
|
+
}
|
|
1309
|
+
const Route$b = createFileRoute("/api/llm-features")({
|
|
1310
|
+
server: {
|
|
1311
|
+
handlers: {
|
|
1312
|
+
/**
|
|
1313
|
+
* GET /api/llm-features - Check LLM features status
|
|
1314
|
+
*/
|
|
1315
|
+
GET: async () => {
|
|
1316
|
+
try {
|
|
1317
|
+
const hasEnvKey = Boolean(process.env.OPENAI_API_KEY?.trim());
|
|
1318
|
+
return json({
|
|
1319
|
+
ok: true,
|
|
1320
|
+
hasEnvKey
|
|
1321
|
+
});
|
|
1322
|
+
} catch (err) {
|
|
1323
|
+
return json({
|
|
1324
|
+
ok: false,
|
|
1325
|
+
hasEnvKey: false,
|
|
1326
|
+
error: err instanceof Error ? err.message : String(err)
|
|
1327
|
+
});
|
|
1328
|
+
}
|
|
1329
|
+
},
|
|
1330
|
+
/**
|
|
1331
|
+
* POST /api/llm-features - Handle LLM feature requests
|
|
1332
|
+
*
|
|
1333
|
+
* Request body should include an "action" field:
|
|
1334
|
+
* - action: "title" - Generate session title
|
|
1335
|
+
* - action: "followups" - Generate follow-up suggestions
|
|
1336
|
+
* - action: "test" - Test API key validity
|
|
1337
|
+
*/
|
|
1338
|
+
POST: async ({ request }) => {
|
|
1339
|
+
try {
|
|
1340
|
+
const body = await request.json().catch(() => ({}));
|
|
1341
|
+
const action = body.action;
|
|
1342
|
+
switch (action) {
|
|
1343
|
+
case "title": {
|
|
1344
|
+
const { message } = body;
|
|
1345
|
+
if (!message || typeof message !== "string" || message.trim().length < 3) {
|
|
1346
|
+
return json({
|
|
1347
|
+
ok: false,
|
|
1348
|
+
error: "Message is required and must be at least 3 characters"
|
|
1349
|
+
});
|
|
1350
|
+
}
|
|
1351
|
+
const apiKey = getApiKey(request);
|
|
1352
|
+
if (!apiKey) {
|
|
1353
|
+
const title = generateHeuristicTitle(message);
|
|
1354
|
+
return json({
|
|
1355
|
+
ok: true,
|
|
1356
|
+
title,
|
|
1357
|
+
source: "heuristic"
|
|
1358
|
+
});
|
|
1359
|
+
}
|
|
1360
|
+
try {
|
|
1361
|
+
const title = await generateSessionTitle(message, { apiKey });
|
|
1362
|
+
return json({
|
|
1363
|
+
ok: true,
|
|
1364
|
+
title,
|
|
1365
|
+
source: "llm"
|
|
1366
|
+
});
|
|
1367
|
+
} catch (err) {
|
|
1368
|
+
console.error("[llm-features] Title generation error:", err);
|
|
1369
|
+
const title = generateHeuristicTitle(message);
|
|
1370
|
+
return json({
|
|
1371
|
+
ok: true,
|
|
1372
|
+
title,
|
|
1373
|
+
source: "heuristic",
|
|
1374
|
+
error: err instanceof Error ? err.message : "LLM error, used heuristic"
|
|
1375
|
+
});
|
|
1376
|
+
}
|
|
1377
|
+
}
|
|
1378
|
+
case "followups": {
|
|
1379
|
+
const { conversationContext } = body;
|
|
1380
|
+
if (!conversationContext || typeof conversationContext !== "string" || conversationContext.trim().length < 10) {
|
|
1381
|
+
return json({
|
|
1382
|
+
ok: true,
|
|
1383
|
+
suggestions: [],
|
|
1384
|
+
source: "heuristic"
|
|
1385
|
+
});
|
|
1386
|
+
}
|
|
1387
|
+
const apiKey = getApiKey(request);
|
|
1388
|
+
if (!apiKey) {
|
|
1389
|
+
return json({
|
|
1390
|
+
ok: true,
|
|
1391
|
+
suggestions: [],
|
|
1392
|
+
source: "heuristic"
|
|
1393
|
+
});
|
|
1394
|
+
}
|
|
1395
|
+
try {
|
|
1396
|
+
const suggestions = await generateFollowUps(conversationContext, { apiKey });
|
|
1397
|
+
return json({
|
|
1398
|
+
ok: true,
|
|
1399
|
+
suggestions,
|
|
1400
|
+
source: "llm"
|
|
1401
|
+
});
|
|
1402
|
+
} catch (err) {
|
|
1403
|
+
console.error("[llm-features] Follow-ups generation error:", err);
|
|
1404
|
+
return json({
|
|
1405
|
+
ok: true,
|
|
1406
|
+
suggestions: [],
|
|
1407
|
+
source: "heuristic",
|
|
1408
|
+
error: err instanceof Error ? err.message : "LLM error"
|
|
1409
|
+
});
|
|
1410
|
+
}
|
|
1411
|
+
}
|
|
1412
|
+
case "test": {
|
|
1413
|
+
const headerKey = request.headers.get("X-OpenAI-API-Key")?.trim();
|
|
1414
|
+
if (!headerKey) {
|
|
1415
|
+
return json({
|
|
1416
|
+
ok: false,
|
|
1417
|
+
error: "API key required in X-OpenAI-API-Key header"
|
|
1418
|
+
});
|
|
1419
|
+
}
|
|
1420
|
+
try {
|
|
1421
|
+
const valid = await testApiKey(headerKey);
|
|
1422
|
+
return json({
|
|
1423
|
+
ok: true,
|
|
1424
|
+
valid
|
|
1425
|
+
});
|
|
1426
|
+
} catch (err) {
|
|
1427
|
+
return json({
|
|
1428
|
+
ok: true,
|
|
1429
|
+
valid: false,
|
|
1430
|
+
error: err instanceof Error ? err.message : "Test failed"
|
|
1431
|
+
});
|
|
1432
|
+
}
|
|
1433
|
+
}
|
|
1434
|
+
default:
|
|
1435
|
+
return json({
|
|
1436
|
+
ok: false,
|
|
1437
|
+
error: `Unknown action: ${action}. Valid actions: title, followups, test`
|
|
1438
|
+
}, { status: 400 });
|
|
1439
|
+
}
|
|
1440
|
+
} catch (err) {
|
|
1441
|
+
console.error("[llm-features] Error:", err);
|
|
1442
|
+
return json({
|
|
1443
|
+
ok: false,
|
|
1444
|
+
error: err instanceof Error ? err.message : String(err)
|
|
1445
|
+
}, { status: 500 });
|
|
1446
|
+
}
|
|
1447
|
+
}
|
|
1448
|
+
}
|
|
1449
|
+
}
|
|
1450
|
+
});
|
|
1451
|
+
const Route$a = createFileRoute("/api/history")({
|
|
1452
|
+
server: {
|
|
1453
|
+
handlers: {
|
|
1454
|
+
GET: async ({ request }) => {
|
|
1455
|
+
try {
|
|
1456
|
+
const url = new URL(request.url);
|
|
1457
|
+
const limit = Number(url.searchParams.get("limit") || "200");
|
|
1458
|
+
const rawSessionKey = url.searchParams.get("sessionKey")?.trim();
|
|
1459
|
+
const friendlyId = url.searchParams.get("friendlyId")?.trim();
|
|
1460
|
+
let sessionKey = rawSessionKey && rawSessionKey.length > 0 ? rawSessionKey : "";
|
|
1461
|
+
if (!sessionKey && friendlyId) {
|
|
1462
|
+
const resolved = await gatewayRpc(
|
|
1463
|
+
"sessions.resolve",
|
|
1464
|
+
{
|
|
1465
|
+
key: friendlyId,
|
|
1466
|
+
includeUnknown: true,
|
|
1467
|
+
includeGlobal: true
|
|
1468
|
+
}
|
|
1469
|
+
);
|
|
1470
|
+
const resolvedKey = typeof resolved.key === "string" ? resolved.key.trim() : "";
|
|
1471
|
+
if (resolvedKey.length === 0) {
|
|
1472
|
+
return json({ error: "session not found" }, { status: 404 });
|
|
1473
|
+
}
|
|
1474
|
+
sessionKey = resolvedKey;
|
|
1475
|
+
}
|
|
1476
|
+
if (sessionKey.length === 0) {
|
|
1477
|
+
sessionKey = "main";
|
|
1478
|
+
}
|
|
1479
|
+
const payload = await gatewayRpc(
|
|
1480
|
+
"chat.history",
|
|
1481
|
+
{
|
|
1482
|
+
sessionKey,
|
|
1483
|
+
limit
|
|
1484
|
+
}
|
|
1485
|
+
);
|
|
1486
|
+
return json(payload);
|
|
1487
|
+
} catch (err) {
|
|
1488
|
+
return json(
|
|
1489
|
+
{
|
|
1490
|
+
error: err instanceof Error ? err.message : String(err)
|
|
1491
|
+
},
|
|
1492
|
+
{ status: 500 }
|
|
1493
|
+
);
|
|
1494
|
+
}
|
|
1495
|
+
}
|
|
1496
|
+
}
|
|
1497
|
+
}
|
|
1498
|
+
});
|
|
1499
|
+
const FOLLOW_UP_SYSTEM_PROMPT = `You are a helpful assistant that generates follow-up question suggestions.
|
|
1500
|
+
Given the assistant's last response, generate exactly 3 short, natural follow-up questions the user might want to ask.
|
|
1501
|
+
|
|
1502
|
+
Rules:
|
|
1503
|
+
- Each suggestion should be a single, concise question (under 60 characters preferred)
|
|
1504
|
+
- Make them contextually relevant to the response
|
|
1505
|
+
- Vary the types: clarification, deeper exploration, practical application
|
|
1506
|
+
- Use natural, conversational language
|
|
1507
|
+
- Do not number them or add any prefix
|
|
1508
|
+
|
|
1509
|
+
Output format: Return ONLY the 3 questions, one per line, nothing else.`;
|
|
1510
|
+
function parseFollowUps(text) {
|
|
1511
|
+
const lines = text.split("\n").map((line) => line.trim()).filter((line) => line.length > 0).map((line) => line.replace(/^\d+[.)\s]+/, "").trim()).map((line) => line.replace(/^[-•*]\s*/, "").trim()).map((line) => line.replace(/^["']|["']$/g, "").trim()).filter((line) => line.length > 0 && line.length < 150);
|
|
1512
|
+
return lines.slice(0, 3);
|
|
1513
|
+
}
|
|
1514
|
+
const Route$9 = createFileRoute("/api/follow-ups")({
|
|
1515
|
+
server: {
|
|
1516
|
+
handlers: {
|
|
1517
|
+
POST: async ({ request }) => {
|
|
1518
|
+
try {
|
|
1519
|
+
const body = await request.json().catch(() => ({}));
|
|
1520
|
+
const responseText = typeof body.responseText === "string" ? body.responseText.trim() : "";
|
|
1521
|
+
if (!responseText || responseText.length < 30) {
|
|
1522
|
+
return json({ ok: true, suggestions: [] });
|
|
1523
|
+
}
|
|
1524
|
+
const truncatedResponse = responseText.length > 1500 ? responseText.slice(0, 1500) + "..." : responseText;
|
|
1525
|
+
const contextSummary = typeof body.contextSummary === "string" ? body.contextSummary.slice(0, 500) : "";
|
|
1526
|
+
const userPrompt = contextSummary ? `Context: ${contextSummary}
|
|
1527
|
+
|
|
1528
|
+
Assistant's response:
|
|
1529
|
+
${truncatedResponse}` : `Assistant's response:
|
|
1530
|
+
${truncatedResponse}`;
|
|
1531
|
+
const res = await gatewayRpc("chat.complete", {
|
|
1532
|
+
messages: [
|
|
1533
|
+
{ role: "system", content: FOLLOW_UP_SYSTEM_PROMPT },
|
|
1534
|
+
{ role: "user", content: userPrompt }
|
|
1535
|
+
],
|
|
1536
|
+
maxTokens: 200,
|
|
1537
|
+
temperature: 0.7
|
|
1538
|
+
// Use session's default model (no hardcoding!)
|
|
1539
|
+
});
|
|
1540
|
+
const content = res.content || res.message?.content || res.choices?.[0]?.message?.content || "";
|
|
1541
|
+
const suggestions = parseFollowUps(content);
|
|
1542
|
+
return json({ ok: true, suggestions });
|
|
1543
|
+
} catch (err) {
|
|
1544
|
+
console.error("[follow-ups] Error generating suggestions:", err);
|
|
1545
|
+
return json({
|
|
1546
|
+
ok: false,
|
|
1547
|
+
error: err instanceof Error ? err.message : String(err),
|
|
1548
|
+
suggestions: []
|
|
1549
|
+
});
|
|
1550
|
+
}
|
|
1551
|
+
}
|
|
1552
|
+
}
|
|
1553
|
+
}
|
|
1554
|
+
});
|
|
1555
|
+
function createError(message, status, code) {
|
|
1556
|
+
const err = new Error(message);
|
|
1557
|
+
err.status = status;
|
|
1558
|
+
err.code = code;
|
|
1559
|
+
return err;
|
|
1560
|
+
}
|
|
1561
|
+
let _cachedRoot = null;
|
|
1562
|
+
function getFilesRoot() {
|
|
1563
|
+
if (_cachedRoot) return _cachedRoot;
|
|
1564
|
+
const root = process.env.FILES_ROOT?.trim();
|
|
1565
|
+
_cachedRoot = root ? resolve(root) : process.env.HOME || "/home";
|
|
1566
|
+
return _cachedRoot;
|
|
1567
|
+
}
|
|
1568
|
+
async function resolveSafePath(virtualPath) {
|
|
1569
|
+
const root = getFilesRoot();
|
|
1570
|
+
if (virtualPath.includes("\0")) {
|
|
1571
|
+
throw createError("Invalid path", 400, "INVALID_PATH");
|
|
1572
|
+
}
|
|
1573
|
+
const cleaned = virtualPath.replace(/^\/+/, "");
|
|
1574
|
+
const resolved = resolve(root, cleaned);
|
|
1575
|
+
const rel = relative(root, resolved);
|
|
1576
|
+
if (rel.startsWith("..") || resolve(root, rel) !== resolved) {
|
|
1577
|
+
throw createError("Path is outside the allowed directory", 403, "FORBIDDEN");
|
|
1578
|
+
}
|
|
1579
|
+
try {
|
|
1580
|
+
const real = await realpath(resolved);
|
|
1581
|
+
const realRoot = await realpath(root);
|
|
1582
|
+
if (!real.startsWith(realRoot + "/") && real !== realRoot) {
|
|
1583
|
+
throw createError("Path is outside the allowed directory", 403, "FORBIDDEN");
|
|
1584
|
+
}
|
|
1585
|
+
} catch (err) {
|
|
1586
|
+
if (err.code !== "ENOENT" && err.status !== 403) ;
|
|
1587
|
+
if (err.status === 403) throw err;
|
|
1588
|
+
}
|
|
1589
|
+
return resolved;
|
|
1590
|
+
}
|
|
1591
|
+
function classifyFile(name, isDirectory) {
|
|
1592
|
+
if (isDirectory) return "directory";
|
|
1593
|
+
const ext = extname(name).toLowerCase().slice(1);
|
|
1594
|
+
const imageExts = /* @__PURE__ */ new Set(["png", "jpg", "jpeg", "gif", "webp", "svg", "bmp", "ico", "tiff"]);
|
|
1595
|
+
const videoExts = /* @__PURE__ */ new Set(["mp4", "mkv", "avi", "mov", "wmv", "flv", "webm"]);
|
|
1596
|
+
const audioExts = /* @__PURE__ */ new Set(["mp3", "wav", "ogg", "flac", "aac", "m4a", "wma"]);
|
|
1597
|
+
const textExts = /* @__PURE__ */ new Set([
|
|
1598
|
+
"txt",
|
|
1599
|
+
"md",
|
|
1600
|
+
"log",
|
|
1601
|
+
"json",
|
|
1602
|
+
"xml",
|
|
1603
|
+
"yaml",
|
|
1604
|
+
"yml",
|
|
1605
|
+
"csv",
|
|
1606
|
+
"ini",
|
|
1607
|
+
"conf",
|
|
1608
|
+
"cfg",
|
|
1609
|
+
"js",
|
|
1610
|
+
"ts",
|
|
1611
|
+
"jsx",
|
|
1612
|
+
"tsx",
|
|
1613
|
+
"py",
|
|
1614
|
+
"java",
|
|
1615
|
+
"cpp",
|
|
1616
|
+
"c",
|
|
1617
|
+
"h",
|
|
1618
|
+
"css",
|
|
1619
|
+
"html",
|
|
1620
|
+
"htm",
|
|
1621
|
+
"php",
|
|
1622
|
+
"rb",
|
|
1623
|
+
"go",
|
|
1624
|
+
"rs",
|
|
1625
|
+
"sh",
|
|
1626
|
+
"bash",
|
|
1627
|
+
"zsh",
|
|
1628
|
+
"fish",
|
|
1629
|
+
"toml",
|
|
1630
|
+
"env",
|
|
1631
|
+
"gitignore",
|
|
1632
|
+
"dockerfile",
|
|
1633
|
+
"makefile"
|
|
1634
|
+
]);
|
|
1635
|
+
if (imageExts.has(ext)) return "image";
|
|
1636
|
+
if (videoExts.has(ext)) return "video";
|
|
1637
|
+
if (audioExts.has(ext)) return "audio";
|
|
1638
|
+
if (textExts.has(ext)) return "text";
|
|
1639
|
+
return "blob";
|
|
1640
|
+
}
|
|
1641
|
+
async function buildFileItem(absolutePath, name, virtualPath) {
|
|
1642
|
+
const [stats, linkStats] = await Promise.all([
|
|
1643
|
+
stat(absolutePath),
|
|
1644
|
+
lstat(absolutePath)
|
|
1645
|
+
]);
|
|
1646
|
+
const isDir = stats.isDirectory();
|
|
1647
|
+
return {
|
|
1648
|
+
path: virtualPath,
|
|
1649
|
+
name,
|
|
1650
|
+
size: stats.size,
|
|
1651
|
+
extension: isDir ? "" : extname(name).slice(1).toLowerCase(),
|
|
1652
|
+
modified: stats.mtime.toISOString(),
|
|
1653
|
+
mode: stats.mode,
|
|
1654
|
+
isDir,
|
|
1655
|
+
isSymlink: linkStats.isSymbolicLink(),
|
|
1656
|
+
type: classifyFile(name, isDir)
|
|
1657
|
+
};
|
|
1658
|
+
}
|
|
1659
|
+
async function listFiles(path2 = "/") {
|
|
1660
|
+
const absolutePath = await resolveSafePath(path2);
|
|
1661
|
+
let stats;
|
|
1662
|
+
try {
|
|
1663
|
+
stats = await stat(absolutePath);
|
|
1664
|
+
} catch (err) {
|
|
1665
|
+
if (err.code === "ENOENT") {
|
|
1666
|
+
throw createError(`Path not found: ${path2}`, 404, "NOT_FOUND");
|
|
1667
|
+
}
|
|
1668
|
+
throw createError(`Failed to access path: ${err.message}`, 500, "FS_ERROR");
|
|
1669
|
+
}
|
|
1670
|
+
if (!stats.isDirectory()) {
|
|
1671
|
+
throw createError("Path is not a directory", 400, "NOT_DIRECTORY");
|
|
1672
|
+
}
|
|
1673
|
+
let entries;
|
|
1674
|
+
try {
|
|
1675
|
+
entries = await readdir(absolutePath, { withFileTypes: true });
|
|
1676
|
+
} catch (err) {
|
|
1677
|
+
if (err.code === "EACCES") {
|
|
1678
|
+
throw createError("Permission denied", 403, "PERMISSION_DENIED");
|
|
1679
|
+
}
|
|
1680
|
+
throw createError(`Failed to read directory: ${err.message}`, 500, "FS_ERROR");
|
|
1681
|
+
}
|
|
1682
|
+
const visibleEntries = entries;
|
|
1683
|
+
const items = [];
|
|
1684
|
+
let numDirs = 0;
|
|
1685
|
+
let numFiles = 0;
|
|
1686
|
+
for (const entry of visibleEntries) {
|
|
1687
|
+
const entryPath = join(absolutePath, entry.name);
|
|
1688
|
+
const virtualEntryPath = path2 === "/" ? `/${entry.name}` : `${path2}/${entry.name}`;
|
|
1689
|
+
try {
|
|
1690
|
+
const item = await buildFileItem(entryPath, entry.name, virtualEntryPath);
|
|
1691
|
+
items.push(item);
|
|
1692
|
+
if (item.isDir) numDirs++;
|
|
1693
|
+
else numFiles++;
|
|
1694
|
+
} catch {
|
|
1695
|
+
}
|
|
1696
|
+
}
|
|
1697
|
+
items.sort((a, b) => {
|
|
1698
|
+
if (a.isDir !== b.isDir) return a.isDir ? -1 : 1;
|
|
1699
|
+
return a.name.localeCompare(b.name, void 0, { sensitivity: "base" });
|
|
1700
|
+
});
|
|
1701
|
+
const dirName = path2 === "/" ? "/" : path2.split("/").pop() || "/";
|
|
1702
|
+
return {
|
|
1703
|
+
items,
|
|
1704
|
+
path: path2,
|
|
1705
|
+
name: dirName,
|
|
1706
|
+
isDir: true,
|
|
1707
|
+
size: stats.size,
|
|
1708
|
+
modified: stats.mtime.toISOString(),
|
|
1709
|
+
mode: stats.mode,
|
|
1710
|
+
numDirs,
|
|
1711
|
+
numFiles,
|
|
1712
|
+
sorting: { by: "name", asc: true }
|
|
1713
|
+
};
|
|
1714
|
+
}
|
|
1715
|
+
async function getFileInfo(path2) {
|
|
1716
|
+
const absolutePath = await resolveSafePath(path2);
|
|
1717
|
+
try {
|
|
1718
|
+
const name = path2.split("/").pop() || "";
|
|
1719
|
+
return await buildFileItem(absolutePath, name, path2);
|
|
1720
|
+
} catch (err) {
|
|
1721
|
+
if (err.code === "ENOENT") {
|
|
1722
|
+
throw createError(`File not found: ${path2}`, 404, "NOT_FOUND");
|
|
1723
|
+
}
|
|
1724
|
+
throw createError(`Failed to get file info: ${err.message}`, 500, "FS_ERROR");
|
|
1725
|
+
}
|
|
1726
|
+
}
|
|
1727
|
+
async function downloadFile(path2) {
|
|
1728
|
+
const absolutePath = await resolveSafePath(path2);
|
|
1729
|
+
try {
|
|
1730
|
+
const buffer = await readFile(absolutePath);
|
|
1731
|
+
return buffer.buffer.slice(buffer.byteOffset, buffer.byteOffset + buffer.byteLength);
|
|
1732
|
+
} catch (err) {
|
|
1733
|
+
if (err.code === "ENOENT") {
|
|
1734
|
+
throw createError(`File not found: ${path2}`, 404, "NOT_FOUND");
|
|
1735
|
+
}
|
|
1736
|
+
if (err.code === "EACCES") {
|
|
1737
|
+
throw createError("Permission denied", 403, "PERMISSION_DENIED");
|
|
1738
|
+
}
|
|
1739
|
+
throw createError(`Failed to read file: ${err.message}`, 500, "FS_ERROR");
|
|
1740
|
+
}
|
|
1741
|
+
}
|
|
1742
|
+
async function uploadFile(path2, content) {
|
|
1743
|
+
const absolutePath = await resolveSafePath(path2);
|
|
1744
|
+
const parentDir = absolutePath.substring(0, absolutePath.lastIndexOf("/"));
|
|
1745
|
+
await mkdir(parentDir, { recursive: true }).catch(() => {
|
|
1746
|
+
});
|
|
1747
|
+
try {
|
|
1748
|
+
await writeFile(absolutePath, Buffer.from(content));
|
|
1749
|
+
} catch (err) {
|
|
1750
|
+
if (err.code === "EACCES") {
|
|
1751
|
+
throw createError("Permission denied", 403, "PERMISSION_DENIED");
|
|
1752
|
+
}
|
|
1753
|
+
throw createError(`Failed to write file: ${err.message}`, 500, "FS_ERROR");
|
|
1754
|
+
}
|
|
1755
|
+
}
|
|
1756
|
+
async function deleteFile(path2) {
|
|
1757
|
+
const absolutePath = await resolveSafePath(path2);
|
|
1758
|
+
try {
|
|
1759
|
+
await rm(absolutePath, { recursive: true });
|
|
1760
|
+
} catch (err) {
|
|
1761
|
+
if (err.code === "ENOENT") {
|
|
1762
|
+
throw createError(`File not found: ${path2}`, 404, "NOT_FOUND");
|
|
1763
|
+
}
|
|
1764
|
+
if (err.code === "EACCES") {
|
|
1765
|
+
throw createError("Permission denied", 403, "PERMISSION_DENIED");
|
|
1766
|
+
}
|
|
1767
|
+
throw createError(`Failed to delete: ${err.message}`, 500, "FS_ERROR");
|
|
1768
|
+
}
|
|
1769
|
+
}
|
|
1770
|
+
async function createFolder(path2) {
|
|
1771
|
+
const absolutePath = await resolveSafePath(path2);
|
|
1772
|
+
try {
|
|
1773
|
+
await mkdir(absolutePath, { recursive: true });
|
|
1774
|
+
} catch (err) {
|
|
1775
|
+
if (err.code === "EACCES") {
|
|
1776
|
+
throw createError("Permission denied", 403, "PERMISSION_DENIED");
|
|
1777
|
+
}
|
|
1778
|
+
if (err.code === "EEXIST") {
|
|
1779
|
+
throw createError("Directory already exists", 409, "ALREADY_EXISTS");
|
|
1780
|
+
}
|
|
1781
|
+
throw createError(`Failed to create directory: ${err.message}`, 500, "FS_ERROR");
|
|
1782
|
+
}
|
|
1783
|
+
}
|
|
1784
|
+
async function renameFile(src, dst) {
|
|
1785
|
+
const absoluteSrc = await resolveSafePath(src);
|
|
1786
|
+
const absoluteDst = await resolveSafePath(dst);
|
|
1787
|
+
const parentDir = absoluteDst.substring(0, absoluteDst.lastIndexOf("/"));
|
|
1788
|
+
await mkdir(parentDir, { recursive: true }).catch(() => {
|
|
1789
|
+
});
|
|
1790
|
+
try {
|
|
1791
|
+
await rename(absoluteSrc, absoluteDst);
|
|
1792
|
+
} catch (err) {
|
|
1793
|
+
if (err.code === "ENOENT") {
|
|
1794
|
+
throw createError(`Source not found: ${src}`, 404, "NOT_FOUND");
|
|
1795
|
+
}
|
|
1796
|
+
if (err.code === "EACCES") {
|
|
1797
|
+
throw createError("Permission denied", 403, "PERMISSION_DENIED");
|
|
1798
|
+
}
|
|
1799
|
+
throw createError(`Failed to rename: ${err.message}`, 500, "FS_ERROR");
|
|
1800
|
+
}
|
|
1801
|
+
}
|
|
1802
|
+
function sanitizePath(path2) {
|
|
1803
|
+
if (typeof path2 !== "string" || path2.length === 0) {
|
|
1804
|
+
return "/";
|
|
1805
|
+
}
|
|
1806
|
+
let normalized = posix.normalize(path2);
|
|
1807
|
+
if (!normalized.startsWith("/")) {
|
|
1808
|
+
normalized = "/" + normalized;
|
|
1809
|
+
}
|
|
1810
|
+
const parts = normalized.split("/").filter((part) => part !== "" && part !== ".");
|
|
1811
|
+
const safeParts = [];
|
|
1812
|
+
for (const part of parts) {
|
|
1813
|
+
if (part === "..") {
|
|
1814
|
+
if (safeParts.length > 0) {
|
|
1815
|
+
safeParts.pop();
|
|
1816
|
+
}
|
|
1817
|
+
} else {
|
|
1818
|
+
safeParts.push(part);
|
|
1819
|
+
}
|
|
1820
|
+
}
|
|
1821
|
+
return "/" + safeParts.join("/");
|
|
1822
|
+
}
|
|
1823
|
+
function isPathSafe(path2) {
|
|
1824
|
+
if (typeof path2 !== "string") {
|
|
1825
|
+
return false;
|
|
1826
|
+
}
|
|
1827
|
+
if (path2.includes("../") || path2.includes("..\\") || path2 === "..") {
|
|
1828
|
+
return false;
|
|
1829
|
+
}
|
|
1830
|
+
if (path2.includes("\0")) {
|
|
1831
|
+
return false;
|
|
1832
|
+
}
|
|
1833
|
+
if (path2.includes("\\")) {
|
|
1834
|
+
return false;
|
|
1835
|
+
}
|
|
1836
|
+
const sanitized = sanitizePath(path2);
|
|
1837
|
+
return path2 === sanitized || path2.endsWith("/") && sanitized === path2.slice(0, -1);
|
|
1838
|
+
}
|
|
1839
|
+
function validatePath(path2, context = "Path") {
|
|
1840
|
+
if (!path2 || typeof path2 !== "string") {
|
|
1841
|
+
throw new Error(`${context} is required`);
|
|
1842
|
+
}
|
|
1843
|
+
if (!isPathSafe(path2)) {
|
|
1844
|
+
throw new Error(`${context} contains invalid characters or traversal attempts`);
|
|
1845
|
+
}
|
|
1846
|
+
return sanitizePath(path2);
|
|
1847
|
+
}
|
|
1848
|
+
function validateFilename(filename) {
|
|
1849
|
+
if (typeof filename !== "string" || filename.length === 0) {
|
|
1850
|
+
return false;
|
|
1851
|
+
}
|
|
1852
|
+
if (filename.includes("/") || filename.includes("\\")) {
|
|
1853
|
+
return false;
|
|
1854
|
+
}
|
|
1855
|
+
if (filename.includes("\0")) {
|
|
1856
|
+
return false;
|
|
1857
|
+
}
|
|
1858
|
+
for (let i = 0; i < filename.length; i++) {
|
|
1859
|
+
const code = filename.charCodeAt(i);
|
|
1860
|
+
if (code >= 0 && code <= 31) {
|
|
1861
|
+
return false;
|
|
1862
|
+
}
|
|
1863
|
+
}
|
|
1864
|
+
const reserved = [
|
|
1865
|
+
"CON",
|
|
1866
|
+
"PRN",
|
|
1867
|
+
"AUX",
|
|
1868
|
+
"NUL",
|
|
1869
|
+
"COM1",
|
|
1870
|
+
"COM2",
|
|
1871
|
+
"COM3",
|
|
1872
|
+
"COM4",
|
|
1873
|
+
"COM5",
|
|
1874
|
+
"COM6",
|
|
1875
|
+
"COM7",
|
|
1876
|
+
"COM8",
|
|
1877
|
+
"COM9",
|
|
1878
|
+
"LPT1",
|
|
1879
|
+
"LPT2",
|
|
1880
|
+
"LPT3",
|
|
1881
|
+
"LPT4",
|
|
1882
|
+
"LPT5",
|
|
1883
|
+
"LPT6",
|
|
1884
|
+
"LPT7",
|
|
1885
|
+
"LPT8",
|
|
1886
|
+
"LPT9"
|
|
1887
|
+
];
|
|
1888
|
+
const nameUpper = filename.toUpperCase();
|
|
1889
|
+
if (reserved.includes(nameUpper) || reserved.some((res) => nameUpper.startsWith(res + "."))) {
|
|
1890
|
+
return false;
|
|
1891
|
+
}
|
|
1892
|
+
if (filename === "." || filename === "..") {
|
|
1893
|
+
return false;
|
|
1894
|
+
}
|
|
1895
|
+
return true;
|
|
1896
|
+
}
|
|
1897
|
+
const MAX_FILE_SIZE = 100 * 1024 * 1024;
|
|
1898
|
+
const Route$8 = createFileRoute("/api/files/upload")({
|
|
1899
|
+
server: {
|
|
1900
|
+
handlers: {
|
|
1901
|
+
POST: async ({ request }) => {
|
|
1902
|
+
try {
|
|
1903
|
+
const formData = await request.formData();
|
|
1904
|
+
const rawPath = formData.get("path");
|
|
1905
|
+
if (!rawPath) {
|
|
1906
|
+
return json(
|
|
1907
|
+
{
|
|
1908
|
+
error: "path parameter is required",
|
|
1909
|
+
code: "MISSING_PATH"
|
|
1910
|
+
},
|
|
1911
|
+
{ status: 400 }
|
|
1912
|
+
);
|
|
1913
|
+
}
|
|
1914
|
+
const path2 = validatePath(rawPath, "Path parameter");
|
|
1915
|
+
const files = [];
|
|
1916
|
+
for (const [key, value] of formData.entries()) {
|
|
1917
|
+
if (key === "file" || key.startsWith("file")) {
|
|
1918
|
+
if (value instanceof File) {
|
|
1919
|
+
files.push(value);
|
|
1920
|
+
}
|
|
1921
|
+
}
|
|
1922
|
+
}
|
|
1923
|
+
if (files.length === 0) {
|
|
1924
|
+
return json(
|
|
1925
|
+
{
|
|
1926
|
+
error: "At least one file is required",
|
|
1927
|
+
code: "NO_FILES"
|
|
1928
|
+
},
|
|
1929
|
+
{ status: 400 }
|
|
1930
|
+
);
|
|
1931
|
+
}
|
|
1932
|
+
const uploadedFiles = [];
|
|
1933
|
+
for (const file of files) {
|
|
1934
|
+
if (file.size > MAX_FILE_SIZE) {
|
|
1935
|
+
return json(
|
|
1936
|
+
{
|
|
1937
|
+
error: `File "${file.name}" is too large. Maximum size is ${MAX_FILE_SIZE / 1024 / 1024}MB`,
|
|
1938
|
+
code: "FILE_TOO_LARGE"
|
|
1939
|
+
},
|
|
1940
|
+
{ status: 413 }
|
|
1941
|
+
// Payload Too Large
|
|
1942
|
+
);
|
|
1943
|
+
}
|
|
1944
|
+
if (!validateFilename(file.name)) {
|
|
1945
|
+
return json(
|
|
1946
|
+
{
|
|
1947
|
+
error: `Invalid filename: "${file.name}". Filenames cannot contain path separators or control characters`,
|
|
1948
|
+
code: "INVALID_FILENAME"
|
|
1949
|
+
},
|
|
1950
|
+
{ status: 400 }
|
|
1951
|
+
);
|
|
1952
|
+
}
|
|
1953
|
+
const content = await file.arrayBuffer();
|
|
1954
|
+
const fullPath = path2.endsWith("/") ? `${path2}${file.name}` : `${path2}/${file.name}`;
|
|
1955
|
+
await uploadFile(fullPath, content);
|
|
1956
|
+
uploadedFiles.push({
|
|
1957
|
+
filename: file.name,
|
|
1958
|
+
size: content.byteLength,
|
|
1959
|
+
path: fullPath
|
|
1960
|
+
});
|
|
1961
|
+
}
|
|
1962
|
+
return json({
|
|
1963
|
+
ok: true,
|
|
1964
|
+
message: `${uploadedFiles.length} file(s) uploaded successfully`,
|
|
1965
|
+
files: uploadedFiles
|
|
1966
|
+
});
|
|
1967
|
+
} catch (err) {
|
|
1968
|
+
const error = err;
|
|
1969
|
+
if (error.message.includes("invalid characters") || error.message.includes("traversal attempts") || error.message.includes("Invalid filename")) {
|
|
1970
|
+
return json(
|
|
1971
|
+
{
|
|
1972
|
+
error: error.message,
|
|
1973
|
+
code: "VALIDATION_ERROR"
|
|
1974
|
+
},
|
|
1975
|
+
{ status: 400 }
|
|
1976
|
+
);
|
|
1977
|
+
}
|
|
1978
|
+
const status = error.status || 500;
|
|
1979
|
+
const code = error.code || "INTERNAL_ERROR";
|
|
1980
|
+
return json(
|
|
1981
|
+
{
|
|
1982
|
+
error: error.message || "An unexpected error occurred",
|
|
1983
|
+
code
|
|
1984
|
+
},
|
|
1985
|
+
{ status }
|
|
1986
|
+
);
|
|
1987
|
+
}
|
|
1988
|
+
}
|
|
1989
|
+
}
|
|
1990
|
+
}
|
|
1991
|
+
});
|
|
1992
|
+
const MAX_TEXT_SIZE$1 = 5 * 1024 * 1024;
|
|
1993
|
+
const Route$7 = createFileRoute("/api/files/save")({
|
|
1994
|
+
server: {
|
|
1995
|
+
handlers: {
|
|
1996
|
+
POST: async ({ request }) => {
|
|
1997
|
+
try {
|
|
1998
|
+
const body = await request.json().catch(() => ({}));
|
|
1999
|
+
const rawPath = typeof body.path === "string" ? body.path.trim() : "";
|
|
2000
|
+
const content = typeof body.content === "string" ? body.content : null;
|
|
2001
|
+
if (!rawPath) {
|
|
2002
|
+
return json({ error: "path is required", code: "MISSING_PATH" }, { status: 400 });
|
|
2003
|
+
}
|
|
2004
|
+
if (content === null) {
|
|
2005
|
+
return json({ error: "content is required", code: "MISSING_CONTENT" }, { status: 400 });
|
|
2006
|
+
}
|
|
2007
|
+
const path2 = validatePath(rawPath, "Path parameter");
|
|
2008
|
+
const encoded = new TextEncoder().encode(content);
|
|
2009
|
+
if (encoded.byteLength > MAX_TEXT_SIZE$1) {
|
|
2010
|
+
return json(
|
|
2011
|
+
{ error: "Content too large (max 5MB)", code: "CONTENT_TOO_LARGE" },
|
|
2012
|
+
{ status: 413 }
|
|
2013
|
+
);
|
|
2014
|
+
}
|
|
2015
|
+
await uploadFile(path2, encoded.buffer);
|
|
2016
|
+
return json({
|
|
2017
|
+
ok: true,
|
|
2018
|
+
message: "File saved successfully",
|
|
2019
|
+
path: path2,
|
|
2020
|
+
size: encoded.byteLength
|
|
2021
|
+
});
|
|
2022
|
+
} catch (err) {
|
|
2023
|
+
const error = err;
|
|
2024
|
+
const status = error.status || 500;
|
|
2025
|
+
const code = error.code || "INTERNAL_ERROR";
|
|
2026
|
+
return json(
|
|
2027
|
+
{ error: error.message || "An unexpected error occurred", code },
|
|
2028
|
+
{ status }
|
|
2029
|
+
);
|
|
2030
|
+
}
|
|
2031
|
+
}
|
|
2032
|
+
}
|
|
2033
|
+
}
|
|
2034
|
+
});
|
|
2035
|
+
const Route$6 = createFileRoute("/api/files/rename")({
|
|
2036
|
+
server: {
|
|
2037
|
+
handlers: {
|
|
2038
|
+
POST: async ({ request }) => {
|
|
2039
|
+
try {
|
|
2040
|
+
const body = await request.json().catch(() => ({}));
|
|
2041
|
+
const rawSrc = typeof body.src === "string" ? body.src.trim() : "";
|
|
2042
|
+
const rawDst = typeof body.dst === "string" ? body.dst.trim() : "";
|
|
2043
|
+
if (!rawSrc) {
|
|
2044
|
+
return json(
|
|
2045
|
+
{
|
|
2046
|
+
error: "src path is required",
|
|
2047
|
+
code: "MISSING_SRC_PATH"
|
|
2048
|
+
},
|
|
2049
|
+
{ status: 400 }
|
|
2050
|
+
);
|
|
2051
|
+
}
|
|
2052
|
+
if (!rawDst) {
|
|
2053
|
+
return json(
|
|
2054
|
+
{
|
|
2055
|
+
error: "dst path is required",
|
|
2056
|
+
code: "MISSING_DST_PATH"
|
|
2057
|
+
},
|
|
2058
|
+
{ status: 400 }
|
|
2059
|
+
);
|
|
2060
|
+
}
|
|
2061
|
+
const src = validatePath(rawSrc, "Source path");
|
|
2062
|
+
const dst = validatePath(rawDst, "Destination path");
|
|
2063
|
+
if (src === "/" || src === "") {
|
|
2064
|
+
return json(
|
|
2065
|
+
{
|
|
2066
|
+
error: "Cannot rename the root directory",
|
|
2067
|
+
code: "FORBIDDEN_RENAME_ROOT"
|
|
2068
|
+
},
|
|
2069
|
+
{ status: 403 }
|
|
2070
|
+
);
|
|
2071
|
+
}
|
|
2072
|
+
const dstFilename = dst.split("/").pop() || "";
|
|
2073
|
+
if (dstFilename && !validateFilename(dstFilename)) {
|
|
2074
|
+
return json(
|
|
2075
|
+
{
|
|
2076
|
+
error: `Invalid destination filename: "${dstFilename}". Filenames cannot contain path separators or control characters`,
|
|
2077
|
+
code: "INVALID_FILENAME"
|
|
2078
|
+
},
|
|
2079
|
+
{ status: 400 }
|
|
2080
|
+
);
|
|
2081
|
+
}
|
|
2082
|
+
let overwriteWarning = false;
|
|
2083
|
+
try {
|
|
2084
|
+
await getFileInfo(dst);
|
|
2085
|
+
overwriteWarning = true;
|
|
2086
|
+
} catch (err) {
|
|
2087
|
+
}
|
|
2088
|
+
const url = new URL(request.url);
|
|
2089
|
+
const allowOverwrite = url.searchParams.get("override") === "true";
|
|
2090
|
+
if (overwriteWarning && !allowOverwrite) {
|
|
2091
|
+
return json(
|
|
2092
|
+
{
|
|
2093
|
+
error: `Destination "${dst}" already exists. Use ?override=true to overwrite`,
|
|
2094
|
+
code: "DESTINATION_EXISTS"
|
|
2095
|
+
},
|
|
2096
|
+
{ status: 409 }
|
|
2097
|
+
// Conflict
|
|
2098
|
+
);
|
|
2099
|
+
}
|
|
2100
|
+
await renameFile(src, dst);
|
|
2101
|
+
return json({
|
|
2102
|
+
ok: true,
|
|
2103
|
+
message: overwriteWarning ? "File renamed successfully (existing file overwritten)" : "File renamed successfully",
|
|
2104
|
+
src,
|
|
2105
|
+
dst
|
|
2106
|
+
});
|
|
2107
|
+
} catch (err) {
|
|
2108
|
+
const error = err;
|
|
2109
|
+
if (error.message.includes("invalid characters") || error.message.includes("traversal attempts") || error.message.includes("Invalid destination") || error.message.includes("Cannot rename") || error.message.includes("already exists")) {
|
|
2110
|
+
return json(
|
|
2111
|
+
{
|
|
2112
|
+
error: error.message,
|
|
2113
|
+
code: "VALIDATION_ERROR"
|
|
2114
|
+
},
|
|
2115
|
+
{ status: 400 }
|
|
2116
|
+
);
|
|
2117
|
+
}
|
|
2118
|
+
const status = error.status || 500;
|
|
2119
|
+
const code = error.code || "INTERNAL_ERROR";
|
|
2120
|
+
return json(
|
|
2121
|
+
{
|
|
2122
|
+
error: error.message || "An unexpected error occurred",
|
|
2123
|
+
code
|
|
2124
|
+
},
|
|
2125
|
+
{ status }
|
|
2126
|
+
);
|
|
2127
|
+
}
|
|
2128
|
+
}
|
|
2129
|
+
}
|
|
2130
|
+
}
|
|
2131
|
+
});
|
|
2132
|
+
const MAX_TEXT_SIZE = 5 * 1024 * 1024;
|
|
2133
|
+
const TEXT_EXTENSIONS = /* @__PURE__ */ new Set([
|
|
2134
|
+
"txt",
|
|
2135
|
+
"md",
|
|
2136
|
+
"log",
|
|
2137
|
+
"json",
|
|
2138
|
+
"xml",
|
|
2139
|
+
"yaml",
|
|
2140
|
+
"yml",
|
|
2141
|
+
"csv",
|
|
2142
|
+
"ini",
|
|
2143
|
+
"conf",
|
|
2144
|
+
"cfg",
|
|
2145
|
+
"js",
|
|
2146
|
+
"ts",
|
|
2147
|
+
"jsx",
|
|
2148
|
+
"tsx",
|
|
2149
|
+
"py",
|
|
2150
|
+
"java",
|
|
2151
|
+
"cpp",
|
|
2152
|
+
"c",
|
|
2153
|
+
"h",
|
|
2154
|
+
"css",
|
|
2155
|
+
"html",
|
|
2156
|
+
"htm",
|
|
2157
|
+
"php",
|
|
2158
|
+
"rb",
|
|
2159
|
+
"go",
|
|
2160
|
+
"rs",
|
|
2161
|
+
"sh",
|
|
2162
|
+
"bash",
|
|
2163
|
+
"zsh",
|
|
2164
|
+
"toml",
|
|
2165
|
+
"env",
|
|
2166
|
+
"gitignore",
|
|
2167
|
+
"dockerfile",
|
|
2168
|
+
"makefile",
|
|
2169
|
+
"sql",
|
|
2170
|
+
"graphql",
|
|
2171
|
+
"svelte",
|
|
2172
|
+
"vue",
|
|
2173
|
+
"scss",
|
|
2174
|
+
"less",
|
|
2175
|
+
"diff",
|
|
2176
|
+
"patch"
|
|
2177
|
+
]);
|
|
2178
|
+
const Route$5 = createFileRoute("/api/files/read")({
|
|
2179
|
+
server: {
|
|
2180
|
+
handlers: {
|
|
2181
|
+
GET: async ({ request }) => {
|
|
2182
|
+
try {
|
|
2183
|
+
const url = new URL(request.url);
|
|
2184
|
+
const rawPath = url.searchParams.get("path");
|
|
2185
|
+
if (!rawPath) {
|
|
2186
|
+
return new Response(
|
|
2187
|
+
JSON.stringify({ error: "path parameter is required", code: "MISSING_PATH" }),
|
|
2188
|
+
{ status: 400, headers: { "Content-Type": "application/json" } }
|
|
2189
|
+
);
|
|
2190
|
+
}
|
|
2191
|
+
const path2 = validatePath(rawPath, "Path parameter");
|
|
2192
|
+
const fileInfo = await getFileInfo(path2);
|
|
2193
|
+
if (fileInfo.isDir) {
|
|
2194
|
+
return new Response(
|
|
2195
|
+
JSON.stringify({ error: "Cannot read a directory", code: "IS_DIRECTORY" }),
|
|
2196
|
+
{ status: 400, headers: { "Content-Type": "application/json" } }
|
|
2197
|
+
);
|
|
2198
|
+
}
|
|
2199
|
+
const ext = fileInfo.extension.toLowerCase();
|
|
2200
|
+
if (!TEXT_EXTENSIONS.has(ext) && ext !== "") {
|
|
2201
|
+
return new Response(
|
|
2202
|
+
JSON.stringify({ error: "File type not supported for text editing", code: "UNSUPPORTED_TYPE" }),
|
|
2203
|
+
{ status: 400, headers: { "Content-Type": "application/json" } }
|
|
2204
|
+
);
|
|
2205
|
+
}
|
|
2206
|
+
if (fileInfo.size > MAX_TEXT_SIZE) {
|
|
2207
|
+
return new Response(
|
|
2208
|
+
JSON.stringify({ error: "File too large for text editing (max 5MB)", code: "FILE_TOO_LARGE" }),
|
|
2209
|
+
{ status: 413, headers: { "Content-Type": "application/json" } }
|
|
2210
|
+
);
|
|
2211
|
+
}
|
|
2212
|
+
const content = await downloadFile(path2);
|
|
2213
|
+
const text = new TextDecoder("utf-8", { fatal: false }).decode(new Uint8Array(content));
|
|
2214
|
+
return new Response(
|
|
2215
|
+
JSON.stringify({ content: text, name: fileInfo.name, extension: fileInfo.extension, size: fileInfo.size }),
|
|
2216
|
+
{ headers: { "Content-Type": "application/json" } }
|
|
2217
|
+
);
|
|
2218
|
+
} catch (err) {
|
|
2219
|
+
const error = err;
|
|
2220
|
+
const status = error.status || 500;
|
|
2221
|
+
const code = error.code || "INTERNAL_ERROR";
|
|
2222
|
+
return new Response(
|
|
2223
|
+
JSON.stringify({ error: error.message || "An unexpected error occurred", code }),
|
|
2224
|
+
{ status, headers: { "Content-Type": "application/json" } }
|
|
2225
|
+
);
|
|
2226
|
+
}
|
|
2227
|
+
}
|
|
2228
|
+
}
|
|
2229
|
+
}
|
|
2230
|
+
});
|
|
2231
|
+
function validateFolderName(path2) {
|
|
2232
|
+
const folderName = path2.split("/").pop() || "";
|
|
2233
|
+
if (!folderName) {
|
|
2234
|
+
throw new Error("Folder name cannot be empty");
|
|
2235
|
+
}
|
|
2236
|
+
const invalidChars = /[<>:"|*?]/;
|
|
2237
|
+
if (invalidChars.test(folderName)) {
|
|
2238
|
+
throw new Error(`Folder name contains invalid characters: ${folderName}`);
|
|
2239
|
+
}
|
|
2240
|
+
const systemPaths = [
|
|
2241
|
+
"bin",
|
|
2242
|
+
"boot",
|
|
2243
|
+
"dev",
|
|
2244
|
+
"etc",
|
|
2245
|
+
"lib",
|
|
2246
|
+
"lib64",
|
|
2247
|
+
"mnt",
|
|
2248
|
+
"opt",
|
|
2249
|
+
"proc",
|
|
2250
|
+
"root",
|
|
2251
|
+
"run",
|
|
2252
|
+
"sbin",
|
|
2253
|
+
"srv",
|
|
2254
|
+
"sys",
|
|
2255
|
+
"tmp",
|
|
2256
|
+
"usr",
|
|
2257
|
+
"var",
|
|
2258
|
+
"System",
|
|
2259
|
+
"Windows",
|
|
2260
|
+
"Program Files",
|
|
2261
|
+
"Program Files (x86)"
|
|
2262
|
+
];
|
|
2263
|
+
if (systemPaths.includes(folderName)) {
|
|
2264
|
+
throw new Error(`Cannot create system directory: ${folderName}`);
|
|
2265
|
+
}
|
|
2266
|
+
if (folderName.trim() === "" || /^\.+$/.test(folderName)) {
|
|
2267
|
+
throw new Error("Invalid folder name");
|
|
2268
|
+
}
|
|
2269
|
+
}
|
|
2270
|
+
const Route$4 = createFileRoute("/api/files/mkdir")({
|
|
2271
|
+
server: {
|
|
2272
|
+
handlers: {
|
|
2273
|
+
POST: async ({ request }) => {
|
|
2274
|
+
try {
|
|
2275
|
+
const body = await request.json().catch(() => ({}));
|
|
2276
|
+
const rawPath = typeof body.path === "string" ? body.path.trim() : "";
|
|
2277
|
+
if (!rawPath) {
|
|
2278
|
+
return json(
|
|
2279
|
+
{
|
|
2280
|
+
error: "path is required",
|
|
2281
|
+
code: "MISSING_PATH"
|
|
2282
|
+
},
|
|
2283
|
+
{ status: 400 }
|
|
2284
|
+
);
|
|
2285
|
+
}
|
|
2286
|
+
const path2 = validatePath(rawPath, "Path parameter");
|
|
2287
|
+
validateFolderName(path2);
|
|
2288
|
+
await createFolder(path2);
|
|
2289
|
+
return json({
|
|
2290
|
+
ok: true,
|
|
2291
|
+
message: "Directory created successfully",
|
|
2292
|
+
path: path2
|
|
2293
|
+
});
|
|
2294
|
+
} catch (err) {
|
|
2295
|
+
const error = err;
|
|
2296
|
+
if (error.message.includes("invalid characters") || error.message.includes("traversal attempts") || error.message.includes("Invalid folder") || error.message.includes("Cannot create system") || error.message.includes("Folder name")) {
|
|
2297
|
+
return json(
|
|
2298
|
+
{
|
|
2299
|
+
error: error.message,
|
|
2300
|
+
code: "VALIDATION_ERROR"
|
|
2301
|
+
},
|
|
2302
|
+
{ status: 400 }
|
|
2303
|
+
);
|
|
2304
|
+
}
|
|
2305
|
+
const status = error.status || 500;
|
|
2306
|
+
const code = error.code || "INTERNAL_ERROR";
|
|
2307
|
+
return json(
|
|
2308
|
+
{
|
|
2309
|
+
error: error.message || "An unexpected error occurred",
|
|
2310
|
+
code
|
|
2311
|
+
},
|
|
2312
|
+
{ status }
|
|
2313
|
+
);
|
|
2314
|
+
}
|
|
2315
|
+
}
|
|
2316
|
+
}
|
|
2317
|
+
}
|
|
2318
|
+
});
|
|
2319
|
+
const Route$3 = createFileRoute("/api/files/list")({
|
|
2320
|
+
server: {
|
|
2321
|
+
handlers: {
|
|
2322
|
+
GET: async ({ request }) => {
|
|
2323
|
+
try {
|
|
2324
|
+
const url = new URL(request.url);
|
|
2325
|
+
const rawPath = url.searchParams.get("path") || "/";
|
|
2326
|
+
const path2 = validatePath(rawPath, "Path parameter");
|
|
2327
|
+
const listing = await listFiles(path2);
|
|
2328
|
+
return json(listing);
|
|
2329
|
+
} catch (err) {
|
|
2330
|
+
const error = err;
|
|
2331
|
+
if (error.message.includes("invalid characters") || error.message.includes("traversal attempts")) {
|
|
2332
|
+
return json(
|
|
2333
|
+
{
|
|
2334
|
+
error: error.message,
|
|
2335
|
+
code: "INVALID_PATH"
|
|
2336
|
+
},
|
|
2337
|
+
{ status: 400 }
|
|
2338
|
+
);
|
|
2339
|
+
}
|
|
2340
|
+
const status = error.status || 500;
|
|
2341
|
+
const code = error.code || "INTERNAL_ERROR";
|
|
2342
|
+
return json(
|
|
2343
|
+
{
|
|
2344
|
+
error: error.message || "An unexpected error occurred",
|
|
2345
|
+
code
|
|
2346
|
+
},
|
|
2347
|
+
{ status }
|
|
2348
|
+
);
|
|
2349
|
+
}
|
|
2350
|
+
}
|
|
2351
|
+
}
|
|
2352
|
+
}
|
|
2353
|
+
});
|
|
2354
|
+
const Route$2 = createFileRoute("/api/files/info")({
|
|
2355
|
+
server: {
|
|
2356
|
+
handlers: {
|
|
2357
|
+
GET: async ({ request }) => {
|
|
2358
|
+
try {
|
|
2359
|
+
const url = new URL(request.url);
|
|
2360
|
+
const rawPath = url.searchParams.get("path");
|
|
2361
|
+
if (!rawPath) {
|
|
2362
|
+
return json(
|
|
2363
|
+
{
|
|
2364
|
+
error: "path parameter is required",
|
|
2365
|
+
code: "MISSING_PATH"
|
|
2366
|
+
},
|
|
2367
|
+
{ status: 400 }
|
|
2368
|
+
);
|
|
2369
|
+
}
|
|
2370
|
+
const path2 = validatePath(rawPath, "Path parameter");
|
|
2371
|
+
const fileInfo = await getFileInfo(path2);
|
|
2372
|
+
return json(fileInfo);
|
|
2373
|
+
} catch (err) {
|
|
2374
|
+
const error = err;
|
|
2375
|
+
if (error.message.includes("invalid characters") || error.message.includes("traversal attempts")) {
|
|
2376
|
+
return json(
|
|
2377
|
+
{
|
|
2378
|
+
error: error.message,
|
|
2379
|
+
code: "INVALID_PATH"
|
|
2380
|
+
},
|
|
2381
|
+
{ status: 400 }
|
|
2382
|
+
);
|
|
2383
|
+
}
|
|
2384
|
+
const status = error.status || 500;
|
|
2385
|
+
const code = error.code || "INTERNAL_ERROR";
|
|
2386
|
+
return json(
|
|
2387
|
+
{
|
|
2388
|
+
error: error.message || "An unexpected error occurred",
|
|
2389
|
+
code
|
|
2390
|
+
},
|
|
2391
|
+
{ status }
|
|
2392
|
+
);
|
|
2393
|
+
}
|
|
2394
|
+
}
|
|
2395
|
+
}
|
|
2396
|
+
}
|
|
2397
|
+
});
|
|
2398
|
+
function getContentType(filename) {
|
|
2399
|
+
const ext = filename.split(".").pop()?.toLowerCase() || "";
|
|
2400
|
+
const mimeTypes = {
|
|
2401
|
+
// Text files
|
|
2402
|
+
"txt": "text/plain",
|
|
2403
|
+
"md": "text/markdown",
|
|
2404
|
+
"html": "text/html",
|
|
2405
|
+
"css": "text/css",
|
|
2406
|
+
"js": "text/javascript",
|
|
2407
|
+
"json": "application/json",
|
|
2408
|
+
"xml": "application/xml",
|
|
2409
|
+
"csv": "text/csv",
|
|
2410
|
+
"yaml": "application/x-yaml",
|
|
2411
|
+
"yml": "application/x-yaml",
|
|
2412
|
+
// Programming languages
|
|
2413
|
+
"ts": "application/typescript",
|
|
2414
|
+
"tsx": "application/typescript",
|
|
2415
|
+
"py": "text/x-python",
|
|
2416
|
+
// Images
|
|
2417
|
+
"png": "image/png",
|
|
2418
|
+
"jpg": "image/jpeg",
|
|
2419
|
+
"jpeg": "image/jpeg",
|
|
2420
|
+
"gif": "image/gif",
|
|
2421
|
+
"webp": "image/webp",
|
|
2422
|
+
"svg": "image/svg+xml",
|
|
2423
|
+
// Audio/Video
|
|
2424
|
+
"mp3": "audio/mpeg",
|
|
2425
|
+
"mp4": "video/mp4",
|
|
2426
|
+
// Documents
|
|
2427
|
+
"pdf": "application/pdf",
|
|
2428
|
+
"doc": "application/msword",
|
|
2429
|
+
"docx": "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
|
|
2430
|
+
"xls": "application/vnd.ms-excel",
|
|
2431
|
+
"xlsx": "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
|
2432
|
+
"ppt": "application/vnd.ms-powerpoint",
|
|
2433
|
+
"pptx": "application/vnd.openxmlformats-officedocument.presentationml.presentation",
|
|
2434
|
+
// Archives
|
|
2435
|
+
"zip": "application/zip",
|
|
2436
|
+
"tar.gz": "application/gzip",
|
|
2437
|
+
"gz": "application/gzip",
|
|
2438
|
+
// Fonts
|
|
2439
|
+
"woff": "font/woff",
|
|
2440
|
+
"woff2": "font/woff2",
|
|
2441
|
+
"ttf": "font/ttf",
|
|
2442
|
+
"otf": "font/otf"
|
|
2443
|
+
};
|
|
2444
|
+
return mimeTypes[ext] || "application/octet-stream";
|
|
2445
|
+
}
|
|
2446
|
+
function encodeFilename(filename) {
|
|
2447
|
+
const encodedFilename = encodeURIComponent(filename);
|
|
2448
|
+
return `filename*=UTF-8''${encodedFilename}`;
|
|
2449
|
+
}
|
|
2450
|
+
function isStaticAsset(filename) {
|
|
2451
|
+
const ext = filename.split(".").pop()?.toLowerCase() || "";
|
|
2452
|
+
const staticExts = ["png", "jpg", "jpeg", "gif", "webp", "svg", "css", "js", "woff", "woff2", "ttf", "otf"];
|
|
2453
|
+
return staticExts.includes(ext);
|
|
2454
|
+
}
|
|
2455
|
+
const Route$1 = createFileRoute("/api/files/download")({
|
|
2456
|
+
server: {
|
|
2457
|
+
handlers: {
|
|
2458
|
+
GET: async ({ request }) => {
|
|
2459
|
+
try {
|
|
2460
|
+
const url = new URL(request.url);
|
|
2461
|
+
const rawPath = url.searchParams.get("path");
|
|
2462
|
+
if (!rawPath) {
|
|
2463
|
+
return new Response(
|
|
2464
|
+
JSON.stringify({
|
|
2465
|
+
error: "path parameter is required",
|
|
2466
|
+
code: "MISSING_PATH"
|
|
2467
|
+
}),
|
|
2468
|
+
{
|
|
2469
|
+
status: 400,
|
|
2470
|
+
headers: { "Content-Type": "application/json" }
|
|
2471
|
+
}
|
|
2472
|
+
);
|
|
2473
|
+
}
|
|
2474
|
+
const path2 = validatePath(rawPath, "Path parameter");
|
|
2475
|
+
const fileInfo = await getFileInfo(path2);
|
|
2476
|
+
const content = await downloadFile(path2);
|
|
2477
|
+
const contentType = getContentType(fileInfo.name);
|
|
2478
|
+
const contentDisposition = `attachment; ${encodeFilename(fileInfo.name)}`;
|
|
2479
|
+
const headers = {
|
|
2480
|
+
"Content-Type": contentType,
|
|
2481
|
+
"Content-Disposition": contentDisposition,
|
|
2482
|
+
"Content-Length": content.byteLength.toString()
|
|
2483
|
+
};
|
|
2484
|
+
if (isStaticAsset(fileInfo.name)) {
|
|
2485
|
+
headers["Cache-Control"] = "public, max-age=31536000";
|
|
2486
|
+
} else {
|
|
2487
|
+
headers["Cache-Control"] = "no-cache";
|
|
2488
|
+
}
|
|
2489
|
+
return new Response(content, { headers });
|
|
2490
|
+
} catch (err) {
|
|
2491
|
+
const error = err;
|
|
2492
|
+
if (error.message.includes("invalid characters") || error.message.includes("traversal attempts")) {
|
|
2493
|
+
return new Response(
|
|
2494
|
+
JSON.stringify({
|
|
2495
|
+
error: error.message,
|
|
2496
|
+
code: "INVALID_PATH"
|
|
2497
|
+
}),
|
|
2498
|
+
{
|
|
2499
|
+
status: 400,
|
|
2500
|
+
headers: { "Content-Type": "application/json" }
|
|
2501
|
+
}
|
|
2502
|
+
);
|
|
2503
|
+
}
|
|
2504
|
+
const status = error.status || 500;
|
|
2505
|
+
const code = error.code || "INTERNAL_ERROR";
|
|
2506
|
+
return new Response(
|
|
2507
|
+
JSON.stringify({
|
|
2508
|
+
error: error.message || "An unexpected error occurred",
|
|
2509
|
+
code
|
|
2510
|
+
}),
|
|
2511
|
+
{
|
|
2512
|
+
status,
|
|
2513
|
+
headers: { "Content-Type": "application/json" }
|
|
2514
|
+
}
|
|
2515
|
+
);
|
|
2516
|
+
}
|
|
2517
|
+
}
|
|
2518
|
+
}
|
|
2519
|
+
}
|
|
2520
|
+
});
|
|
2521
|
+
const Route = createFileRoute("/api/files/delete")({
|
|
2522
|
+
server: {
|
|
2523
|
+
handlers: {
|
|
2524
|
+
DELETE: async ({ request }) => {
|
|
2525
|
+
try {
|
|
2526
|
+
const url = new URL(request.url);
|
|
2527
|
+
const rawPath = url.searchParams.get("path");
|
|
2528
|
+
if (!rawPath) {
|
|
2529
|
+
return json(
|
|
2530
|
+
{
|
|
2531
|
+
error: "path parameter is required",
|
|
2532
|
+
code: "MISSING_PATH"
|
|
2533
|
+
},
|
|
2534
|
+
{ status: 400 }
|
|
2535
|
+
);
|
|
2536
|
+
}
|
|
2537
|
+
const path2 = validatePath(rawPath, "Path parameter");
|
|
2538
|
+
if (path2 === "/" || path2 === "") {
|
|
2539
|
+
return json(
|
|
2540
|
+
{
|
|
2541
|
+
error: "Cannot delete the root directory",
|
|
2542
|
+
code: "FORBIDDEN_DELETE_ROOT"
|
|
2543
|
+
},
|
|
2544
|
+
{ status: 403 }
|
|
2545
|
+
// Forbidden
|
|
2546
|
+
);
|
|
2547
|
+
}
|
|
2548
|
+
await deleteFile(path2);
|
|
2549
|
+
return json({
|
|
2550
|
+
ok: true,
|
|
2551
|
+
message: "File deleted successfully",
|
|
2552
|
+
path: path2
|
|
2553
|
+
});
|
|
2554
|
+
} catch (err) {
|
|
2555
|
+
const error = err;
|
|
2556
|
+
if (error.message.includes("invalid characters") || error.message.includes("traversal attempts") || error.message.includes("Cannot delete")) {
|
|
2557
|
+
return json(
|
|
2558
|
+
{
|
|
2559
|
+
error: error.message,
|
|
2560
|
+
code: "VALIDATION_ERROR"
|
|
2561
|
+
},
|
|
2562
|
+
{ status: 400 }
|
|
2563
|
+
);
|
|
2564
|
+
}
|
|
2565
|
+
const status = error.status || 500;
|
|
2566
|
+
const code = error.code || "INTERNAL_ERROR";
|
|
2567
|
+
return json(
|
|
2568
|
+
{
|
|
2569
|
+
error: error.message || "An unexpected error occurred",
|
|
2570
|
+
code
|
|
2571
|
+
},
|
|
2572
|
+
{ status }
|
|
2573
|
+
);
|
|
2574
|
+
}
|
|
2575
|
+
}
|
|
2576
|
+
}
|
|
2577
|
+
}
|
|
2578
|
+
});
|
|
2579
|
+
const NewRoute = Route$o.update({
|
|
2580
|
+
id: "/new",
|
|
2581
|
+
path: "/new",
|
|
2582
|
+
getParentRoute: () => Route$p
|
|
2583
|
+
});
|
|
2584
|
+
const FilesRoute = Route$n.update({
|
|
2585
|
+
id: "/files",
|
|
2586
|
+
path: "/files",
|
|
2587
|
+
getParentRoute: () => Route$p
|
|
2588
|
+
});
|
|
2589
|
+
const ConnectRoute = Route$m.update({
|
|
2590
|
+
id: "/connect",
|
|
2591
|
+
path: "/connect",
|
|
2592
|
+
getParentRoute: () => Route$p
|
|
2593
|
+
});
|
|
2594
|
+
const IndexRoute = Route$l.update({
|
|
2595
|
+
id: "/",
|
|
2596
|
+
path: "/",
|
|
2597
|
+
getParentRoute: () => Route$p
|
|
2598
|
+
});
|
|
2599
|
+
const ChatSessionKeyRoute = Route$k.update({
|
|
2600
|
+
id: "/chat/$sessionKey",
|
|
2601
|
+
path: "/chat/$sessionKey",
|
|
2602
|
+
getParentRoute: () => Route$p
|
|
2603
|
+
});
|
|
2604
|
+
const ApiTtsRoute = Route$j.update({
|
|
2605
|
+
id: "/api/tts",
|
|
2606
|
+
path: "/api/tts",
|
|
2607
|
+
getParentRoute: () => Route$p
|
|
2608
|
+
});
|
|
2609
|
+
const ApiStreamRoute = Route$i.update({
|
|
2610
|
+
id: "/api/stream",
|
|
2611
|
+
path: "/api/stream",
|
|
2612
|
+
getParentRoute: () => Route$p
|
|
2613
|
+
});
|
|
2614
|
+
const ApiSessionsRoute = Route$h.update({
|
|
2615
|
+
id: "/api/sessions",
|
|
2616
|
+
path: "/api/sessions",
|
|
2617
|
+
getParentRoute: () => Route$p
|
|
2618
|
+
});
|
|
2619
|
+
const ApiSendRoute = Route$g.update({
|
|
2620
|
+
id: "/api/send",
|
|
2621
|
+
path: "/api/send",
|
|
2622
|
+
getParentRoute: () => Route$p
|
|
2623
|
+
});
|
|
2624
|
+
const ApiPingRoute = Route$f.update({
|
|
2625
|
+
id: "/api/ping",
|
|
2626
|
+
path: "/api/ping",
|
|
2627
|
+
getParentRoute: () => Route$p
|
|
2628
|
+
});
|
|
2629
|
+
const ApiPersonasRoute = Route$e.update({
|
|
2630
|
+
id: "/api/personas",
|
|
2631
|
+
path: "/api/personas",
|
|
2632
|
+
getParentRoute: () => Route$p
|
|
2633
|
+
});
|
|
2634
|
+
const ApiPathsRoute = Route$d.update({
|
|
2635
|
+
id: "/api/paths",
|
|
2636
|
+
path: "/api/paths",
|
|
2637
|
+
getParentRoute: () => Route$p
|
|
2638
|
+
});
|
|
2639
|
+
const ApiModelsRoute = Route$c.update({
|
|
2640
|
+
id: "/api/models",
|
|
2641
|
+
path: "/api/models",
|
|
2642
|
+
getParentRoute: () => Route$p
|
|
2643
|
+
});
|
|
2644
|
+
const ApiLlmFeaturesRoute = Route$b.update({
|
|
2645
|
+
id: "/api/llm-features",
|
|
2646
|
+
path: "/api/llm-features",
|
|
2647
|
+
getParentRoute: () => Route$p
|
|
2648
|
+
});
|
|
2649
|
+
const ApiHistoryRoute = Route$a.update({
|
|
2650
|
+
id: "/api/history",
|
|
2651
|
+
path: "/api/history",
|
|
2652
|
+
getParentRoute: () => Route$p
|
|
2653
|
+
});
|
|
2654
|
+
const ApiFollowUpsRoute = Route$9.update({
|
|
2655
|
+
id: "/api/follow-ups",
|
|
2656
|
+
path: "/api/follow-ups",
|
|
2657
|
+
getParentRoute: () => Route$p
|
|
2658
|
+
});
|
|
2659
|
+
const ApiFilesUploadRoute = Route$8.update({
|
|
2660
|
+
id: "/api/files/upload",
|
|
2661
|
+
path: "/api/files/upload",
|
|
2662
|
+
getParentRoute: () => Route$p
|
|
2663
|
+
});
|
|
2664
|
+
const ApiFilesSaveRoute = Route$7.update({
|
|
2665
|
+
id: "/api/files/save",
|
|
2666
|
+
path: "/api/files/save",
|
|
2667
|
+
getParentRoute: () => Route$p
|
|
2668
|
+
});
|
|
2669
|
+
const ApiFilesRenameRoute = Route$6.update({
|
|
2670
|
+
id: "/api/files/rename",
|
|
2671
|
+
path: "/api/files/rename",
|
|
2672
|
+
getParentRoute: () => Route$p
|
|
2673
|
+
});
|
|
2674
|
+
const ApiFilesReadRoute = Route$5.update({
|
|
2675
|
+
id: "/api/files/read",
|
|
2676
|
+
path: "/api/files/read",
|
|
2677
|
+
getParentRoute: () => Route$p
|
|
2678
|
+
});
|
|
2679
|
+
const ApiFilesMkdirRoute = Route$4.update({
|
|
2680
|
+
id: "/api/files/mkdir",
|
|
2681
|
+
path: "/api/files/mkdir",
|
|
2682
|
+
getParentRoute: () => Route$p
|
|
2683
|
+
});
|
|
2684
|
+
const ApiFilesListRoute = Route$3.update({
|
|
2685
|
+
id: "/api/files/list",
|
|
2686
|
+
path: "/api/files/list",
|
|
2687
|
+
getParentRoute: () => Route$p
|
|
2688
|
+
});
|
|
2689
|
+
const ApiFilesInfoRoute = Route$2.update({
|
|
2690
|
+
id: "/api/files/info",
|
|
2691
|
+
path: "/api/files/info",
|
|
2692
|
+
getParentRoute: () => Route$p
|
|
2693
|
+
});
|
|
2694
|
+
const ApiFilesDownloadRoute = Route$1.update({
|
|
2695
|
+
id: "/api/files/download",
|
|
2696
|
+
path: "/api/files/download",
|
|
2697
|
+
getParentRoute: () => Route$p
|
|
2698
|
+
});
|
|
2699
|
+
const ApiFilesDeleteRoute = Route.update({
|
|
2700
|
+
id: "/api/files/delete",
|
|
2701
|
+
path: "/api/files/delete",
|
|
2702
|
+
getParentRoute: () => Route$p
|
|
2703
|
+
});
|
|
2704
|
+
const rootRouteChildren = {
|
|
2705
|
+
IndexRoute,
|
|
2706
|
+
ConnectRoute,
|
|
2707
|
+
FilesRoute,
|
|
2708
|
+
NewRoute,
|
|
2709
|
+
ApiFollowUpsRoute,
|
|
2710
|
+
ApiHistoryRoute,
|
|
2711
|
+
ApiLlmFeaturesRoute,
|
|
2712
|
+
ApiModelsRoute,
|
|
2713
|
+
ApiPathsRoute,
|
|
2714
|
+
ApiPersonasRoute,
|
|
2715
|
+
ApiPingRoute,
|
|
2716
|
+
ApiSendRoute,
|
|
2717
|
+
ApiSessionsRoute,
|
|
2718
|
+
ApiStreamRoute,
|
|
2719
|
+
ApiTtsRoute,
|
|
2720
|
+
ChatSessionKeyRoute,
|
|
2721
|
+
ApiFilesDeleteRoute,
|
|
2722
|
+
ApiFilesDownloadRoute,
|
|
2723
|
+
ApiFilesInfoRoute,
|
|
2724
|
+
ApiFilesListRoute,
|
|
2725
|
+
ApiFilesMkdirRoute,
|
|
2726
|
+
ApiFilesReadRoute,
|
|
2727
|
+
ApiFilesRenameRoute,
|
|
2728
|
+
ApiFilesSaveRoute,
|
|
2729
|
+
ApiFilesUploadRoute
|
|
2730
|
+
};
|
|
2731
|
+
const routeTree = Route$p._addFileChildren(rootRouteChildren)._addFileTypes();
|
|
2732
|
+
const getRouter = () => {
|
|
2733
|
+
const router2 = createRouter({
|
|
2734
|
+
routeTree,
|
|
2735
|
+
context: {},
|
|
2736
|
+
scrollRestoration: true,
|
|
2737
|
+
defaultPreloadStaleTime: 0
|
|
2738
|
+
});
|
|
2739
|
+
return router2;
|
|
2740
|
+
};
|
|
2741
|
+
const router = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({
|
|
2742
|
+
__proto__: null,
|
|
2743
|
+
getRouter
|
|
2744
|
+
}, Symbol.toStringTag, { value: "Module" }));
|
|
2745
|
+
export {
|
|
2746
|
+
Route$l as R,
|
|
2747
|
+
Route$k as a,
|
|
2748
|
+
router as r
|
|
2749
|
+
};
|