@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.
Files changed (473) hide show
  1. package/LICENSE +22 -0
  2. package/README.md +355 -0
  3. package/bin/kanna +9 -0
  4. package/dist/client/apple-touch-icon.png +0 -0
  5. package/dist/client/assets/abap-BdImnpbu.js +1 -0
  6. package/dist/client/assets/actionscript-3-CoDkCxhg.js +1 -0
  7. package/dist/client/assets/ada-bCR0ucgS.js +1 -0
  8. package/dist/client/assets/andromeeda-C4gqWexZ.js +1 -0
  9. package/dist/client/assets/angular-html-CU67Zn6k.js +1 -0
  10. package/dist/client/assets/angular-ts-BwZT4LLn.js +1 -0
  11. package/dist/client/assets/apache-Pmp26Uib.js +1 -0
  12. package/dist/client/assets/apex-D8_7TLub.js +1 -0
  13. package/dist/client/assets/apl-dKokRX4l.js +1 -0
  14. package/dist/client/assets/applescript-Co6uUVPk.js +1 -0
  15. package/dist/client/assets/ara-BRHolxvo.js +1 -0
  16. package/dist/client/assets/asciidoc-Ve4PFQV2.js +1 -0
  17. package/dist/client/assets/asm-D_Q5rh1f.js +1 -0
  18. package/dist/client/assets/astro-CbQHKStN.js +1 -0
  19. package/dist/client/assets/aurora-x-D-2ljcwZ.js +1 -0
  20. package/dist/client/assets/awk-DMzUqQB5.js +1 -0
  21. package/dist/client/assets/ayu-dark-DYE7WIF3.js +1 -0
  22. package/dist/client/assets/ayu-light-BA47KaF1.js +1 -0
  23. package/dist/client/assets/ayu-mirage-32ctXXKs.js +1 -0
  24. package/dist/client/assets/ballerina-BFfxhgS-.js +1 -0
  25. package/dist/client/assets/bat-BkioyH1T.js +1 -0
  26. package/dist/client/assets/beancount-k_qm7-4y.js +1 -0
  27. package/dist/client/assets/berry-uYugtg8r.js +1 -0
  28. package/dist/client/assets/bibtex-CHM0blh-.js +1 -0
  29. package/dist/client/assets/bicep-Bmn6On1c.js +1 -0
  30. package/dist/client/assets/bird2-DPOp833l.js +1 -0
  31. package/dist/client/assets/blade-D4QpJJKB.js +1 -0
  32. package/dist/client/assets/bricolage-grotesque-latin-ext-wght-normal-CcLUaPy7.woff2 +0 -0
  33. package/dist/client/assets/bricolage-grotesque-latin-wght-normal-DLoelf7F.woff2 +0 -0
  34. package/dist/client/assets/bricolage-grotesque-vietnamese-wght-normal-BUzh504Q.woff2 +0 -0
  35. package/dist/client/assets/bsl-BO_Y6i37.js +1 -0
  36. package/dist/client/assets/c-BIGW1oBm.js +1 -0
  37. package/dist/client/assets/c3-eo99z4R2.js +1 -0
  38. package/dist/client/assets/cadence-Bv_4Rxtq.js +1 -0
  39. package/dist/client/assets/cairo-KRGpt6FW.js +1 -0
  40. package/dist/client/assets/catppuccin-frappe-DFWUc33u.js +1 -0
  41. package/dist/client/assets/catppuccin-latte-C9dUb6Cb.js +1 -0
  42. package/dist/client/assets/catppuccin-macchiato-DQyhUUbL.js +1 -0
  43. package/dist/client/assets/catppuccin-mocha-D87Tk5Gz.js +1 -0
  44. package/dist/client/assets/clarity-D53aC0YG.js +1 -0
  45. package/dist/client/assets/clojure-P80f7IUj.js +1 -0
  46. package/dist/client/assets/cmake-D1j8_8rp.js +1 -0
  47. package/dist/client/assets/cobol-nwyudZeR.js +1 -0
  48. package/dist/client/assets/codeowners-Bp6g37R7.js +1 -0
  49. package/dist/client/assets/codeql-DsOJ9woJ.js +1 -0
  50. package/dist/client/assets/coffee-Ch7k5sss.js +1 -0
  51. package/dist/client/assets/common-lisp-Cg-RD9OK.js +1 -0
  52. package/dist/client/assets/coq-DkFqJrB1.js +1 -0
  53. package/dist/client/assets/cpp-CofmeUqb.js +1 -0
  54. package/dist/client/assets/crystal-tKQVLTB8.js +1 -0
  55. package/dist/client/assets/csharp-COcwbKMJ.js +1 -0
  56. package/dist/client/assets/css-DPfMkruS.js +1 -0
  57. package/dist/client/assets/csv-fuZLfV_i.js +1 -0
  58. package/dist/client/assets/cue-D82EKSYY.js +1 -0
  59. package/dist/client/assets/cypher-COkxafJQ.js +1 -0
  60. package/dist/client/assets/d-85-TOEBH.js +1 -0
  61. package/dist/client/assets/dark-plus-C3mMm8J8.js +1 -0
  62. package/dist/client/assets/dart-CF10PKvl.js +1 -0
  63. package/dist/client/assets/dax-CEL-wOlO.js +1 -0
  64. package/dist/client/assets/desktop-BmXAJ9_W.js +1 -0
  65. package/dist/client/assets/diff-D97Zzqfu.js +1 -0
  66. package/dist/client/assets/docker-BcOcwvcX.js +1 -0
  67. package/dist/client/assets/dotenv-Da5cRb03.js +1 -0
  68. package/dist/client/assets/dracula-BzJJZx-M.js +1 -0
  69. package/dist/client/assets/dracula-soft-BXkSAIEj.js +1 -0
  70. package/dist/client/assets/dream-maker-BtqSS_iP.js +1 -0
  71. package/dist/client/assets/edge-BkV0erSs.js +1 -0
  72. package/dist/client/assets/elixir-CDX3lj18.js +1 -0
  73. package/dist/client/assets/elm-DbKCFpqz.js +1 -0
  74. package/dist/client/assets/emacs-lisp-C9XAeP06.js +1 -0
  75. package/dist/client/assets/erb-B12qg9BL.js +1 -0
  76. package/dist/client/assets/erlang-DsQrWhSR.js +1 -0
  77. package/dist/client/assets/everforest-dark-BgDCqdQA.js +1 -0
  78. package/dist/client/assets/everforest-light-C8M2exoo.js +1 -0
  79. package/dist/client/assets/fennel-BYunw83y.js +1 -0
  80. package/dist/client/assets/fish-BvzEVeQv.js +1 -0
  81. package/dist/client/assets/fluent-C4IJs8-o.js +1 -0
  82. package/dist/client/assets/fortran-fixed-form-CkoXwp7k.js +1 -0
  83. package/dist/client/assets/fortran-free-form-BxgE0vQu.js +1 -0
  84. package/dist/client/assets/fsharp-CXgrBDvD.js +1 -0
  85. package/dist/client/assets/gdresource-BOOCDP_w.js +1 -0
  86. package/dist/client/assets/gdscript-C5YyOfLZ.js +1 -0
  87. package/dist/client/assets/gdshader-DkwncUOv.js +1 -0
  88. package/dist/client/assets/genie-D0YGMca9.js +1 -0
  89. package/dist/client/assets/gherkin-DyxjwDmM.js +1 -0
  90. package/dist/client/assets/git-commit-F4YmCXRG.js +1 -0
  91. package/dist/client/assets/git-rebase-r7XF79zn.js +1 -0
  92. package/dist/client/assets/github-dark-DHJKELXO.js +1 -0
  93. package/dist/client/assets/github-dark-default-Cuk6v7N8.js +1 -0
  94. package/dist/client/assets/github-dark-dimmed-DH5Ifo-i.js +1 -0
  95. package/dist/client/assets/github-dark-high-contrast-E3gJ1_iC.js +1 -0
  96. package/dist/client/assets/github-light-DAi9KRSo.js +1 -0
  97. package/dist/client/assets/github-light-default-D7oLnXFd.js +1 -0
  98. package/dist/client/assets/github-light-high-contrast-BfjtVDDH.js +1 -0
  99. package/dist/client/assets/gleam-BspZqrRM.js +1 -0
  100. package/dist/client/assets/glimmer-js-Rg0-pVw9.js +1 -0
  101. package/dist/client/assets/glimmer-ts-U6CK756n.js +1 -0
  102. package/dist/client/assets/glsl-DplSGwfg.js +1 -0
  103. package/dist/client/assets/gn-n2N0HUVH.js +1 -0
  104. package/dist/client/assets/gnuplot-DdkO51Og.js +1 -0
  105. package/dist/client/assets/go-CxLEBnE3.js +1 -0
  106. package/dist/client/assets/graphql-ChdNCCLP.js +1 -0
  107. package/dist/client/assets/groovy-gcz8RCvz.js +1 -0
  108. package/dist/client/assets/gruvbox-dark-hard-CFHQjOhq.js +1 -0
  109. package/dist/client/assets/gruvbox-dark-medium-GsRaNv29.js +1 -0
  110. package/dist/client/assets/gruvbox-dark-soft-CVdnzihN.js +1 -0
  111. package/dist/client/assets/gruvbox-light-hard-CH1njM8p.js +1 -0
  112. package/dist/client/assets/gruvbox-light-medium-DRw_LuNl.js +1 -0
  113. package/dist/client/assets/gruvbox-light-soft-hJgmCMqR.js +1 -0
  114. package/dist/client/assets/hack-CaT9iCJl.js +1 -0
  115. package/dist/client/assets/haml-B8DHNrY2.js +1 -0
  116. package/dist/client/assets/handlebars-BL8al0AC.js +1 -0
  117. package/dist/client/assets/haskell-Df6bDoY_.js +1 -0
  118. package/dist/client/assets/haxe-CzTSHFRz.js +1 -0
  119. package/dist/client/assets/hcl-BWvSN4gD.js +1 -0
  120. package/dist/client/assets/hjson-D5-asLiD.js +1 -0
  121. package/dist/client/assets/hlsl-D3lLCCz7.js +1 -0
  122. package/dist/client/assets/horizon-BUw7H-hv.js +1 -0
  123. package/dist/client/assets/horizon-bright-Cn-bp-IR.js +1 -0
  124. package/dist/client/assets/houston-DnULxvSX.js +1 -0
  125. package/dist/client/assets/html-GMplVEZG.js +1 -0
  126. package/dist/client/assets/html-derivative-BFtXZ54Q.js +1 -0
  127. package/dist/client/assets/http-jrhK8wxY.js +1 -0
  128. package/dist/client/assets/hurl-irOxFIW8.js +1 -0
  129. package/dist/client/assets/hxml-Bvhsp5Yf.js +1 -0
  130. package/dist/client/assets/hy-DFXneXwc.js +1 -0
  131. package/dist/client/assets/imba-DGztddWO.js +1 -0
  132. package/dist/client/assets/index-Do7324M0.css +32 -0
  133. package/dist/client/assets/index-ktE9DLCD.js +2620 -0
  134. package/dist/client/assets/ini-BEwlwnbL.js +1 -0
  135. package/dist/client/assets/java-CylS5w8V.js +1 -0
  136. package/dist/client/assets/javascript-wDzz0qaB.js +1 -0
  137. package/dist/client/assets/jinja-4LBKfQ-Z.js +1 -0
  138. package/dist/client/assets/jison-wvAkD_A8.js +1 -0
  139. package/dist/client/assets/json-Cp-IABpG.js +1 -0
  140. package/dist/client/assets/json5-C9tS-k6U.js +1 -0
  141. package/dist/client/assets/jsonc-Des-eS-w.js +1 -0
  142. package/dist/client/assets/jsonl-DcaNXYhu.js +1 -0
  143. package/dist/client/assets/jsonnet-DFQXde-d.js +1 -0
  144. package/dist/client/assets/jssm-C2t-YnRu.js +1 -0
  145. package/dist/client/assets/jsx-g9-lgVsj.js +1 -0
  146. package/dist/client/assets/julia-CxzCAyBv.js +1 -0
  147. package/dist/client/assets/just-Cw27pwNe.js +1 -0
  148. package/dist/client/assets/kanagawa-dragon-CkXjmgJE.js +1 -0
  149. package/dist/client/assets/kanagawa-lotus-CfQXZHmo.js +1 -0
  150. package/dist/client/assets/kanagawa-wave-DWedfzmr.js +1 -0
  151. package/dist/client/assets/kdl-DV7GczEv.js +1 -0
  152. package/dist/client/assets/kotlin-BdnUsdx6.js +1 -0
  153. package/dist/client/assets/kusto-DZf3V79B.js +1 -0
  154. package/dist/client/assets/laserwave-DUszq2jm.js +1 -0
  155. package/dist/client/assets/latex-CWtU0Tv5.js +1 -0
  156. package/dist/client/assets/lean-BZvkOJ9d.js +1 -0
  157. package/dist/client/assets/less-B1dDrJ26.js +1 -0
  158. package/dist/client/assets/light-plus-B7mTdjB0.js +1 -0
  159. package/dist/client/assets/liquid-DYVedYrR.js +1 -0
  160. package/dist/client/assets/llvm-DjAJT7YJ.js +1 -0
  161. package/dist/client/assets/log-2UxHyX5q.js +1 -0
  162. package/dist/client/assets/logo-BtOb2qkB.js +1 -0
  163. package/dist/client/assets/lua-BaeVxFsk.js +1 -0
  164. package/dist/client/assets/luau-C-HG3fhB.js +1 -0
  165. package/dist/client/assets/make-CHLpvVh8.js +1 -0
  166. package/dist/client/assets/markdown-Cvjx9yec.js +1 -0
  167. package/dist/client/assets/marko-CnJfTvn9.js +1 -0
  168. package/dist/client/assets/material-theme-D5KoaKCx.js +1 -0
  169. package/dist/client/assets/material-theme-darker-BfHTSMKl.js +1 -0
  170. package/dist/client/assets/material-theme-lighter-B0m2ddpp.js +1 -0
  171. package/dist/client/assets/material-theme-ocean-CyktbL80.js +1 -0
  172. package/dist/client/assets/material-theme-palenight-Csfq5Kiy.js +1 -0
  173. package/dist/client/assets/matlab-D7o27uSR.js +1 -0
  174. package/dist/client/assets/mdc-BMNejdWA.js +1 -0
  175. package/dist/client/assets/mdx-Cmh6b_Ma.js +1 -0
  176. package/dist/client/assets/mermaid-mWjccvbQ.js +1 -0
  177. package/dist/client/assets/min-dark-CafNBF8u.js +1 -0
  178. package/dist/client/assets/min-light-CTRr51gU.js +1 -0
  179. package/dist/client/assets/mipsasm-CKIfxQSi.js +1 -0
  180. package/dist/client/assets/mojo-rZm6bMo-.js +1 -0
  181. package/dist/client/assets/monokai-D4h5O-jR.js +1 -0
  182. package/dist/client/assets/moonbit-_H4v1dQx.js +1 -0
  183. package/dist/client/assets/move-IF9eRakj.js +1 -0
  184. package/dist/client/assets/narrat-DRg8JJMk.js +1 -0
  185. package/dist/client/assets/nextflow-Zz6hmt5N.js +1 -0
  186. package/dist/client/assets/nextflow-groovy-BeH2EWoN.js +1 -0
  187. package/dist/client/assets/nginx-BpAMiNFr.js +1 -0
  188. package/dist/client/assets/night-owl-C39BiMTA.js +1 -0
  189. package/dist/client/assets/night-owl-light-CMTm3GFP.js +1 -0
  190. package/dist/client/assets/nim-CVrawwO9.js +1 -0
  191. package/dist/client/assets/nix-CwoSXNpI.js +1 -0
  192. package/dist/client/assets/nord-Ddv68eIx.js +1 -0
  193. package/dist/client/assets/nushell-Cz2AlsmD.js +1 -0
  194. package/dist/client/assets/objective-c-DXmwc3jG.js +1 -0
  195. package/dist/client/assets/objective-cpp-CLxacb5B.js +1 -0
  196. package/dist/client/assets/ocaml-C0hk2d4L.js +1 -0
  197. package/dist/client/assets/odin-BBf5iR-q.js +1 -0
  198. package/dist/client/assets/one-dark-pro-DVMEJ2y_.js +1 -0
  199. package/dist/client/assets/one-light-C3Wv6jpd.js +1 -0
  200. package/dist/client/assets/openscad-C4EeE6gA.js +1 -0
  201. package/dist/client/assets/pascal-D93ZcfNL.js +1 -0
  202. package/dist/client/assets/perl-C0TMdlhV.js +1 -0
  203. package/dist/client/assets/php-Dhbhpdrm.js +1 -0
  204. package/dist/client/assets/pierre-dark-DF2SEV7i.js +1 -0
  205. package/dist/client/assets/pierre-light-DOlZxES8.js +1 -0
  206. package/dist/client/assets/pkl-u5AG7uiY.js +1 -0
  207. package/dist/client/assets/plastic-3e1v2bzS.js +1 -0
  208. package/dist/client/assets/plsql-ChMvpjG-.js +1 -0
  209. package/dist/client/assets/po-BTJTHyun.js +1 -0
  210. package/dist/client/assets/poimandres-CS3Unz2-.js +1 -0
  211. package/dist/client/assets/polar-C0HS_06l.js +1 -0
  212. package/dist/client/assets/postcss-CXtECtnM.js +1 -0
  213. package/dist/client/assets/powerquery-CEu0bR-o.js +1 -0
  214. package/dist/client/assets/powershell-Dpen1YoG.js +1 -0
  215. package/dist/client/assets/prisma-Dd19v3D-.js +1 -0
  216. package/dist/client/assets/prolog-CbFg5uaA.js +1 -0
  217. package/dist/client/assets/proto-C7zT0LnQ.js +1 -0
  218. package/dist/client/assets/pug-CGlum2m_.js +1 -0
  219. package/dist/client/assets/puppet-BMWR74SV.js +1 -0
  220. package/dist/client/assets/purescript-CklMAg4u.js +1 -0
  221. package/dist/client/assets/python-B6aJPvgy.js +1 -0
  222. package/dist/client/assets/qml-3beO22l8.js +1 -0
  223. package/dist/client/assets/qmldir-C8lEn-DE.js +1 -0
  224. package/dist/client/assets/qss-IeuSbFQv.js +1 -0
  225. package/dist/client/assets/r-Dspwwk_N.js +1 -0
  226. package/dist/client/assets/racket-BqYA7rlc.js +1 -0
  227. package/dist/client/assets/raku-DXvB9xmW.js +1 -0
  228. package/dist/client/assets/razor-Uh8Bk_45.js +1 -0
  229. package/dist/client/assets/red-bN70gL4F.js +1 -0
  230. package/dist/client/assets/reg-C-SQnVFl.js +1 -0
  231. package/dist/client/assets/regexp-CDVJQ6XC.js +1 -0
  232. package/dist/client/assets/rel-C3B-1QV4.js +1 -0
  233. package/dist/client/assets/riscv-BM1_JUlF.js +1 -0
  234. package/dist/client/assets/ron-D8l8udqQ.js +1 -0
  235. package/dist/client/assets/rose-pine-dawn-DHQR4-dF.js +1 -0
  236. package/dist/client/assets/rose-pine-moon-D4_iv3hh.js +1 -0
  237. package/dist/client/assets/rose-pine-qdsjHGoJ.js +1 -0
  238. package/dist/client/assets/rosmsg-BJDFO7_C.js +1 -0
  239. package/dist/client/assets/rst-BrH8l1NY.js +1 -0
  240. package/dist/client/assets/ruby-Dw2BHqvy.js +1 -0
  241. package/dist/client/assets/rust-B1yitclQ.js +1 -0
  242. package/dist/client/assets/sas-cz2c8ADy.js +1 -0
  243. package/dist/client/assets/sass-Cj5Yp3dK.js +1 -0
  244. package/dist/client/assets/scala-C151Ov-r.js +1 -0
  245. package/dist/client/assets/scheme-C98Dy4si.js +1 -0
  246. package/dist/client/assets/scss-OYdSNvt2.js +1 -0
  247. package/dist/client/assets/sdbl-DVxCFoDh.js +1 -0
  248. package/dist/client/assets/shaderlab-Dg9Lc6iA.js +1 -0
  249. package/dist/client/assets/shellscript-Yzrsuije.js +1 -0
  250. package/dist/client/assets/shellsession-BADoaaVG.js +1 -0
  251. package/dist/client/assets/slack-dark-BthQWCQV.js +1 -0
  252. package/dist/client/assets/slack-ochin-DqwNpetd.js +1 -0
  253. package/dist/client/assets/smalltalk-BERRCDM3.js +1 -0
  254. package/dist/client/assets/snazzy-light-Bw305WKR.js +1 -0
  255. package/dist/client/assets/solarized-dark-DXbdFlpD.js +1 -0
  256. package/dist/client/assets/solarized-light-L9t79GZl.js +1 -0
  257. package/dist/client/assets/solidity-rGO070M0.js +1 -0
  258. package/dist/client/assets/soy-Brmx7dQM.js +1 -0
  259. package/dist/client/assets/sparql-rVzFXLq3.js +1 -0
  260. package/dist/client/assets/splunk-BtCnVYZw.js +1 -0
  261. package/dist/client/assets/sql-BLtJtn59.js +1 -0
  262. package/dist/client/assets/ssh-config-_ykCGR6B.js +1 -0
  263. package/dist/client/assets/stata-BH5u7GGu.js +1 -0
  264. package/dist/client/assets/stylus-BEDo0Tqx.js +1 -0
  265. package/dist/client/assets/surrealql-Bq5Q-fJD.js +1 -0
  266. package/dist/client/assets/svelte-C_ipcX3V.js +1 -0
  267. package/dist/client/assets/swift-D82vCrfD.js +1 -0
  268. package/dist/client/assets/synthwave-84-CbfX1IO0.js +1 -0
  269. package/dist/client/assets/system-verilog-CnnmHF94.js +1 -0
  270. package/dist/client/assets/systemd-4A_iFExJ.js +1 -0
  271. package/dist/client/assets/talonscript-CkByrt1z.js +1 -0
  272. package/dist/client/assets/tasl-QIJgUcNo.js +1 -0
  273. package/dist/client/assets/tcl-dwOrl1Do.js +1 -0
  274. package/dist/client/assets/templ-P3uqSqPl.js +1 -0
  275. package/dist/client/assets/terraform-BETggiCN.js +1 -0
  276. package/dist/client/assets/tex-idrVyKtj.js +1 -0
  277. package/dist/client/assets/tokyo-night-hegEt444.js +1 -0
  278. package/dist/client/assets/toml-vGWfd6FD.js +1 -0
  279. package/dist/client/assets/ts-tags-zn1MmPIZ.js +1 -0
  280. package/dist/client/assets/tsv-B_m7g4N7.js +1 -0
  281. package/dist/client/assets/tsx-COt5Ahok.js +1 -0
  282. package/dist/client/assets/turtle-BsS91CYL.js +1 -0
  283. package/dist/client/assets/twig-DNn4PbVi.js +1 -0
  284. package/dist/client/assets/typescript-BPQ3VLAy.js +1 -0
  285. package/dist/client/assets/typespec-BGHnOYBU.js +1 -0
  286. package/dist/client/assets/typst-DHCkPAjA.js +1 -0
  287. package/dist/client/assets/v-BcVCzyr7.js +1 -0
  288. package/dist/client/assets/vala-CsfeWuGM.js +1 -0
  289. package/dist/client/assets/vb-D17OF-Vu.js +1 -0
  290. package/dist/client/assets/verilog-BQ8w6xss.js +1 -0
  291. package/dist/client/assets/vesper-DU1UobuO.js +1 -0
  292. package/dist/client/assets/vhdl-CeAyd5Ju.js +1 -0
  293. package/dist/client/assets/viml-CJc9bBzg.js +1 -0
  294. package/dist/client/assets/vitesse-black-Bkuqu6BP.js +1 -0
  295. package/dist/client/assets/vitesse-dark-D0r3Knsf.js +1 -0
  296. package/dist/client/assets/vitesse-light-CVO1_9PV.js +1 -0
  297. package/dist/client/assets/vue-DN_0RTcg.js +1 -0
  298. package/dist/client/assets/vue-html-AaS7Mt5G.js +1 -0
  299. package/dist/client/assets/vue-vine-CQOfvN7w.js +1 -0
  300. package/dist/client/assets/vyper-CDx5xZoG.js +1 -0
  301. package/dist/client/assets/wasm-CG6Dc4jp.js +1 -0
  302. package/dist/client/assets/wasm-MzD3tlZU.js +1 -0
  303. package/dist/client/assets/wenyan-BV7otONQ.js +1 -0
  304. package/dist/client/assets/wgsl-Dx-B1_4e.js +1 -0
  305. package/dist/client/assets/wikitext-BhOHFoWU.js +1 -0
  306. package/dist/client/assets/wit-5i3qLPDT.js +1 -0
  307. package/dist/client/assets/wolfram-lXgVvXCa.js +1 -0
  308. package/dist/client/assets/xml-sdJ4AIDG.js +1 -0
  309. package/dist/client/assets/xsl-CtQFsRM5.js +1 -0
  310. package/dist/client/assets/yaml-Buea-lGh.js +1 -0
  311. package/dist/client/assets/zenscript-DVFEvuxE.js +1 -0
  312. package/dist/client/assets/zig-VOosw3JB.js +1 -0
  313. package/dist/client/chat-sounds/Blow.mp3 +0 -0
  314. package/dist/client/chat-sounds/Bottle.mp3 +0 -0
  315. package/dist/client/chat-sounds/Frog.mp3 +0 -0
  316. package/dist/client/chat-sounds/Funk.mp3 +0 -0
  317. package/dist/client/chat-sounds/Glass.mp3 +0 -0
  318. package/dist/client/chat-sounds/Ping.mp3 +0 -0
  319. package/dist/client/chat-sounds/Pop.mp3 +0 -0
  320. package/dist/client/chat-sounds/Purr.mp3 +0 -0
  321. package/dist/client/chat-sounds/Tink.mp3 +0 -0
  322. package/dist/client/editor-icons/cursor.png +0 -0
  323. package/dist/client/editor-icons/custom.png +0 -0
  324. package/dist/client/editor-icons/default-app.png +0 -0
  325. package/dist/client/editor-icons/finder.png +0 -0
  326. package/dist/client/editor-icons/preview.png +0 -0
  327. package/dist/client/editor-icons/terminal.png +0 -0
  328. package/dist/client/editor-icons/windsurf.png +0 -0
  329. package/dist/client/editor-icons/xcode.png +0 -0
  330. package/dist/client/favicon.png +0 -0
  331. package/dist/client/fonts/body-medium.woff2 +0 -0
  332. package/dist/client/fonts/body-regular-italic.woff2 +0 -0
  333. package/dist/client/fonts/body-regular.woff2 +0 -0
  334. package/dist/client/fonts/body-semibold.woff2 +0 -0
  335. package/dist/client/icon-192.png +0 -0
  336. package/dist/client/icon-512.png +0 -0
  337. package/dist/client/icon-maskable-512.png +0 -0
  338. package/dist/client/icon.svg +4 -0
  339. package/dist/client/index.html +34 -0
  340. package/dist/client/manifest.webmanifest +46 -0
  341. package/dist/client/screenshot-light.png +0 -0
  342. package/dist/client/screenshot.png +0 -0
  343. package/dist/export-viewer/assets/bricolage-grotesque-latin-ext-wght-normal-CcLUaPy7.woff2 +0 -0
  344. package/dist/export-viewer/assets/bricolage-grotesque-latin-wght-normal-DLoelf7F.woff2 +0 -0
  345. package/dist/export-viewer/assets/bricolage-grotesque-vietnamese-wght-normal-BUzh504Q.woff2 +0 -0
  346. package/dist/export-viewer/assets/index-D1qUumZR.js +410 -0
  347. package/dist/export-viewer/assets/index-gG2nMW51.css +1 -0
  348. package/dist/export-viewer/editor-icons/cursor.png +0 -0
  349. package/dist/export-viewer/editor-icons/custom.png +0 -0
  350. package/dist/export-viewer/editor-icons/default-app.png +0 -0
  351. package/dist/export-viewer/editor-icons/finder.png +0 -0
  352. package/dist/export-viewer/editor-icons/preview.png +0 -0
  353. package/dist/export-viewer/editor-icons/terminal.png +0 -0
  354. package/dist/export-viewer/editor-icons/windsurf.png +0 -0
  355. package/dist/export-viewer/editor-icons/xcode.png +0 -0
  356. package/dist/export-viewer/fonts/body-medium.woff2 +0 -0
  357. package/dist/export-viewer/fonts/body-regular-italic.woff2 +0 -0
  358. package/dist/export-viewer/fonts/body-regular.woff2 +0 -0
  359. package/dist/export-viewer/fonts/body-semibold.woff2 +0 -0
  360. package/dist/export-viewer/index.html +14 -0
  361. package/package.json +99 -0
  362. package/src/server/__fixtures__/claude-session-empty.jsonl +0 -0
  363. package/src/server/__fixtures__/claude-session-malformed.jsonl +3 -0
  364. package/src/server/__fixtures__/claude-session-valid.jsonl +6 -0
  365. package/src/server/agent.test.ts +2369 -0
  366. package/src/server/agent.ts +1927 -0
  367. package/src/server/analytics.test.ts +313 -0
  368. package/src/server/analytics.ts +131 -0
  369. package/src/server/app-settings.test.ts +233 -0
  370. package/src/server/app-settings.ts +548 -0
  371. package/src/server/auth.test.ts +329 -0
  372. package/src/server/auth.ts +204 -0
  373. package/src/server/auto-continue/e2e.test.ts +215 -0
  374. package/src/server/auto-continue/events.test.ts +30 -0
  375. package/src/server/auto-continue/events.ts +35 -0
  376. package/src/server/auto-continue/limit-detector.test.ts +153 -0
  377. package/src/server/auto-continue/limit-detector.ts +159 -0
  378. package/src/server/auto-continue/read-model.test.ts +109 -0
  379. package/src/server/auto-continue/read-model.ts +83 -0
  380. package/src/server/auto-continue/schedule-manager.test.ts +155 -0
  381. package/src/server/auto-continue/schedule-manager.ts +116 -0
  382. package/src/server/claude-session-importer.test.ts +214 -0
  383. package/src/server/claude-session-importer.ts +187 -0
  384. package/src/server/claude-session-mapper.test.ts +88 -0
  385. package/src/server/claude-session-mapper.ts +106 -0
  386. package/src/server/claude-session-parser.test.ts +38 -0
  387. package/src/server/claude-session-parser.ts +67 -0
  388. package/src/server/claude-session-scanner.test.ts +49 -0
  389. package/src/server/claude-session-scanner.ts +24 -0
  390. package/src/server/claude-session-types.ts +61 -0
  391. package/src/server/cli-runtime.test.ts +523 -0
  392. package/src/server/cli-runtime.ts +405 -0
  393. package/src/server/cli-supervisor.ts +102 -0
  394. package/src/server/cli.ts +64 -0
  395. package/src/server/cloudflare-tunnel/agent-integration.test.ts +76 -0
  396. package/src/server/cloudflare-tunnel/agent-integration.ts +55 -0
  397. package/src/server/cloudflare-tunnel/detector.test.ts +72 -0
  398. package/src/server/cloudflare-tunnel/detector.ts +44 -0
  399. package/src/server/cloudflare-tunnel/e2e.test.ts +194 -0
  400. package/src/server/cloudflare-tunnel/events.test.ts +43 -0
  401. package/src/server/cloudflare-tunnel/events.ts +31 -0
  402. package/src/server/cloudflare-tunnel/gateway.ts +143 -0
  403. package/src/server/cloudflare-tunnel/lifecycle.test.ts +48 -0
  404. package/src/server/cloudflare-tunnel/lifecycle.ts +62 -0
  405. package/src/server/cloudflare-tunnel/read-model.test.ts +69 -0
  406. package/src/server/cloudflare-tunnel/read-model.ts +80 -0
  407. package/src/server/cloudflare-tunnel/tunnel-manager.test.ts +116 -0
  408. package/src/server/cloudflare-tunnel/tunnel-manager.ts +165 -0
  409. package/src/server/codex-app-server-protocol.ts +487 -0
  410. package/src/server/codex-app-server.test.ts +1816 -0
  411. package/src/server/codex-app-server.ts +1475 -0
  412. package/src/server/diff-store.test.ts +737 -0
  413. package/src/server/diff-store.ts +2199 -0
  414. package/src/server/discovery.test.ts +211 -0
  415. package/src/server/discovery.ts +301 -0
  416. package/src/server/event-store.test.ts +797 -0
  417. package/src/server/event-store.ts +1421 -0
  418. package/src/server/events.ts +217 -0
  419. package/src/server/external-open.test.ts +112 -0
  420. package/src/server/external-open.ts +345 -0
  421. package/src/server/generate-commit-message.test.ts +79 -0
  422. package/src/server/generate-commit-message.ts +126 -0
  423. package/src/server/generate-title.ts +76 -0
  424. package/src/server/harness-types.ts +19 -0
  425. package/src/server/keybindings.test.ts +144 -0
  426. package/src/server/keybindings.ts +178 -0
  427. package/src/server/llm-provider.test.ts +134 -0
  428. package/src/server/llm-provider.ts +207 -0
  429. package/src/server/machine-name.ts +22 -0
  430. package/src/server/paths-route.test.ts +64 -0
  431. package/src/server/paths.ts +35 -0
  432. package/src/server/process-utils.test.ts +12 -0
  433. package/src/server/process-utils.ts +47 -0
  434. package/src/server/project-paths.test.ts +95 -0
  435. package/src/server/project-paths.ts +191 -0
  436. package/src/server/provider-catalog.test.ts +69 -0
  437. package/src/server/provider-catalog.ts +87 -0
  438. package/src/server/quick-response.test.ts +440 -0
  439. package/src/server/quick-response.ts +300 -0
  440. package/src/server/read-models.test.ts +509 -0
  441. package/src/server/read-models.ts +230 -0
  442. package/src/server/restart.test.ts +27 -0
  443. package/src/server/restart.ts +30 -0
  444. package/src/server/server.ts +616 -0
  445. package/src/server/share.test.ts +180 -0
  446. package/src/server/share.ts +150 -0
  447. package/src/server/standalone-export.test.ts +224 -0
  448. package/src/server/standalone-export.ts +419 -0
  449. package/src/server/terminal-manager.test.ts +315 -0
  450. package/src/server/terminal-manager.ts +350 -0
  451. package/src/server/test-helpers/async-event-queue.ts +52 -0
  452. package/src/server/test-helpers/wait-for.ts +14 -0
  453. package/src/server/title-generation.live.test.ts +44 -0
  454. package/src/server/update-manager.test.ts +158 -0
  455. package/src/server/update-manager.ts +222 -0
  456. package/src/server/update-strategy.test.ts +237 -0
  457. package/src/server/update-strategy.ts +241 -0
  458. package/src/server/uploads.test.ts +292 -0
  459. package/src/server/uploads.ts +131 -0
  460. package/src/server/ws-router.test.ts +2292 -0
  461. package/src/server/ws-router.ts +1465 -0
  462. package/src/shared/analytics.ts +30 -0
  463. package/src/shared/branding.test.ts +31 -0
  464. package/src/shared/branding.ts +77 -0
  465. package/src/shared/dev-ports.test.ts +113 -0
  466. package/src/shared/dev-ports.ts +134 -0
  467. package/src/shared/ports.ts +2 -0
  468. package/src/shared/protocol.ts +257 -0
  469. package/src/shared/share.ts +27 -0
  470. package/src/shared/tools.test.ts +164 -0
  471. package/src/shared/tools.ts +327 -0
  472. package/src/shared/types.test.ts +25 -0
  473. 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
+ }