@cuongtran001/kanna 0.39.2
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 +355 -0
- package/bin/kanna +9 -0
- package/dist/client/apple-touch-icon.png +0 -0
- package/dist/client/assets/abap-BdImnpbu.js +1 -0
- package/dist/client/assets/actionscript-3-CoDkCxhg.js +1 -0
- package/dist/client/assets/ada-bCR0ucgS.js +1 -0
- package/dist/client/assets/andromeeda-C4gqWexZ.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-Ve4PFQV2.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-DYE7WIF3.js +1 -0
- package/dist/client/assets/ayu-light-BA47KaF1.js +1 -0
- package/dist/client/assets/ayu-mirage-32ctXXKs.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/bird2-DPOp833l.js +1 -0
- package/dist/client/assets/blade-D4QpJJKB.js +1 -0
- package/dist/client/assets/bricolage-grotesque-latin-ext-wght-normal-CcLUaPy7.woff2 +0 -0
- package/dist/client/assets/bricolage-grotesque-latin-wght-normal-DLoelf7F.woff2 +0 -0
- package/dist/client/assets/bricolage-grotesque-vietnamese-wght-normal-BUzh504Q.woff2 +0 -0
- package/dist/client/assets/bsl-BO_Y6i37.js +1 -0
- package/dist/client/assets/c-BIGW1oBm.js +1 -0
- package/dist/client/assets/c3-eo99z4R2.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/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-COcwbKMJ.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-B12qg9BL.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/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-BOOCDP_w.js +1 -0
- package/dist/client/assets/gdscript-C5YyOfLZ.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-CxLEBnE3.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/horizon-BUw7H-hv.js +1 -0
- package/dist/client/assets/horizon-bright-Cn-bp-IR.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-Do7324M0.css +32 -0
- package/dist/client/assets/index-ktE9DLCD.js +2620 -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/just-Cw27pwNe.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/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-CWtU0Tv5.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-DjAJT7YJ.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-BaeVxFsk.js +1 -0
- package/dist/client/assets/luau-C-HG3fhB.js +1 -0
- package/dist/client/assets/make-CHLpvVh8.js +1 -0
- package/dist/client/assets/markdown-Cvjx9yec.js +1 -0
- package/dist/client/assets/marko-CnJfTvn9.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-BMNejdWA.js +1 -0
- package/dist/client/assets/mdx-Cmh6b_Ma.js +1 -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-rZm6bMo-.js +1 -0
- package/dist/client/assets/monokai-D4h5O-jR.js +1 -0
- package/dist/client/assets/moonbit-_H4v1dQx.js +1 -0
- package/dist/client/assets/move-IF9eRakj.js +1 -0
- package/dist/client/assets/narrat-DRg8JJMk.js +1 -0
- package/dist/client/assets/nextflow-Zz6hmt5N.js +1 -0
- package/dist/client/assets/nextflow-groovy-BeH2EWoN.js +1 -0
- package/dist/client/assets/nginx-BpAMiNFr.js +1 -0
- package/dist/client/assets/night-owl-C39BiMTA.js +1 -0
- package/dist/client/assets/night-owl-light-CMTm3GFP.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-Cz2AlsmD.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/odin-BBf5iR-q.js +1 -0
- package/dist/client/assets/one-dark-pro-DVMEJ2y_.js +1 -0
- package/dist/client/assets/one-light-C3Wv6jpd.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-Dhbhpdrm.js +1 -0
- package/dist/client/assets/pierre-dark-DF2SEV7i.js +1 -0
- package/dist/client/assets/pierre-light-DOlZxES8.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-Uh8Bk_45.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/ron-D8l8udqQ.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-BrH8l1NY.js +1 -0
- package/dist/client/assets/ruby-Dw2BHqvy.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/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/stylus-BEDo0Tqx.js +1 -0
- package/dist/client/assets/surrealql-Bq5Q-fJD.js +1 -0
- package/dist/client/assets/svelte-C_ipcX3V.js +1 -0
- package/dist/client/assets/swift-D82vCrfD.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-P3uqSqPl.js +1 -0
- package/dist/client/assets/terraform-BETggiCN.js +1 -0
- package/dist/client/assets/tex-idrVyKtj.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-DNn4PbVi.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/chat-sounds/Blow.mp3 +0 -0
- package/dist/client/chat-sounds/Bottle.mp3 +0 -0
- package/dist/client/chat-sounds/Frog.mp3 +0 -0
- package/dist/client/chat-sounds/Funk.mp3 +0 -0
- package/dist/client/chat-sounds/Glass.mp3 +0 -0
- package/dist/client/chat-sounds/Ping.mp3 +0 -0
- package/dist/client/chat-sounds/Pop.mp3 +0 -0
- package/dist/client/chat-sounds/Purr.mp3 +0 -0
- package/dist/client/chat-sounds/Tink.mp3 +0 -0
- package/dist/client/editor-icons/cursor.png +0 -0
- package/dist/client/editor-icons/custom.png +0 -0
- package/dist/client/editor-icons/default-app.png +0 -0
- package/dist/client/editor-icons/finder.png +0 -0
- package/dist/client/editor-icons/preview.png +0 -0
- package/dist/client/editor-icons/terminal.png +0 -0
- package/dist/client/editor-icons/windsurf.png +0 -0
- package/dist/client/editor-icons/xcode.png +0 -0
- package/dist/client/favicon.png +0 -0
- package/dist/client/fonts/body-medium.woff2 +0 -0
- package/dist/client/fonts/body-regular-italic.woff2 +0 -0
- package/dist/client/fonts/body-regular.woff2 +0 -0
- package/dist/client/fonts/body-semibold.woff2 +0 -0
- package/dist/client/icon-192.png +0 -0
- package/dist/client/icon-512.png +0 -0
- package/dist/client/icon-maskable-512.png +0 -0
- package/dist/client/icon.svg +4 -0
- package/dist/client/index.html +34 -0
- package/dist/client/manifest.webmanifest +46 -0
- package/dist/client/screenshot-light.png +0 -0
- package/dist/client/screenshot.png +0 -0
- package/dist/export-viewer/assets/bricolage-grotesque-latin-ext-wght-normal-CcLUaPy7.woff2 +0 -0
- package/dist/export-viewer/assets/bricolage-grotesque-latin-wght-normal-DLoelf7F.woff2 +0 -0
- package/dist/export-viewer/assets/bricolage-grotesque-vietnamese-wght-normal-BUzh504Q.woff2 +0 -0
- package/dist/export-viewer/assets/index-D1qUumZR.js +410 -0
- package/dist/export-viewer/assets/index-gG2nMW51.css +1 -0
- package/dist/export-viewer/editor-icons/cursor.png +0 -0
- package/dist/export-viewer/editor-icons/custom.png +0 -0
- package/dist/export-viewer/editor-icons/default-app.png +0 -0
- package/dist/export-viewer/editor-icons/finder.png +0 -0
- package/dist/export-viewer/editor-icons/preview.png +0 -0
- package/dist/export-viewer/editor-icons/terminal.png +0 -0
- package/dist/export-viewer/editor-icons/windsurf.png +0 -0
- package/dist/export-viewer/editor-icons/xcode.png +0 -0
- package/dist/export-viewer/fonts/body-medium.woff2 +0 -0
- package/dist/export-viewer/fonts/body-regular-italic.woff2 +0 -0
- package/dist/export-viewer/fonts/body-regular.woff2 +0 -0
- package/dist/export-viewer/fonts/body-semibold.woff2 +0 -0
- package/dist/export-viewer/index.html +14 -0
- package/package.json +99 -0
- package/src/server/__fixtures__/claude-session-empty.jsonl +0 -0
- package/src/server/__fixtures__/claude-session-malformed.jsonl +3 -0
- package/src/server/__fixtures__/claude-session-valid.jsonl +6 -0
- package/src/server/agent.test.ts +2369 -0
- package/src/server/agent.ts +1927 -0
- package/src/server/analytics.test.ts +313 -0
- package/src/server/analytics.ts +131 -0
- package/src/server/app-settings.test.ts +233 -0
- package/src/server/app-settings.ts +548 -0
- package/src/server/auth.test.ts +329 -0
- package/src/server/auth.ts +204 -0
- package/src/server/auto-continue/e2e.test.ts +215 -0
- package/src/server/auto-continue/events.test.ts +30 -0
- package/src/server/auto-continue/events.ts +35 -0
- package/src/server/auto-continue/limit-detector.test.ts +153 -0
- package/src/server/auto-continue/limit-detector.ts +159 -0
- package/src/server/auto-continue/read-model.test.ts +109 -0
- package/src/server/auto-continue/read-model.ts +83 -0
- package/src/server/auto-continue/schedule-manager.test.ts +155 -0
- package/src/server/auto-continue/schedule-manager.ts +116 -0
- package/src/server/claude-session-importer.test.ts +214 -0
- package/src/server/claude-session-importer.ts +187 -0
- package/src/server/claude-session-mapper.test.ts +88 -0
- package/src/server/claude-session-mapper.ts +106 -0
- package/src/server/claude-session-parser.test.ts +38 -0
- package/src/server/claude-session-parser.ts +67 -0
- package/src/server/claude-session-scanner.test.ts +49 -0
- package/src/server/claude-session-scanner.ts +24 -0
- package/src/server/claude-session-types.ts +61 -0
- package/src/server/cli-runtime.test.ts +523 -0
- package/src/server/cli-runtime.ts +405 -0
- package/src/server/cli-supervisor.ts +102 -0
- package/src/server/cli.ts +64 -0
- package/src/server/cloudflare-tunnel/agent-integration.test.ts +76 -0
- package/src/server/cloudflare-tunnel/agent-integration.ts +55 -0
- package/src/server/cloudflare-tunnel/detector.test.ts +72 -0
- package/src/server/cloudflare-tunnel/detector.ts +44 -0
- package/src/server/cloudflare-tunnel/e2e.test.ts +194 -0
- package/src/server/cloudflare-tunnel/events.test.ts +43 -0
- package/src/server/cloudflare-tunnel/events.ts +31 -0
- package/src/server/cloudflare-tunnel/gateway.ts +143 -0
- package/src/server/cloudflare-tunnel/lifecycle.test.ts +48 -0
- package/src/server/cloudflare-tunnel/lifecycle.ts +62 -0
- package/src/server/cloudflare-tunnel/read-model.test.ts +69 -0
- package/src/server/cloudflare-tunnel/read-model.ts +80 -0
- package/src/server/cloudflare-tunnel/tunnel-manager.test.ts +116 -0
- package/src/server/cloudflare-tunnel/tunnel-manager.ts +165 -0
- package/src/server/codex-app-server-protocol.ts +487 -0
- package/src/server/codex-app-server.test.ts +1816 -0
- package/src/server/codex-app-server.ts +1475 -0
- package/src/server/diff-store.test.ts +737 -0
- package/src/server/diff-store.ts +2199 -0
- package/src/server/discovery.test.ts +211 -0
- package/src/server/discovery.ts +301 -0
- package/src/server/event-store.test.ts +797 -0
- package/src/server/event-store.ts +1421 -0
- package/src/server/events.ts +217 -0
- package/src/server/external-open.test.ts +112 -0
- package/src/server/external-open.ts +345 -0
- package/src/server/generate-commit-message.test.ts +79 -0
- package/src/server/generate-commit-message.ts +126 -0
- package/src/server/generate-title.ts +76 -0
- package/src/server/harness-types.ts +19 -0
- package/src/server/keybindings.test.ts +144 -0
- package/src/server/keybindings.ts +178 -0
- package/src/server/llm-provider.test.ts +134 -0
- package/src/server/llm-provider.ts +207 -0
- package/src/server/machine-name.ts +22 -0
- package/src/server/paths-route.test.ts +64 -0
- package/src/server/paths.ts +35 -0
- package/src/server/process-utils.test.ts +12 -0
- package/src/server/process-utils.ts +47 -0
- package/src/server/project-paths.test.ts +95 -0
- package/src/server/project-paths.ts +191 -0
- package/src/server/provider-catalog.test.ts +69 -0
- package/src/server/provider-catalog.ts +87 -0
- package/src/server/quick-response.test.ts +440 -0
- package/src/server/quick-response.ts +300 -0
- package/src/server/read-models.test.ts +509 -0
- package/src/server/read-models.ts +230 -0
- package/src/server/restart.test.ts +27 -0
- package/src/server/restart.ts +30 -0
- package/src/server/server.ts +616 -0
- package/src/server/share.test.ts +180 -0
- package/src/server/share.ts +150 -0
- package/src/server/standalone-export.test.ts +224 -0
- package/src/server/standalone-export.ts +419 -0
- package/src/server/terminal-manager.test.ts +315 -0
- package/src/server/terminal-manager.ts +350 -0
- package/src/server/test-helpers/async-event-queue.ts +52 -0
- package/src/server/test-helpers/wait-for.ts +14 -0
- package/src/server/title-generation.live.test.ts +44 -0
- package/src/server/update-manager.test.ts +158 -0
- package/src/server/update-manager.ts +222 -0
- package/src/server/update-strategy.test.ts +237 -0
- package/src/server/update-strategy.ts +241 -0
- package/src/server/uploads.test.ts +292 -0
- package/src/server/uploads.ts +131 -0
- package/src/server/ws-router.test.ts +2292 -0
- package/src/server/ws-router.ts +1465 -0
- package/src/shared/analytics.ts +30 -0
- package/src/shared/branding.test.ts +31 -0
- package/src/shared/branding.ts +77 -0
- package/src/shared/dev-ports.test.ts +113 -0
- package/src/shared/dev-ports.ts +134 -0
- package/src/shared/ports.ts +2 -0
- package/src/shared/protocol.ts +257 -0
- package/src/shared/share.ts +27 -0
- package/src/shared/tools.test.ts +164 -0
- package/src/shared/tools.ts +327 -0
- package/src/shared/types.test.ts +25 -0
- package/src/shared/types.ts +1088 -0
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import { afterEach, describe, expect, test } from "bun:test"
|
|
2
|
+
import { mkdtemp, rm, writeFile } from "node:fs/promises"
|
|
3
|
+
import { tmpdir } from "node:os"
|
|
4
|
+
import path from "node:path"
|
|
5
|
+
import { DEFAULT_OPENAI_SDK_MODEL, DEFAULT_OPENROUTER_SDK_MODEL } from "../shared/types"
|
|
6
|
+
import {
|
|
7
|
+
normalizeLlmProviderSnapshot,
|
|
8
|
+
OPENAI_BASE_URL,
|
|
9
|
+
OPENROUTER_BASE_URL,
|
|
10
|
+
readLlmProviderSnapshot,
|
|
11
|
+
resolveLlmProviderBaseUrl,
|
|
12
|
+
writeLlmProviderSnapshot,
|
|
13
|
+
} from "./llm-provider"
|
|
14
|
+
|
|
15
|
+
let tempDirs: string[] = []
|
|
16
|
+
const TEST_FILE_PATH = "/tmp/kanna-test-llm-provider.json"
|
|
17
|
+
|
|
18
|
+
afterEach(async () => {
|
|
19
|
+
await Promise.all(tempDirs.map((dir) => rm(dir, { recursive: true, force: true })))
|
|
20
|
+
tempDirs = []
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
async function createTempFilePath() {
|
|
24
|
+
const dir = await mkdtemp(path.join(tmpdir(), "kanna-llm-provider-"))
|
|
25
|
+
tempDirs.push(dir)
|
|
26
|
+
return path.join(dir, "llm-provider.json")
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
describe("resolveLlmProviderBaseUrl", () => {
|
|
30
|
+
test("derives known provider URLs and preserves custom URLs", () => {
|
|
31
|
+
expect(resolveLlmProviderBaseUrl("openai", "")).toBe(OPENAI_BASE_URL)
|
|
32
|
+
expect(resolveLlmProviderBaseUrl("openrouter", "")).toBe(OPENROUTER_BASE_URL)
|
|
33
|
+
expect(resolveLlmProviderBaseUrl("custom", " https://example.com/v1 ")).toBe("https://example.com/v1")
|
|
34
|
+
})
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
describe("normalizeLlmProviderSnapshot", () => {
|
|
38
|
+
test("normalizes a valid custom provider config", () => {
|
|
39
|
+
expect(normalizeLlmProviderSnapshot({
|
|
40
|
+
provider: "custom",
|
|
41
|
+
apiKey: " test-key ",
|
|
42
|
+
model: " gpt-test ",
|
|
43
|
+
baseUrl: " https://example.com/v1 ",
|
|
44
|
+
}, TEST_FILE_PATH)).toEqual({
|
|
45
|
+
provider: "custom",
|
|
46
|
+
apiKey: "test-key",
|
|
47
|
+
model: "gpt-test",
|
|
48
|
+
baseUrl: "https://example.com/v1",
|
|
49
|
+
resolvedBaseUrl: "https://example.com/v1",
|
|
50
|
+
enabled: true,
|
|
51
|
+
warning: null,
|
|
52
|
+
filePathDisplay: TEST_FILE_PATH,
|
|
53
|
+
})
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
test("disables invalid configs with a warning", () => {
|
|
57
|
+
const snapshot = normalizeLlmProviderSnapshot({
|
|
58
|
+
provider: "custom",
|
|
59
|
+
apiKey: "test-key",
|
|
60
|
+
model: "gpt-test",
|
|
61
|
+
baseUrl: "",
|
|
62
|
+
}, TEST_FILE_PATH)
|
|
63
|
+
|
|
64
|
+
expect(snapshot.enabled).toBe(false)
|
|
65
|
+
expect(snapshot.warning).toContain("custom provider requires a baseUrl")
|
|
66
|
+
})
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
describe("readLlmProviderSnapshot", () => {
|
|
70
|
+
test("returns defaults when the file does not exist", async () => {
|
|
71
|
+
const filePath = await createTempFilePath()
|
|
72
|
+
expect(await readLlmProviderSnapshot(filePath)).toEqual({
|
|
73
|
+
provider: "openai",
|
|
74
|
+
apiKey: "",
|
|
75
|
+
model: DEFAULT_OPENAI_SDK_MODEL,
|
|
76
|
+
baseUrl: "",
|
|
77
|
+
resolvedBaseUrl: OPENAI_BASE_URL,
|
|
78
|
+
enabled: false,
|
|
79
|
+
warning: null,
|
|
80
|
+
filePathDisplay: filePath,
|
|
81
|
+
})
|
|
82
|
+
})
|
|
83
|
+
|
|
84
|
+
test("returns a warning when the file contains invalid json", async () => {
|
|
85
|
+
const filePath = await createTempFilePath()
|
|
86
|
+
await writeFile(filePath, "{not-json", "utf8")
|
|
87
|
+
|
|
88
|
+
const snapshot = await readLlmProviderSnapshot(filePath)
|
|
89
|
+
expect(snapshot.enabled).toBe(false)
|
|
90
|
+
expect(snapshot.warning).toContain("invalid JSON")
|
|
91
|
+
})
|
|
92
|
+
|
|
93
|
+
test("fills named-provider default models when the file omits them", async () => {
|
|
94
|
+
const filePath = await createTempFilePath()
|
|
95
|
+
await writeFile(filePath, JSON.stringify({
|
|
96
|
+
provider: "openrouter",
|
|
97
|
+
apiKey: "test-key",
|
|
98
|
+
baseUrl: null,
|
|
99
|
+
}), "utf8")
|
|
100
|
+
|
|
101
|
+
const snapshot = await readLlmProviderSnapshot(filePath)
|
|
102
|
+
expect(snapshot.model).toBe(DEFAULT_OPENROUTER_SDK_MODEL)
|
|
103
|
+
expect(snapshot.enabled).toBe(true)
|
|
104
|
+
})
|
|
105
|
+
})
|
|
106
|
+
|
|
107
|
+
describe("writeLlmProviderSnapshot", () => {
|
|
108
|
+
test("writes normalized config to disk", async () => {
|
|
109
|
+
const filePath = await createTempFilePath()
|
|
110
|
+
const snapshot = await writeLlmProviderSnapshot({
|
|
111
|
+
provider: "openrouter",
|
|
112
|
+
apiKey: " test-key ",
|
|
113
|
+
model: " openrouter/model ",
|
|
114
|
+
baseUrl: "ignored",
|
|
115
|
+
}, filePath)
|
|
116
|
+
|
|
117
|
+
expect(snapshot).toEqual({
|
|
118
|
+
provider: "openrouter",
|
|
119
|
+
apiKey: "test-key",
|
|
120
|
+
model: "openrouter/model",
|
|
121
|
+
baseUrl: "ignored",
|
|
122
|
+
resolvedBaseUrl: OPENROUTER_BASE_URL,
|
|
123
|
+
enabled: true,
|
|
124
|
+
warning: null,
|
|
125
|
+
filePathDisplay: filePath,
|
|
126
|
+
})
|
|
127
|
+
expect(await Bun.file(filePath).json()).toEqual({
|
|
128
|
+
provider: "openrouter",
|
|
129
|
+
apiKey: "test-key",
|
|
130
|
+
model: "openrouter/model",
|
|
131
|
+
baseUrl: null,
|
|
132
|
+
})
|
|
133
|
+
})
|
|
134
|
+
})
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
import { mkdir, readFile, writeFile } from "node:fs/promises"
|
|
2
|
+
import { homedir } from "node:os"
|
|
3
|
+
import path from "node:path"
|
|
4
|
+
import OpenAI from "openai"
|
|
5
|
+
import { getLlmProviderFilePath } from "../shared/branding"
|
|
6
|
+
import {
|
|
7
|
+
DEFAULT_OPENAI_SDK_MODEL,
|
|
8
|
+
DEFAULT_OPENROUTER_SDK_MODEL,
|
|
9
|
+
type LlmProviderFile,
|
|
10
|
+
type LlmProviderKind,
|
|
11
|
+
type LlmProviderSnapshot,
|
|
12
|
+
type LlmProviderValidationResult,
|
|
13
|
+
} from "../shared/types"
|
|
14
|
+
|
|
15
|
+
export const OPENAI_BASE_URL = "https://api.openai.com/v1"
|
|
16
|
+
export const OPENROUTER_BASE_URL = "https://openrouter.ai/api/v1"
|
|
17
|
+
|
|
18
|
+
const DEFAULT_PROVIDER: LlmProviderKind = "openai"
|
|
19
|
+
|
|
20
|
+
function formatDisplayPath(filePath: string) {
|
|
21
|
+
const homePath = homedir()
|
|
22
|
+
if (filePath === homePath) return "~"
|
|
23
|
+
if (filePath.startsWith(`${homePath}${path.sep}`)) {
|
|
24
|
+
return `~${filePath.slice(homePath.length)}`
|
|
25
|
+
}
|
|
26
|
+
return filePath
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function resolveProvider(value: unknown) {
|
|
30
|
+
if (value === "openai" || value === "openrouter" || value === "custom") {
|
|
31
|
+
return value
|
|
32
|
+
}
|
|
33
|
+
return null
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function normalizeString(value: unknown) {
|
|
37
|
+
return typeof value === "string" ? value.trim() : ""
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function resolveLlmProviderBaseUrl(provider: LlmProviderKind, baseUrl: string) {
|
|
41
|
+
if (provider === "openai") return OPENAI_BASE_URL
|
|
42
|
+
if (provider === "openrouter") return OPENROUTER_BASE_URL
|
|
43
|
+
return baseUrl.trim()
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function resolveLlmProviderDefaultModel(provider: LlmProviderKind) {
|
|
47
|
+
if (provider === "openai") return DEFAULT_OPENAI_SDK_MODEL
|
|
48
|
+
if (provider === "openrouter") return DEFAULT_OPENROUTER_SDK_MODEL
|
|
49
|
+
return ""
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function normalizeLlmProviderSnapshot(
|
|
53
|
+
value: unknown,
|
|
54
|
+
filePath = getLlmProviderFilePath(homedir())
|
|
55
|
+
): LlmProviderSnapshot {
|
|
56
|
+
const source = value && typeof value === "object" && !Array.isArray(value)
|
|
57
|
+
? value as Record<string, unknown>
|
|
58
|
+
: null
|
|
59
|
+
const warnings: string[] = []
|
|
60
|
+
|
|
61
|
+
if (!source) {
|
|
62
|
+
return createDefaultSnapshot(
|
|
63
|
+
filePath,
|
|
64
|
+
value === undefined || value === null ? null : "LLM provider file must contain a JSON object. Using defaults."
|
|
65
|
+
)
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const provider = resolveProvider(source.provider)
|
|
69
|
+
const apiKey = normalizeString(source.apiKey)
|
|
70
|
+
const model = normalizeString(source.model)
|
|
71
|
+
const baseUrl = normalizeString(source.baseUrl)
|
|
72
|
+
|
|
73
|
+
if (!provider) {
|
|
74
|
+
warnings.push("provider must be one of openai, openrouter, or custom")
|
|
75
|
+
}
|
|
76
|
+
if (source.apiKey !== undefined && typeof source.apiKey !== "string") {
|
|
77
|
+
warnings.push("apiKey must be a string")
|
|
78
|
+
}
|
|
79
|
+
if (source.model !== undefined && typeof source.model !== "string") {
|
|
80
|
+
warnings.push("model must be a string")
|
|
81
|
+
}
|
|
82
|
+
if (source.baseUrl !== undefined && source.baseUrl !== null && typeof source.baseUrl !== "string") {
|
|
83
|
+
warnings.push("baseUrl must be a string or null")
|
|
84
|
+
}
|
|
85
|
+
if ((provider ?? DEFAULT_PROVIDER) === "custom" && !baseUrl) {
|
|
86
|
+
warnings.push("custom provider requires a baseUrl")
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const normalizedProvider = provider ?? DEFAULT_PROVIDER
|
|
90
|
+
const resolvedModel = model || resolveLlmProviderDefaultModel(normalizedProvider)
|
|
91
|
+
const resolvedBaseUrl = resolveLlmProviderBaseUrl(normalizedProvider, baseUrl)
|
|
92
|
+
const enabled = warnings.length === 0 && apiKey.length > 0 && resolvedModel.length > 0 && resolvedBaseUrl.length > 0
|
|
93
|
+
|
|
94
|
+
return {
|
|
95
|
+
provider: normalizedProvider,
|
|
96
|
+
apiKey,
|
|
97
|
+
model: resolvedModel,
|
|
98
|
+
baseUrl,
|
|
99
|
+
resolvedBaseUrl,
|
|
100
|
+
enabled,
|
|
101
|
+
warning: warnings.length > 0 ? `Some LLM provider settings are invalid: ${warnings.join("; ")}` : null,
|
|
102
|
+
filePathDisplay: formatDisplayPath(filePath),
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function createDefaultSnapshot(filePath: string, warning: string | null = null): LlmProviderSnapshot {
|
|
107
|
+
return {
|
|
108
|
+
provider: DEFAULT_PROVIDER,
|
|
109
|
+
apiKey: "",
|
|
110
|
+
model: DEFAULT_OPENAI_SDK_MODEL,
|
|
111
|
+
baseUrl: "",
|
|
112
|
+
resolvedBaseUrl: OPENAI_BASE_URL,
|
|
113
|
+
enabled: false,
|
|
114
|
+
warning,
|
|
115
|
+
filePathDisplay: formatDisplayPath(filePath),
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export async function readLlmProviderSnapshot(filePath = getLlmProviderFilePath(homedir())) {
|
|
120
|
+
try {
|
|
121
|
+
const text = await readFile(filePath, "utf8")
|
|
122
|
+
if (!text.trim()) {
|
|
123
|
+
return createDefaultSnapshot(filePath, "LLM provider file was empty. Using defaults.")
|
|
124
|
+
}
|
|
125
|
+
return normalizeLlmProviderSnapshot(JSON.parse(text), filePath)
|
|
126
|
+
} catch (error) {
|
|
127
|
+
if ((error as NodeJS.ErrnoException)?.code === "ENOENT") {
|
|
128
|
+
return createDefaultSnapshot(filePath)
|
|
129
|
+
}
|
|
130
|
+
if (error instanceof SyntaxError) {
|
|
131
|
+
return createDefaultSnapshot(filePath, "LLM provider file is invalid JSON. Using defaults.")
|
|
132
|
+
}
|
|
133
|
+
throw error
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
export async function writeLlmProviderSnapshot(
|
|
138
|
+
value: Pick<LlmProviderFile, "provider" | "apiKey" | "model"> & { baseUrl: string },
|
|
139
|
+
filePath = getLlmProviderFilePath(homedir())
|
|
140
|
+
) {
|
|
141
|
+
const snapshot = normalizeLlmProviderSnapshot(value, filePath)
|
|
142
|
+
const payload: LlmProviderFile = {
|
|
143
|
+
provider: snapshot.provider,
|
|
144
|
+
apiKey: snapshot.apiKey,
|
|
145
|
+
model: snapshot.model,
|
|
146
|
+
baseUrl: snapshot.provider === "custom" ? snapshot.baseUrl : null,
|
|
147
|
+
}
|
|
148
|
+
await mkdir(path.dirname(filePath), { recursive: true })
|
|
149
|
+
await writeFile(filePath, `${JSON.stringify(payload, null, 2)}\n`, "utf8")
|
|
150
|
+
return snapshot
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function toSerializableValue(value: unknown): unknown {
|
|
154
|
+
if (value === null || value === undefined) return value ?? null
|
|
155
|
+
if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") return value
|
|
156
|
+
if (Array.isArray(value)) {
|
|
157
|
+
return value.map((entry) => toSerializableValue(entry))
|
|
158
|
+
}
|
|
159
|
+
if (value instanceof Error) {
|
|
160
|
+
return toSerializableValue(Object.fromEntries(
|
|
161
|
+
Object.getOwnPropertyNames(value).map((key) => [key, (value as unknown as Record<string, unknown>)[key]])
|
|
162
|
+
))
|
|
163
|
+
}
|
|
164
|
+
if (typeof value === "object") {
|
|
165
|
+
const record = value as Record<string, unknown>
|
|
166
|
+
return Object.fromEntries(
|
|
167
|
+
Object.keys(record).map((key) => [key, toSerializableValue(record[key])])
|
|
168
|
+
)
|
|
169
|
+
}
|
|
170
|
+
return String(value)
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
export async function validateLlmProviderCredentials(
|
|
174
|
+
value: Pick<LlmProviderSnapshot, "provider" | "apiKey" | "model" | "baseUrl">
|
|
175
|
+
): Promise<LlmProviderValidationResult> {
|
|
176
|
+
const snapshot = normalizeLlmProviderSnapshot(value)
|
|
177
|
+
if (!snapshot.enabled) {
|
|
178
|
+
return {
|
|
179
|
+
ok: false,
|
|
180
|
+
error: {
|
|
181
|
+
type: "config_error",
|
|
182
|
+
message: snapshot.warning ?? "LLM provider configuration is incomplete.",
|
|
183
|
+
},
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
try {
|
|
188
|
+
const client = new OpenAI({
|
|
189
|
+
apiKey: snapshot.apiKey,
|
|
190
|
+
baseURL: snapshot.resolvedBaseUrl,
|
|
191
|
+
})
|
|
192
|
+
await client.responses.create({
|
|
193
|
+
model: snapshot.model,
|
|
194
|
+
input: "Reply with ok.",
|
|
195
|
+
max_output_tokens: 5,
|
|
196
|
+
})
|
|
197
|
+
return {
|
|
198
|
+
ok: true,
|
|
199
|
+
error: null,
|
|
200
|
+
}
|
|
201
|
+
} catch (error) {
|
|
202
|
+
return {
|
|
203
|
+
ok: false,
|
|
204
|
+
error: toSerializableValue(error),
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { hostname } from "node:os"
|
|
2
|
+
import process from "node:process"
|
|
3
|
+
import { spawnSync } from "node:child_process"
|
|
4
|
+
|
|
5
|
+
function runAndRead(command: string, args: string[]) {
|
|
6
|
+
const result = spawnSync(command, args, { encoding: "utf8" })
|
|
7
|
+
if (result.status !== 0) return null
|
|
8
|
+
const value = result.stdout.trim()
|
|
9
|
+
return value || null
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function getMachineDisplayName() {
|
|
13
|
+
if (process.platform === "darwin") {
|
|
14
|
+
const computerName = runAndRead("scutil", ["--get", "ComputerName"])
|
|
15
|
+
if (computerName) {
|
|
16
|
+
return computerName
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const rawHostname = hostname().trim()
|
|
21
|
+
return rawHostname.replace(/\.local$|\.lan$/i, "") || "This Machine"
|
|
22
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { afterEach, describe, expect, test } from "bun:test"
|
|
2
|
+
import { mkdtemp, rm, writeFile, mkdir } from "node:fs/promises"
|
|
3
|
+
import { tmpdir } from "node:os"
|
|
4
|
+
import path from "node:path"
|
|
5
|
+
import { startKannaServer } from "./server"
|
|
6
|
+
|
|
7
|
+
const tempDirs: string[] = []
|
|
8
|
+
|
|
9
|
+
afterEach(async () => {
|
|
10
|
+
await Promise.all(tempDirs.splice(0).map((d) => rm(d, { recursive: true, force: true })))
|
|
11
|
+
})
|
|
12
|
+
|
|
13
|
+
async function startServer(port: number) {
|
|
14
|
+
const projectDir = await mkdtemp(path.join(tmpdir(), "kanna-paths-route-"))
|
|
15
|
+
tempDirs.push(projectDir)
|
|
16
|
+
const server = await startKannaServer({ port, strictPort: true })
|
|
17
|
+
return { server, projectDir }
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
describe("GET /api/projects/:id/paths", () => {
|
|
21
|
+
test("returns 404 for unknown project", async () => {
|
|
22
|
+
const { server } = await startServer(4330)
|
|
23
|
+
try {
|
|
24
|
+
const response = await fetch(`http://localhost:${server.port}/api/projects/does-not-exist/paths`)
|
|
25
|
+
expect(response.status).toBe(404)
|
|
26
|
+
} finally {
|
|
27
|
+
await server.stop()
|
|
28
|
+
}
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
test("returns top-level entries for empty query", async () => {
|
|
32
|
+
const { server, projectDir } = await startServer(4331)
|
|
33
|
+
try {
|
|
34
|
+
await mkdir(path.join(projectDir, "src"))
|
|
35
|
+
await writeFile(path.join(projectDir, "README.md"), "")
|
|
36
|
+
|
|
37
|
+
const project = await server.store.openProject(projectDir, "t")
|
|
38
|
+
const response = await fetch(`http://localhost:${server.port}/api/projects/${project.id}/paths`)
|
|
39
|
+
expect(response.status).toBe(200)
|
|
40
|
+
const payload = await response.json() as { paths: Array<{ path: string; kind: string }> }
|
|
41
|
+
const names = payload.paths.map((p) => p.path)
|
|
42
|
+
expect(names).toContain("README.md")
|
|
43
|
+
expect(names).toContain("src/")
|
|
44
|
+
} finally {
|
|
45
|
+
await server.stop()
|
|
46
|
+
}
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
test("respects ?query= and ?limit=", async () => {
|
|
50
|
+
const { server, projectDir } = await startServer(4332)
|
|
51
|
+
try {
|
|
52
|
+
for (let i = 0; i < 5; i++) await writeFile(path.join(projectDir, `file-${i}.txt`), "")
|
|
53
|
+
|
|
54
|
+
const project = await server.store.openProject(projectDir, "t")
|
|
55
|
+
const response = await fetch(
|
|
56
|
+
`http://localhost:${server.port}/api/projects/${project.id}/paths?query=file&limit=2`,
|
|
57
|
+
)
|
|
58
|
+
const payload = await response.json() as { paths: Array<{ path: string }> }
|
|
59
|
+
expect(payload.paths.length).toBe(2)
|
|
60
|
+
} finally {
|
|
61
|
+
await server.stop()
|
|
62
|
+
}
|
|
63
|
+
})
|
|
64
|
+
})
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { mkdir, stat } from "node:fs/promises"
|
|
2
|
+
import { homedir } from "node:os"
|
|
3
|
+
import path from "node:path"
|
|
4
|
+
|
|
5
|
+
export function resolveLocalPath(localPath: string) {
|
|
6
|
+
const trimmed = localPath.trim()
|
|
7
|
+
if (!trimmed) {
|
|
8
|
+
throw new Error("Project path is required")
|
|
9
|
+
}
|
|
10
|
+
if (trimmed === "~") {
|
|
11
|
+
return homedir()
|
|
12
|
+
}
|
|
13
|
+
if (trimmed.startsWith("~/")) {
|
|
14
|
+
return path.join(homedir(), trimmed.slice(2))
|
|
15
|
+
}
|
|
16
|
+
return path.resolve(trimmed)
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export async function ensureProjectDirectory(localPath: string) {
|
|
20
|
+
const resolvedPath = resolveLocalPath(localPath)
|
|
21
|
+
|
|
22
|
+
await mkdir(resolvedPath, { recursive: true })
|
|
23
|
+
const info = await stat(resolvedPath)
|
|
24
|
+
if (!info.isDirectory()) {
|
|
25
|
+
throw new Error("Project path must be a directory")
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function getProjectUploadDir(localPath: string) {
|
|
30
|
+
return path.join(resolveLocalPath(localPath), ".kanna", "uploads")
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function getProjectExportDir(localPath: string) {
|
|
34
|
+
return path.join(resolveLocalPath(localPath), ".kanna", "exports")
|
|
35
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test"
|
|
2
|
+
import { spawnDetached } from "./process-utils"
|
|
3
|
+
|
|
4
|
+
describe("spawnDetached", () => {
|
|
5
|
+
test("rejects when the command does not exist", async () => {
|
|
6
|
+
await expect(spawnDetached("definitely-not-a-real-command-kanna", [])).rejects.toThrow("Command not found")
|
|
7
|
+
})
|
|
8
|
+
|
|
9
|
+
test("resolves when the process starts successfully", async () => {
|
|
10
|
+
await expect(spawnDetached("sh", ["-c", "exit 0"])).resolves.toBeUndefined()
|
|
11
|
+
})
|
|
12
|
+
})
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { spawn, spawnSync } from "node:child_process"
|
|
2
|
+
|
|
3
|
+
function formatSpawnError(command: string, error: unknown) {
|
|
4
|
+
if (!(error instanceof Error)) {
|
|
5
|
+
return new Error(`Failed to start ${command}`)
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
const code = "code" in error ? (error as NodeJS.ErrnoException).code : undefined
|
|
9
|
+
if (code === "ENOENT") {
|
|
10
|
+
return new Error(`Command not found: ${command}`)
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
return new Error(error.message || `Failed to start ${command}`)
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function spawnDetached(command: string, args: string[]) {
|
|
17
|
+
return new Promise<void>((resolve, reject) => {
|
|
18
|
+
let child
|
|
19
|
+
try {
|
|
20
|
+
child = spawn(command, args, { stdio: "ignore", detached: true })
|
|
21
|
+
} catch (error) {
|
|
22
|
+
reject(formatSpawnError(command, error))
|
|
23
|
+
return
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const handleError = (error: Error) => {
|
|
27
|
+
reject(formatSpawnError(command, error))
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
child.once("error", handleError)
|
|
31
|
+
child.once("spawn", () => {
|
|
32
|
+
child.off("error", handleError)
|
|
33
|
+
child.unref()
|
|
34
|
+
resolve()
|
|
35
|
+
})
|
|
36
|
+
})
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function hasCommand(command: string) {
|
|
40
|
+
const result = spawnSync("sh", ["-lc", `command -v ${command}`], { stdio: "ignore" })
|
|
41
|
+
return result.status === 0
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function canOpenMacApp(appName: string) {
|
|
45
|
+
const result = spawnSync("open", ["-Ra", appName], { stdio: "ignore" })
|
|
46
|
+
return result.status === 0
|
|
47
|
+
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, test } from "bun:test"
|
|
2
|
+
import { mkdtemp, rm, writeFile, mkdir } from "node:fs/promises"
|
|
3
|
+
import { tmpdir } from "node:os"
|
|
4
|
+
import path from "node:path"
|
|
5
|
+
import { $ } from "bun"
|
|
6
|
+
import { clearProjectPathCache, listProjectPaths } from "./project-paths"
|
|
7
|
+
|
|
8
|
+
const tempDirs: string[] = []
|
|
9
|
+
|
|
10
|
+
beforeEach(() => {
|
|
11
|
+
clearProjectPathCache()
|
|
12
|
+
})
|
|
13
|
+
|
|
14
|
+
afterEach(async () => {
|
|
15
|
+
await Promise.all(tempDirs.splice(0).map((dir) => rm(dir, { recursive: true, force: true })))
|
|
16
|
+
})
|
|
17
|
+
|
|
18
|
+
async function makeTempDir(prefix: string): Promise<string> {
|
|
19
|
+
const dir = await mkdtemp(path.join(tmpdir(), prefix))
|
|
20
|
+
tempDirs.push(dir)
|
|
21
|
+
return dir
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
describe("listProjectPaths", () => {
|
|
25
|
+
test("empty query returns top-level entries with dirs suffixed", async () => {
|
|
26
|
+
const root = await makeTempDir("kanna-paths-empty-")
|
|
27
|
+
await writeFile(path.join(root, "a.txt"), "a")
|
|
28
|
+
await mkdir(path.join(root, "src"))
|
|
29
|
+
await writeFile(path.join(root, "src", "b.ts"), "b")
|
|
30
|
+
|
|
31
|
+
const paths = await listProjectPaths({ projectId: "p1", localPath: root, query: "" })
|
|
32
|
+
const names = paths.map((p) => p.path).sort()
|
|
33
|
+
expect(names).toEqual(["a.txt", "src/"])
|
|
34
|
+
expect(paths.find((p) => p.path === "src/")?.kind).toBe("dir")
|
|
35
|
+
expect(paths.find((p) => p.path === "a.txt")?.kind).toBe("file")
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
test("git repo: returns tracked files + derived dirs", async () => {
|
|
39
|
+
const root = await makeTempDir("kanna-paths-git-")
|
|
40
|
+
await $`git init -q`.cwd(root)
|
|
41
|
+
await $`git -c user.email=t@t -c user.name=t commit -q --allow-empty -m init`.cwd(root)
|
|
42
|
+
await mkdir(path.join(root, "src"))
|
|
43
|
+
await writeFile(path.join(root, "src", "agent.ts"), "x")
|
|
44
|
+
await writeFile(path.join(root, "README.md"), "r")
|
|
45
|
+
await $`git add .`.cwd(root)
|
|
46
|
+
await $`git -c user.email=t@t -c user.name=t commit -q -m add`.cwd(root)
|
|
47
|
+
|
|
48
|
+
const paths = await listProjectPaths({ projectId: "p2", localPath: root, query: "agent" })
|
|
49
|
+
const names = paths.map((p) => p.path)
|
|
50
|
+
expect(names).toContain("src/agent.ts")
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
test("git repo: respects .gitignore for untracked files", async () => {
|
|
54
|
+
const root = await makeTempDir("kanna-paths-ignore-")
|
|
55
|
+
await $`git init -q`.cwd(root)
|
|
56
|
+
await writeFile(path.join(root, ".gitignore"), "node_modules\n")
|
|
57
|
+
await mkdir(path.join(root, "node_modules"))
|
|
58
|
+
await writeFile(path.join(root, "node_modules", "junk.js"), "x")
|
|
59
|
+
await writeFile(path.join(root, "app.ts"), "x")
|
|
60
|
+
|
|
61
|
+
const paths = await listProjectPaths({ projectId: "p3", localPath: root, query: "junk" })
|
|
62
|
+
expect(paths.map((p) => p.path)).not.toContain("node_modules/junk.js")
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
test("fuzzy ranking: prefix matches before substring matches", async () => {
|
|
66
|
+
const root = await makeTempDir("kanna-paths-rank-")
|
|
67
|
+
await writeFile(path.join(root, "review.ts"), "")
|
|
68
|
+
await writeFile(path.join(root, "unreview.ts"), "")
|
|
69
|
+
|
|
70
|
+
const paths = await listProjectPaths({ projectId: "p4", localPath: root, query: "rev" })
|
|
71
|
+
expect(paths.map((p) => p.path)).toEqual(["review.ts", "unreview.ts"])
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
test("respects limit", async () => {
|
|
75
|
+
const root = await makeTempDir("kanna-paths-limit-")
|
|
76
|
+
for (let i = 0; i < 10; i++) {
|
|
77
|
+
await writeFile(path.join(root, `file-${i}.txt`), "")
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const paths = await listProjectPaths({ projectId: "p5", localPath: root, query: "file", limit: 3 })
|
|
81
|
+
expect(paths.length).toBe(3)
|
|
82
|
+
})
|
|
83
|
+
|
|
84
|
+
test("cache returns from memory on repeat call", async () => {
|
|
85
|
+
const root = await makeTempDir("kanna-paths-cache-")
|
|
86
|
+
await writeFile(path.join(root, "a.txt"), "")
|
|
87
|
+
|
|
88
|
+
const first = await listProjectPaths({ projectId: "p6", localPath: root, query: "a" })
|
|
89
|
+
await writeFile(path.join(root, "b.txt"), "") // added after first call
|
|
90
|
+
const second = await listProjectPaths({ projectId: "p6", localPath: root, query: "b" })
|
|
91
|
+
|
|
92
|
+
expect(first.map((p) => p.path)).toContain("a.txt")
|
|
93
|
+
expect(second.map((p) => p.path)).not.toContain("b.txt")
|
|
94
|
+
})
|
|
95
|
+
})
|