@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,215 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test"
|
|
2
|
+
import { mkdtemp, rm } from "node:fs/promises"
|
|
3
|
+
import { tmpdir } from "node:os"
|
|
4
|
+
import { join } from "node:path"
|
|
5
|
+
import { AgentCoordinator } from "../agent"
|
|
6
|
+
import { EventStore } from "../event-store"
|
|
7
|
+
import { AsyncEventQueue } from "../test-helpers/async-event-queue"
|
|
8
|
+
import { waitFor } from "../test-helpers/wait-for"
|
|
9
|
+
import type { AutoContinueEvent } from "./events"
|
|
10
|
+
import { ClaudeLimitDetector, CodexLimitDetector } from "./limit-detector"
|
|
11
|
+
import { ScheduleManager, type Clock } from "./schedule-manager"
|
|
12
|
+
|
|
13
|
+
// ---------------------------------------------------------------------------
|
|
14
|
+
// FakeClock — controllable wall-clock for ScheduleManager
|
|
15
|
+
// ---------------------------------------------------------------------------
|
|
16
|
+
|
|
17
|
+
class FakeClock implements Clock {
|
|
18
|
+
private currentTime: number
|
|
19
|
+
private readonly timers = new Map<number, { fn: () => void; fireAt: number }>()
|
|
20
|
+
private nextId = 1
|
|
21
|
+
|
|
22
|
+
constructor(startAt: number) {
|
|
23
|
+
this.currentTime = startAt
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
now(): number {
|
|
27
|
+
return this.currentTime
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
setTimeout(fn: () => void, delayMs: number): number {
|
|
31
|
+
const id = this.nextId++
|
|
32
|
+
this.timers.set(id, { fn, fireAt: this.currentTime + delayMs })
|
|
33
|
+
return id
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
clearTimeout(id: number): void {
|
|
37
|
+
this.timers.delete(id)
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
advance(ms: number): void {
|
|
41
|
+
this.currentTime += ms
|
|
42
|
+
for (const [id, timer] of [...this.timers.entries()]) {
|
|
43
|
+
if (timer.fireAt <= this.currentTime) {
|
|
44
|
+
this.timers.delete(id)
|
|
45
|
+
timer.fn()
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// ---------------------------------------------------------------------------
|
|
52
|
+
// Helpers
|
|
53
|
+
// ---------------------------------------------------------------------------
|
|
54
|
+
|
|
55
|
+
/** Build a rate-limit error that ClaudeLimitDetector recognises.
|
|
56
|
+
*
|
|
57
|
+
* The `anthropic-ratelimit-unified-reset` header is set to
|
|
58
|
+
* `new Date(resetAt).toISOString()` so the detector returns exactly
|
|
59
|
+
* `resetAt` as the reset timestamp.
|
|
60
|
+
*/
|
|
61
|
+
function makeRateLimitError(resetAt: number): Error & { status: number; headers: Record<string, string> } {
|
|
62
|
+
const err = new Error(
|
|
63
|
+
JSON.stringify({ type: "error", error: { type: "rate_limit_error" } })
|
|
64
|
+
) as Error & { status: number; headers: Record<string, string> }
|
|
65
|
+
err.status = 429
|
|
66
|
+
err.headers = {
|
|
67
|
+
"anthropic-ratelimit-unified-reset": new Date(resetAt).toISOString(),
|
|
68
|
+
"x-anthropic-timezone": "Asia/Saigon",
|
|
69
|
+
}
|
|
70
|
+
return err
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// ---------------------------------------------------------------------------
|
|
74
|
+
// End-to-end test
|
|
75
|
+
// ---------------------------------------------------------------------------
|
|
76
|
+
|
|
77
|
+
describe("auto-continue end-to-end", () => {
|
|
78
|
+
test("rate limit → proposed → accept → timer fires → auto_continue_fired + 'continue' user_prompt", async () => {
|
|
79
|
+
const dir = await mkdtemp(join(tmpdir(), "kanna-ac-e2e-"))
|
|
80
|
+
let scheduleManager: ScheduleManager | undefined
|
|
81
|
+
try {
|
|
82
|
+
// --- Set up real EventStore ---
|
|
83
|
+
const store = new EventStore(dir)
|
|
84
|
+
await store.initialize()
|
|
85
|
+
const project = await store.openProject("/tmp/e2e-proj")
|
|
86
|
+
const chat = await store.createChat(project.id)
|
|
87
|
+
const chatId = chat.id
|
|
88
|
+
|
|
89
|
+
// --- FakeClock anchored to real wall-clock so scheduledAt guard passes ---
|
|
90
|
+
// acceptAutoContinue checks `scheduledAt > Date.now()` using real Date.now().
|
|
91
|
+
// ScheduleManager.arm computes `delay = scheduledAt - clock.now()`.
|
|
92
|
+
// By starting the fake clock at Date.now(), both quantities agree and
|
|
93
|
+
// a 10s delta is large enough to survive slow CI runners.
|
|
94
|
+
const clockStart = Date.now()
|
|
95
|
+
const clock = new FakeClock(clockStart)
|
|
96
|
+
const resetAtMs = clockStart + 10_000 // rate-limit resets 10s "from now"
|
|
97
|
+
|
|
98
|
+
// --- ScheduleManager + coordinator (forward-reference pattern) ---
|
|
99
|
+
let coordinator!: AgentCoordinator
|
|
100
|
+
scheduleManager = new ScheduleManager({
|
|
101
|
+
clock,
|
|
102
|
+
fire: async (cid, sid) => {
|
|
103
|
+
await coordinator.fireAutoContinue(cid, sid)
|
|
104
|
+
},
|
|
105
|
+
})
|
|
106
|
+
|
|
107
|
+
// Async event queue so we can throw a rate-limit error on demand.
|
|
108
|
+
const events = new AsyncEventQueue<never>()
|
|
109
|
+
|
|
110
|
+
coordinator = new AgentCoordinator({
|
|
111
|
+
store,
|
|
112
|
+
onStateChange: () => {},
|
|
113
|
+
claudeLimitDetector: new ClaudeLimitDetector(),
|
|
114
|
+
codexLimitDetector: new CodexLimitDetector(),
|
|
115
|
+
scheduleManager,
|
|
116
|
+
// manual mode: do NOT auto-resume so we exercise the proposed → accept path
|
|
117
|
+
getAutoResumePreference: () => false,
|
|
118
|
+
startClaudeSession: async () => ({
|
|
119
|
+
provider: "claude" as const,
|
|
120
|
+
stream: events,
|
|
121
|
+
getAccountInfo: async () => null,
|
|
122
|
+
interrupt: async () => {},
|
|
123
|
+
close: () => {},
|
|
124
|
+
setModel: async () => {},
|
|
125
|
+
setPermissionMode: async () => {},
|
|
126
|
+
getSupportedCommands: async () => [],
|
|
127
|
+
sendPrompt: async () => {
|
|
128
|
+
// Throw a rate-limit error; this is caught by runClaudeSession
|
|
129
|
+
// which routes it through handleLimitError → ClaudeLimitDetector.
|
|
130
|
+
events.throw(makeRateLimitError(resetAtMs))
|
|
131
|
+
},
|
|
132
|
+
}),
|
|
133
|
+
})
|
|
134
|
+
|
|
135
|
+
// ----------------------------------------------------------------
|
|
136
|
+
// Step 1: Send a message; session throws a rate-limit error.
|
|
137
|
+
// ----------------------------------------------------------------
|
|
138
|
+
await coordinator.send({
|
|
139
|
+
type: "chat.send",
|
|
140
|
+
chatId,
|
|
141
|
+
content: "hello",
|
|
142
|
+
model: "claude-opus-4-5",
|
|
143
|
+
provider: "claude",
|
|
144
|
+
autoResumeOnRateLimit: false,
|
|
145
|
+
})
|
|
146
|
+
|
|
147
|
+
// Wait for the proposed event to be persisted.
|
|
148
|
+
await waitFor(() => store.getAutoContinueEvents(chatId).length >= 1)
|
|
149
|
+
|
|
150
|
+
const acEventsAfterPropose = store.getAutoContinueEvents(chatId)
|
|
151
|
+
expect(acEventsAfterPropose).toHaveLength(1)
|
|
152
|
+
expect(acEventsAfterPropose[0].kind).toBe("auto_continue_proposed")
|
|
153
|
+
const proposed = acEventsAfterPropose[0] as Extract<AutoContinueEvent, { kind: "auto_continue_proposed" }>
|
|
154
|
+
expect(proposed.tz).toBe("Asia/Saigon")
|
|
155
|
+
const { scheduleId } = proposed
|
|
156
|
+
|
|
157
|
+
// ----------------------------------------------------------------
|
|
158
|
+
// Step 2: Client accepts — scheduleManager arms the timer.
|
|
159
|
+
// ----------------------------------------------------------------
|
|
160
|
+
const scheduledAt = clock.now() + 10_000 // in the future per both real and fake clock
|
|
161
|
+
await coordinator.acceptAutoContinue(chatId, scheduleId, scheduledAt)
|
|
162
|
+
|
|
163
|
+
const acEventsAfterAccept = store.getAutoContinueEvents(chatId)
|
|
164
|
+
expect(acEventsAfterAccept).toHaveLength(2)
|
|
165
|
+
const accepted = acEventsAfterAccept[1] as Extract<AutoContinueEvent, { kind: "auto_continue_accepted" }>
|
|
166
|
+
expect(accepted.kind).toBe("auto_continue_accepted")
|
|
167
|
+
expect(accepted.scheduleId).toBe(scheduleId)
|
|
168
|
+
expect(accepted.source).toBe("user")
|
|
169
|
+
expect(accepted.scheduledAt).toBe(scheduledAt)
|
|
170
|
+
|
|
171
|
+
// ----------------------------------------------------------------
|
|
172
|
+
// Step 3: Advance the fake clock past scheduledAt — timer fires.
|
|
173
|
+
// The ScheduleManager callback calls coordinator.fireAutoContinue
|
|
174
|
+
// which is async; we need to drain the microtask queue after advance.
|
|
175
|
+
// ----------------------------------------------------------------
|
|
176
|
+
clock.advance(10_100)
|
|
177
|
+
|
|
178
|
+
// ----------------------------------------------------------------
|
|
179
|
+
// Step 4: Assert auto_continue_fired event.
|
|
180
|
+
// ----------------------------------------------------------------
|
|
181
|
+
await waitFor(() =>
|
|
182
|
+
store.getAutoContinueEvents(chatId).some((e) => e.kind === "auto_continue_fired")
|
|
183
|
+
)
|
|
184
|
+
|
|
185
|
+
const acEventsAfterFire = store.getAutoContinueEvents(chatId)
|
|
186
|
+
const firedEvent = acEventsAfterFire.find(
|
|
187
|
+
(e) => e.kind === "auto_continue_fired"
|
|
188
|
+
) as Extract<AutoContinueEvent, { kind: "auto_continue_fired" }> | undefined
|
|
189
|
+
expect(firedEvent).toBeDefined()
|
|
190
|
+
expect(firedEvent!.scheduleId).toBe(scheduleId)
|
|
191
|
+
|
|
192
|
+
// ----------------------------------------------------------------
|
|
193
|
+
// Step 5: Assert "continue" user_prompt with autoContinue metadata.
|
|
194
|
+
// ----------------------------------------------------------------
|
|
195
|
+
await waitFor(() =>
|
|
196
|
+
store.getMessages(chatId).some((m) => m.kind === "user_prompt" && m.content === "continue")
|
|
197
|
+
)
|
|
198
|
+
|
|
199
|
+
const messages = store.getMessages(chatId)
|
|
200
|
+
const continuePrompts = messages.filter(
|
|
201
|
+
(m) => m.kind === "user_prompt" && m.content === "continue"
|
|
202
|
+
)
|
|
203
|
+
expect(continuePrompts).toHaveLength(1)
|
|
204
|
+
const continuePrompt = continuePrompts[0]
|
|
205
|
+
if (continuePrompt?.kind === "user_prompt") {
|
|
206
|
+
expect(continuePrompt.autoContinue?.scheduleId).toBe(scheduleId)
|
|
207
|
+
} else {
|
|
208
|
+
throw new Error("Expected user_prompt entry")
|
|
209
|
+
}
|
|
210
|
+
} finally {
|
|
211
|
+
scheduleManager?.shutdown()
|
|
212
|
+
await rm(dir, { recursive: true, force: true })
|
|
213
|
+
}
|
|
214
|
+
})
|
|
215
|
+
})
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test"
|
|
2
|
+
import type { AutoContinueEvent } from "./events"
|
|
3
|
+
|
|
4
|
+
describe("AutoContinueEvent", () => {
|
|
5
|
+
test("covers the five lifecycle kinds", () => {
|
|
6
|
+
const kinds: AutoContinueEvent["kind"][] = [
|
|
7
|
+
"auto_continue_proposed",
|
|
8
|
+
"auto_continue_accepted",
|
|
9
|
+
"auto_continue_rescheduled",
|
|
10
|
+
"auto_continue_cancelled",
|
|
11
|
+
"auto_continue_fired",
|
|
12
|
+
]
|
|
13
|
+
expect(kinds.length).toBe(5)
|
|
14
|
+
})
|
|
15
|
+
|
|
16
|
+
test("proposed event carries reset + tz metadata", () => {
|
|
17
|
+
const event: AutoContinueEvent = {
|
|
18
|
+
v: 3,
|
|
19
|
+
kind: "auto_continue_proposed",
|
|
20
|
+
timestamp: 1_000,
|
|
21
|
+
chatId: "c1",
|
|
22
|
+
scheduleId: "s1",
|
|
23
|
+
detectedAt: 1_000,
|
|
24
|
+
resetAt: 2_000,
|
|
25
|
+
tz: "Asia/Saigon",
|
|
26
|
+
|
|
27
|
+
}
|
|
28
|
+
expect(event.tz).toBe("Asia/Saigon")
|
|
29
|
+
})
|
|
30
|
+
})
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
export const AUTO_CONTINUE_EVENT_VERSION = 3 as const
|
|
2
|
+
|
|
3
|
+
interface AutoContinueEventBase {
|
|
4
|
+
v: typeof AUTO_CONTINUE_EVENT_VERSION
|
|
5
|
+
timestamp: number
|
|
6
|
+
chatId: string
|
|
7
|
+
scheduleId: string
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export type AutoContinueEvent =
|
|
11
|
+
| (AutoContinueEventBase & {
|
|
12
|
+
kind: "auto_continue_proposed"
|
|
13
|
+
detectedAt: number
|
|
14
|
+
resetAt: number
|
|
15
|
+
tz: string
|
|
16
|
+
})
|
|
17
|
+
| (AutoContinueEventBase & {
|
|
18
|
+
kind: "auto_continue_accepted"
|
|
19
|
+
scheduledAt: number
|
|
20
|
+
tz: string
|
|
21
|
+
source: "user" | "auto_setting"
|
|
22
|
+
resetAt: number
|
|
23
|
+
detectedAt: number
|
|
24
|
+
})
|
|
25
|
+
| (AutoContinueEventBase & {
|
|
26
|
+
kind: "auto_continue_rescheduled"
|
|
27
|
+
scheduledAt: number
|
|
28
|
+
})
|
|
29
|
+
| (AutoContinueEventBase & {
|
|
30
|
+
kind: "auto_continue_cancelled"
|
|
31
|
+
reason: "user" | "chat_deleted"
|
|
32
|
+
})
|
|
33
|
+
| (AutoContinueEventBase & {
|
|
34
|
+
kind: "auto_continue_fired"
|
|
35
|
+
})
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test"
|
|
2
|
+
import { ClaudeLimitDetector, CodexLimitDetector, parseResetFromText } from "./limit-detector"
|
|
3
|
+
|
|
4
|
+
const detector = new ClaudeLimitDetector()
|
|
5
|
+
|
|
6
|
+
function anthropicError(body: Record<string, unknown>, headers: Record<string, string> = {}) {
|
|
7
|
+
const error = new Error(JSON.stringify(body)) as Error & { status?: number; headers?: Record<string, string> }
|
|
8
|
+
error.status = 429
|
|
9
|
+
error.headers = headers
|
|
10
|
+
return error
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
describe("ClaudeLimitDetector", () => {
|
|
14
|
+
test("returns null for non-rate-limit errors", () => {
|
|
15
|
+
const err = new Error("Something unrelated went wrong")
|
|
16
|
+
expect(detector.detect("c1", err)).toBeNull()
|
|
17
|
+
})
|
|
18
|
+
|
|
19
|
+
test("detects rate limit with ISO reset timestamp in headers", () => {
|
|
20
|
+
const resetIso = "2026-04-23T00:00:00+07:00"
|
|
21
|
+
const err = anthropicError(
|
|
22
|
+
{ type: "error", error: { type: "rate_limit_error", message: "You've hit your limit · resets 12am (Asia/Saigon)" } },
|
|
23
|
+
{ "anthropic-ratelimit-unified-reset": resetIso, "x-anthropic-timezone": "Asia/Saigon" }
|
|
24
|
+
)
|
|
25
|
+
const detection = detector.detect("c1", err)
|
|
26
|
+
expect(detection).not.toBeNull()
|
|
27
|
+
expect(detection!.chatId).toBe("c1")
|
|
28
|
+
expect(detection!.resetAt).toBe(new Date(resetIso).getTime())
|
|
29
|
+
expect(detection!.tz).toBe("Asia/Saigon")
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
test("falls back to tz=system when no timezone header is present", () => {
|
|
33
|
+
const resetIso = "2026-04-23T05:00:00Z"
|
|
34
|
+
const err = anthropicError(
|
|
35
|
+
{ type: "error", error: { type: "rate_limit_error" } },
|
|
36
|
+
{ "anthropic-ratelimit-unified-reset": resetIso }
|
|
37
|
+
)
|
|
38
|
+
const detection = detector.detect("c1", err)
|
|
39
|
+
expect(detection!.tz).toBe("system")
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
test("returns null when the payload is rate-limit but no reset timestamp can be parsed", () => {
|
|
43
|
+
const err = anthropicError({ type: "error", error: { type: "rate_limit_error" } })
|
|
44
|
+
expect(detector.detect("c1", err)).toBeNull()
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
test("parses resetAt from the message body when headers are absent", () => {
|
|
48
|
+
const resetIso = "2026-04-23T00:00:00+07:00"
|
|
49
|
+
const err = new Error(JSON.stringify({
|
|
50
|
+
type: "error",
|
|
51
|
+
error: {
|
|
52
|
+
type: "rate_limit_error",
|
|
53
|
+
resets_at: resetIso,
|
|
54
|
+
timezone: "Asia/Saigon",
|
|
55
|
+
},
|
|
56
|
+
}))
|
|
57
|
+
const detection = detector.detect("c1", err)
|
|
58
|
+
expect(detection!.resetAt).toBe(new Date(resetIso).getTime())
|
|
59
|
+
expect(detection!.tz).toBe("Asia/Saigon")
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
test("does not match on status-only errors (400, 500, etc.)", () => {
|
|
63
|
+
const err = anthropicError({ type: "error", error: { type: "overloaded_error" } })
|
|
64
|
+
expect(detector.detect("c1", err)).toBeNull()
|
|
65
|
+
})
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
const codex = new CodexLimitDetector()
|
|
69
|
+
|
|
70
|
+
describe("CodexLimitDetector", () => {
|
|
71
|
+
test("returns null for non-rate-limit JSON-RPC errors", () => {
|
|
72
|
+
const err = { code: -32601, message: "Method not found" }
|
|
73
|
+
expect(codex.detect("c1", err)).toBeNull()
|
|
74
|
+
})
|
|
75
|
+
|
|
76
|
+
test("detects rate limit from error.data.code with epoch-ms reset", () => {
|
|
77
|
+
const err = {
|
|
78
|
+
code: -32001,
|
|
79
|
+
message: "Rate limited",
|
|
80
|
+
data: { code: "rate_limit", resets_at_ms: 2_000_000, timezone: "Asia/Saigon" },
|
|
81
|
+
}
|
|
82
|
+
const detection = codex.detect("c1", err)
|
|
83
|
+
expect(detection!.resetAt).toBe(2_000_000)
|
|
84
|
+
expect(detection!.tz).toBe("Asia/Saigon")
|
|
85
|
+
})
|
|
86
|
+
|
|
87
|
+
test("detects rate limit with ISO resets_at", () => {
|
|
88
|
+
const resetIso = "2026-04-23T00:00:00+07:00"
|
|
89
|
+
const err = {
|
|
90
|
+
code: -32001,
|
|
91
|
+
message: "Rate limited",
|
|
92
|
+
data: { code: "rate_limit", resets_at: resetIso },
|
|
93
|
+
}
|
|
94
|
+
const detection = codex.detect("c1", err)
|
|
95
|
+
expect(detection!.resetAt).toBe(new Date(resetIso).getTime())
|
|
96
|
+
expect(detection!.tz).toBe("system")
|
|
97
|
+
})
|
|
98
|
+
|
|
99
|
+
test("returns null when no reset timestamp can be parsed", () => {
|
|
100
|
+
const err = { code: -32001, data: { code: "rate_limit" } }
|
|
101
|
+
expect(codex.detect("c1", err)).toBeNull()
|
|
102
|
+
})
|
|
103
|
+
})
|
|
104
|
+
|
|
105
|
+
describe("parseResetFromText", () => {
|
|
106
|
+
test("parses 'resets 2pm (Asia/Saigon)' for later-same-day", () => {
|
|
107
|
+
const now = Date.parse("2026-04-23T05:00:00Z") // 12:00 Saigon
|
|
108
|
+
const parsed = parseResetFromText("You've hit your limit · resets 2pm (Asia/Saigon)", now)
|
|
109
|
+
expect(parsed).not.toBeNull()
|
|
110
|
+
expect(parsed!.tz).toBe("Asia/Saigon")
|
|
111
|
+
expect(new Date(parsed!.resetAt).toISOString()).toBe("2026-04-23T07:00:00.000Z")
|
|
112
|
+
})
|
|
113
|
+
|
|
114
|
+
test("parses 'resets 2pm (Asia/Saigon)' wraps to next day if past", () => {
|
|
115
|
+
const now = Date.parse("2026-04-23T08:00:00Z") // 15:00 Saigon
|
|
116
|
+
const parsed = parseResetFromText("You've hit your limit · resets 2pm (Asia/Saigon)", now)
|
|
117
|
+
expect(new Date(parsed!.resetAt).toISOString()).toBe("2026-04-24T07:00:00.000Z")
|
|
118
|
+
})
|
|
119
|
+
|
|
120
|
+
test("parses '12am' as midnight", () => {
|
|
121
|
+
const now = Date.parse("2026-04-23T10:00:00Z")
|
|
122
|
+
const parsed = parseResetFromText("resets 12am (UTC)", now)
|
|
123
|
+
expect(new Date(parsed!.resetAt).toISOString()).toBe("2026-04-24T00:00:00.000Z")
|
|
124
|
+
})
|
|
125
|
+
|
|
126
|
+
test("returns null when no 'resets' token", () => {
|
|
127
|
+
expect(parseResetFromText("nothing interesting", Date.now())).toBeNull()
|
|
128
|
+
})
|
|
129
|
+
|
|
130
|
+
test("parses 'resets 2:40pm (Asia/Saigon)' with minutes", () => {
|
|
131
|
+
const now = Date.parse("2026-04-23T05:00:00Z") // 12:00 Saigon
|
|
132
|
+
const parsed = parseResetFromText("You've hit your limit · resets 2:40pm (Asia/Saigon)", now)
|
|
133
|
+
expect(parsed).not.toBeNull()
|
|
134
|
+
expect(parsed!.tz).toBe("Asia/Saigon")
|
|
135
|
+
expect(new Date(parsed!.resetAt).toISOString()).toBe("2026-04-23T07:40:00.000Z")
|
|
136
|
+
})
|
|
137
|
+
|
|
138
|
+
test("parses 'resets 12:30am (UTC)' with minutes wraps next day", () => {
|
|
139
|
+
const now = Date.parse("2026-04-23T10:00:00Z")
|
|
140
|
+
const parsed = parseResetFromText("resets 12:30am (UTC)", now)
|
|
141
|
+
expect(new Date(parsed!.resetAt).toISOString()).toBe("2026-04-24T00:30:00.000Z")
|
|
142
|
+
})
|
|
143
|
+
})
|
|
144
|
+
|
|
145
|
+
describe("ClaudeLimitDetector.detectFromResultText", () => {
|
|
146
|
+
test("detects from stream result text", () => {
|
|
147
|
+
const now = Date.parse("2026-04-23T05:00:00Z")
|
|
148
|
+
const detection = detector.detectFromResultText("c1", "You've hit your limit · resets 2pm (Asia/Saigon)", now)
|
|
149
|
+
expect(detection).not.toBeNull()
|
|
150
|
+
expect(detection!.tz).toBe("Asia/Saigon")
|
|
151
|
+
expect(new Date(detection!.resetAt).toISOString()).toBe("2026-04-23T07:00:00.000Z")
|
|
152
|
+
})
|
|
153
|
+
})
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
export interface LimitDetection {
|
|
2
|
+
chatId: string
|
|
3
|
+
resetAt: number
|
|
4
|
+
tz: string
|
|
5
|
+
raw: unknown
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export interface LimitDetector {
|
|
9
|
+
detect(chatId: string, error: unknown): LimitDetection | null
|
|
10
|
+
detectFromResultText?(chatId: string, text: string, nowMs?: number): LimitDetection | null
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
interface ErrorLike {
|
|
14
|
+
message?: string
|
|
15
|
+
status?: number
|
|
16
|
+
headers?: Record<string, string>
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function extractHeaders(error: unknown): Record<string, string> {
|
|
20
|
+
if (error && typeof error === "object" && "headers" in error) {
|
|
21
|
+
const headers = (error as ErrorLike).headers
|
|
22
|
+
if (headers && typeof headers === "object") return headers
|
|
23
|
+
}
|
|
24
|
+
return {}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function parseBody(error: unknown): Record<string, unknown> | null {
|
|
28
|
+
if (!error || typeof error !== "object") return null
|
|
29
|
+
const message = (error as ErrorLike).message
|
|
30
|
+
if (!message) return null
|
|
31
|
+
try {
|
|
32
|
+
const parsed = JSON.parse(message)
|
|
33
|
+
return parsed && typeof parsed === "object" ? (parsed as Record<string, unknown>) : null
|
|
34
|
+
} catch {
|
|
35
|
+
return null
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function parseIsoMillis(value: unknown): number | null {
|
|
40
|
+
if (typeof value !== "string" || !value) return null
|
|
41
|
+
const millis = new Date(value).getTime()
|
|
42
|
+
return Number.isFinite(millis) ? millis : null
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function zonedWallClockToUtcMs(
|
|
46
|
+
year: number, month: number, day: number, hour: number, minute: number, tz: string,
|
|
47
|
+
): number {
|
|
48
|
+
const utcGuess = Date.UTC(year, month - 1, day, hour, minute)
|
|
49
|
+
const dtf = new Intl.DateTimeFormat("en-US", {
|
|
50
|
+
timeZone: tz, year: "numeric", month: "2-digit", day: "2-digit",
|
|
51
|
+
hour: "2-digit", minute: "2-digit", second: "2-digit", hour12: false,
|
|
52
|
+
})
|
|
53
|
+
const parts = Object.fromEntries(
|
|
54
|
+
dtf.formatToParts(new Date(utcGuess))
|
|
55
|
+
.filter((part) => part.type !== "literal")
|
|
56
|
+
.map((part) => [part.type, part.value]),
|
|
57
|
+
)
|
|
58
|
+
const asLocal = Date.UTC(
|
|
59
|
+
Number(parts.year), Number(parts.month) - 1, Number(parts.day),
|
|
60
|
+
parts.hour === "24" ? 0 : Number(parts.hour), Number(parts.minute),
|
|
61
|
+
)
|
|
62
|
+
return utcGuess - (asLocal - utcGuess)
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function parseResetFromText(text: string, nowMs: number = Date.now()): { resetAt: number; tz: string } | null {
|
|
66
|
+
if (typeof text !== "string") return null
|
|
67
|
+
const match = text.match(/resets\s+(\d{1,2})(?::(\d{2}))?(am|pm)\s*\(([^)]+)\)/i)
|
|
68
|
+
if (!match) return null
|
|
69
|
+
const hour12 = Number(match[1])
|
|
70
|
+
const minute = match[2] ? Number(match[2]) : 0
|
|
71
|
+
const meridiem = match[3].toLowerCase()
|
|
72
|
+
const tz = match[4].trim()
|
|
73
|
+
if (!Number.isFinite(hour12) || hour12 < 1 || hour12 > 12) return null
|
|
74
|
+
if (!Number.isFinite(minute) || minute < 0 || minute > 59) return null
|
|
75
|
+
const hour24 = meridiem === "pm"
|
|
76
|
+
? (hour12 === 12 ? 12 : hour12 + 12)
|
|
77
|
+
: (hour12 === 12 ? 0 : hour12)
|
|
78
|
+
let tzYear: number, tzMonth: number, tzDay: number
|
|
79
|
+
try {
|
|
80
|
+
const dtf = new Intl.DateTimeFormat("en-US", {
|
|
81
|
+
timeZone: tz, year: "numeric", month: "2-digit", day: "2-digit",
|
|
82
|
+
})
|
|
83
|
+
const parts = Object.fromEntries(
|
|
84
|
+
dtf.formatToParts(new Date(nowMs))
|
|
85
|
+
.filter((part) => part.type !== "literal")
|
|
86
|
+
.map((part) => [part.type, part.value]),
|
|
87
|
+
)
|
|
88
|
+
tzYear = Number(parts.year)
|
|
89
|
+
tzMonth = Number(parts.month)
|
|
90
|
+
tzDay = Number(parts.day)
|
|
91
|
+
} catch {
|
|
92
|
+
return null
|
|
93
|
+
}
|
|
94
|
+
let resetAt = zonedWallClockToUtcMs(tzYear, tzMonth, tzDay, hour24, minute, tz)
|
|
95
|
+
if (resetAt <= nowMs) {
|
|
96
|
+
const next = new Date(Date.UTC(tzYear, tzMonth - 1, tzDay) + 24 * 3600_000)
|
|
97
|
+
resetAt = zonedWallClockToUtcMs(
|
|
98
|
+
next.getUTCFullYear(), next.getUTCMonth() + 1, next.getUTCDate(), hour24, minute, tz,
|
|
99
|
+
)
|
|
100
|
+
}
|
|
101
|
+
return { resetAt, tz }
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export class ClaudeLimitDetector implements LimitDetector {
|
|
105
|
+
detect(chatId: string, error: unknown): LimitDetection | null {
|
|
106
|
+
const body = parseBody(error)
|
|
107
|
+
const inner = body && typeof body.error === "object" && body.error !== null
|
|
108
|
+
? (body.error as Record<string, unknown>)
|
|
109
|
+
: null
|
|
110
|
+
const isRateLimit = inner?.type === "rate_limit_error"
|
|
111
|
+
|| (error as ErrorLike | null)?.status === 429 && inner?.type === "rate_limit_error"
|
|
112
|
+
if (!isRateLimit) return null
|
|
113
|
+
|
|
114
|
+
const headers = extractHeaders(error)
|
|
115
|
+
const resetAt = parseIsoMillis(headers["anthropic-ratelimit-unified-reset"])
|
|
116
|
+
?? parseIsoMillis(inner?.resets_at)
|
|
117
|
+
?? parseIsoMillis(inner?.reset_at)
|
|
118
|
+
if (resetAt === null) return null
|
|
119
|
+
|
|
120
|
+
const tz = headers["x-anthropic-timezone"]
|
|
121
|
+
?? (typeof inner?.timezone === "string" ? (inner.timezone as string) : null)
|
|
122
|
+
?? "system"
|
|
123
|
+
|
|
124
|
+
return { chatId, resetAt, tz, raw: error }
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
detectFromResultText(chatId: string, text: string, nowMs: number = Date.now()): LimitDetection | null {
|
|
128
|
+
const parsed = parseResetFromText(text, nowMs)
|
|
129
|
+
if (!parsed) return null
|
|
130
|
+
return { chatId, resetAt: parsed.resetAt, tz: parsed.tz, raw: text }
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
interface JsonRpcErrorLike {
|
|
135
|
+
code?: number
|
|
136
|
+
message?: string
|
|
137
|
+
data?: Record<string, unknown>
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
export class CodexLimitDetector implements LimitDetector {
|
|
141
|
+
detect(chatId: string, error: unknown): LimitDetection | null {
|
|
142
|
+
if (!error || typeof error !== "object") return null
|
|
143
|
+
const rpc = error as JsonRpcErrorLike
|
|
144
|
+
const data = rpc.data && typeof rpc.data === "object" ? rpc.data : null
|
|
145
|
+
const isRateLimit = data?.code === "rate_limit" || rpc.code === -32001
|
|
146
|
+
if (!isRateLimit) return null
|
|
147
|
+
|
|
148
|
+
let resetAt: number | null = null
|
|
149
|
+
if (typeof data?.resets_at_ms === "number" && Number.isFinite(data.resets_at_ms)) {
|
|
150
|
+
resetAt = data.resets_at_ms
|
|
151
|
+
} else {
|
|
152
|
+
resetAt = parseIsoMillis(data?.resets_at)
|
|
153
|
+
}
|
|
154
|
+
if (resetAt === null) return null
|
|
155
|
+
|
|
156
|
+
const tz = typeof data?.timezone === "string" ? (data.timezone as string) : "system"
|
|
157
|
+
return { chatId, resetAt, tz, raw: error }
|
|
158
|
+
}
|
|
159
|
+
}
|