@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,72 @@
1
+ import { describe, expect, test } from "bun:test"
2
+ import { evaluateBashOutput } from "./detector"
3
+
4
+ describe("evaluateBashOutput", () => {
5
+ test("extracts port from Vite-style localhost URL", () => {
6
+ const result = evaluateBashOutput({
7
+ command: "bun run dev",
8
+ stdout: " ➜ Local: http://localhost:5173/\n",
9
+ })
10
+ expect(result).toEqual({ isServer: true, ports: [5173] })
11
+ })
12
+
13
+ test("extracts port from 'listening on PORT'", () => {
14
+ const result = evaluateBashOutput({
15
+ command: "go run main.go",
16
+ stdout: "Server listening on port 8080\n",
17
+ })
18
+ expect(result).toEqual({ isServer: true, ports: [8080] })
19
+ })
20
+
21
+ test("dedups + sorts multiple ports", () => {
22
+ const result = evaluateBashOutput({
23
+ command: "bun run dev",
24
+ stdout: "Local: http://localhost:5174\nNetwork: http://127.0.0.1:5174\nHMR ready on port 5173\n",
25
+ })
26
+ expect(result).toEqual({ isServer: true, ports: [5173, 5174] })
27
+ })
28
+
29
+ test("handles ipv6 [::1]:port", () => {
30
+ const result = evaluateBashOutput({
31
+ command: "node server.js",
32
+ stdout: "Listening on [::1]:3000\n",
33
+ })
34
+ expect(result).toEqual({ isServer: true, ports: [3000] })
35
+ })
36
+
37
+ test("handles 0.0.0.0:port", () => {
38
+ const result = evaluateBashOutput({
39
+ command: "uvicorn app:main",
40
+ stdout: "Uvicorn running on http://0.0.0.0:8000 (Press CTRL+C to quit)\n",
41
+ })
42
+ expect(result).toEqual({ isServer: true, ports: [8000] })
43
+ })
44
+
45
+ test("returns no-server for non-server output", () => {
46
+ expect(evaluateBashOutput({ command: "ls", stdout: "a b c\n" })).toEqual({ isServer: false })
47
+ })
48
+
49
+ test("rejects ports below 1024", () => {
50
+ const result = evaluateBashOutput({
51
+ command: "x",
52
+ stdout: "localhost:80 listening\n",
53
+ })
54
+ expect(result).toEqual({ isServer: false })
55
+ })
56
+
57
+ test("caps at 5 ports", () => {
58
+ const stdout = Array.from({ length: 10 }, (_, i) => `localhost:${5000 + i}`).join("\n")
59
+ const result = evaluateBashOutput({ command: "x", stdout })
60
+ if (result.isServer) {
61
+ expect(result.ports).toHaveLength(5)
62
+ } else {
63
+ throw new Error("expected isServer true")
64
+ }
65
+ })
66
+
67
+ test("trims stdout to last 8KB", () => {
68
+ const stdout = "x".repeat(20_000) + "\nLocal: http://localhost:5173"
69
+ const result = evaluateBashOutput({ command: "x", stdout })
70
+ expect(result).toEqual({ isServer: true, ports: [5173] })
71
+ })
72
+ })
@@ -0,0 +1,44 @@
1
+ export interface DetectorInput {
2
+ command: string
3
+ stdout: string
4
+ }
5
+
6
+ export type DetectorResult =
7
+ | { isServer: true; ports: number[] }
8
+ | { isServer: false }
9
+
10
+ const STDOUT_TAIL_LIMIT = 8192
11
+ const MAX_PORTS = 5
12
+ const MIN_PORT = 1024
13
+ const MAX_PORT = 65535
14
+
15
+ const STRONG_PATTERNS: RegExp[] = [
16
+ /\blocalhost:(\d+)/gi,
17
+ /\b127\.0\.0\.1:(\d+)/gi,
18
+ /\b0\.0\.0\.0:(\d+)/gi,
19
+ /\[::1?\]:(\d+)/gi,
20
+ /\bhttps?:\/\/[^/\s:]+:(\d+)/gi,
21
+ /(?:listening|ready|started|running)\s+(?:on\s+)?(?:port\s+)?:?(\d{4,5})\b/gi,
22
+ /\bport\s+(\d{4,5})\b/gi,
23
+ ]
24
+
25
+ export function evaluateBashOutput(input: DetectorInput): DetectorResult {
26
+ const tail = input.stdout.slice(-STDOUT_TAIL_LIMIT)
27
+ const found = new Set<number>()
28
+
29
+ for (const pattern of STRONG_PATTERNS) {
30
+ pattern.lastIndex = 0
31
+ let match: RegExpExecArray | null
32
+ while ((match = pattern.exec(tail)) !== null) {
33
+ const port = Number.parseInt(match[1] ?? "", 10)
34
+ if (Number.isInteger(port) && port >= MIN_PORT && port <= MAX_PORT) {
35
+ found.add(port)
36
+ if (found.size >= MAX_PORTS) break
37
+ }
38
+ }
39
+ if (found.size >= MAX_PORTS) break
40
+ }
41
+
42
+ if (found.size === 0) return { isServer: false }
43
+ return { isServer: true, ports: [...found].sort((a, b) => a - b) }
44
+ }
@@ -0,0 +1,194 @@
1
+ import { afterEach, beforeEach, 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 { AppSettingsManager } from "../app-settings"
6
+ import { EventStore } from "../event-store"
7
+ import { waitFor } from "../test-helpers/wait-for"
8
+ import type { CloudflareTunnelEvent } from "./events"
9
+ import { TunnelGateway } from "./gateway"
10
+ import { TunnelLifecycle } from "./lifecycle"
11
+ import { TunnelManager, type ChildHandle } from "./tunnel-manager"
12
+
13
+ interface FakeChild extends ChildHandle {
14
+ emitStdout: (chunk: string) => void
15
+ emitExit: (code: number) => void
16
+ }
17
+
18
+ function fakeChild(): FakeChild {
19
+ const stdoutListeners: Array<(c: string) => void> = []
20
+ const exitListeners: Array<(c: number) => void> = []
21
+ const child: FakeChild = {
22
+ pid: 9999,
23
+ kill: () => {
24
+ for (const l of exitListeners) l(0)
25
+ },
26
+ onStdout: (l: (chunk: string) => void) => {
27
+ stdoutListeners.push(l)
28
+ },
29
+ onStderr: () => {},
30
+ onExit: (l: (code: number) => void) => {
31
+ exitListeners.push(l)
32
+ },
33
+ isKilled: () => false,
34
+ emitStdout: (chunk: string) => {
35
+ for (const l of stdoutListeners) l(chunk)
36
+ },
37
+ emitExit: (code: number) => {
38
+ for (const l of exitListeners) l(code)
39
+ },
40
+ }
41
+ return child
42
+ }
43
+
44
+ describe("cloudflare tunnel e2e", () => {
45
+ let dataDir: string
46
+ let store: EventStore
47
+ let appSettings: AppSettingsManager
48
+ let manager: TunnelManager
49
+ let lifecycle: TunnelLifecycle
50
+ let gateway: TunnelGateway
51
+ let pendingChildren: FakeChild[]
52
+ let broadcasts: string[]
53
+
54
+ beforeEach(async () => {
55
+ dataDir = await mkdtemp(join(tmpdir(), "kanna-tunnel-e2e-"))
56
+ store = new EventStore(dataDir)
57
+ await store.initialize()
58
+
59
+ const settingsPath = join(dataDir, "settings.json")
60
+ await Bun.write(
61
+ settingsPath,
62
+ JSON.stringify({
63
+ analyticsEnabled: false,
64
+ cloudflareTunnel: { enabled: true, cloudflaredPath: "cloudflared", mode: "always-ask" },
65
+ }),
66
+ )
67
+ appSettings = new AppSettingsManager(settingsPath)
68
+ await appSettings.initialize()
69
+
70
+ pendingChildren = []
71
+ broadcasts = []
72
+
73
+ manager = new TunnelManager({
74
+ cloudflaredPath: "cloudflared",
75
+ spawn: () => {
76
+ const child = fakeChild()
77
+ pendingChildren.push(child)
78
+ return child
79
+ },
80
+ onEvent: (event: CloudflareTunnelEvent) => {
81
+ void store.appendTunnelEvent(event)
82
+ broadcasts.push(event.chatId)
83
+ },
84
+ })
85
+ lifecycle = new TunnelLifecycle({
86
+ pollIntervalMs: 1000,
87
+ isPidAlive: () => true,
88
+ onSourceExit: () => {},
89
+ })
90
+ gateway = new TunnelGateway({
91
+ manager,
92
+ lifecycle,
93
+ settings: appSettings,
94
+ store,
95
+ broadcast: (chatId: string) => {
96
+ broadcasts.push(chatId)
97
+ },
98
+ })
99
+ })
100
+
101
+ afterEach(async () => {
102
+ gateway.shutdown()
103
+ appSettings.dispose()
104
+ await rm(dataDir, { recursive: true, force: true })
105
+ })
106
+
107
+ test("propose → accept → active → stop full flow", async () => {
108
+ await gateway.handleBashResult({
109
+ command: "bun run dev",
110
+ stdout: "Local: http://localhost:5173",
111
+ chatId: "c1",
112
+ sourcePid: null,
113
+ })
114
+
115
+ await waitFor(
116
+ () => store.getTunnelEvents("c1").some((e) => e.kind === "tunnel_proposed"),
117
+ 2000,
118
+ "tunnel_proposed event",
119
+ )
120
+
121
+ const eventsAfterPropose = store.getTunnelEvents("c1")
122
+ expect(eventsAfterPropose.some((e) => e.kind === "tunnel_proposed")).toBe(true)
123
+ const proposed = eventsAfterPropose.find((e) => e.kind === "tunnel_proposed")
124
+ expect(proposed).toBeDefined()
125
+ if (!proposed || proposed.kind !== "tunnel_proposed") throw new Error("no proposed event")
126
+ expect(proposed.port).toBe(5173)
127
+
128
+ await gateway.accept("c1", proposed.tunnelId)
129
+ expect(pendingChildren).toHaveLength(1)
130
+
131
+ pendingChildren[0].emitStdout("https://abc.trycloudflare.com\n")
132
+
133
+ await waitFor(
134
+ () => store.getTunnelEvents("c1").some((e) => e.kind === "tunnel_active"),
135
+ 2000,
136
+ "tunnel_active event",
137
+ )
138
+
139
+ const eventsAfterActive = store.getTunnelEvents("c1")
140
+ const active = eventsAfterActive.find((e) => e.kind === "tunnel_active")
141
+ expect(active).toBeDefined()
142
+ if (!active || active.kind !== "tunnel_active") throw new Error("no active event")
143
+ expect(active.url).toBe("https://abc.trycloudflare.com")
144
+
145
+ await gateway.stop("c1", proposed.tunnelId)
146
+
147
+ await waitFor(
148
+ () => store.getTunnelEvents("c1").some((e) => e.kind === "tunnel_stopped"),
149
+ 2000,
150
+ "tunnel_stopped event",
151
+ )
152
+
153
+ const eventsAfterStop = store.getTunnelEvents("c1")
154
+ const stopped = eventsAfterStop.find((e) => e.kind === "tunnel_stopped")
155
+ expect(stopped).toBeDefined()
156
+ if (stopped && stopped.kind === "tunnel_stopped") {
157
+ expect(stopped.reason).toBe("user")
158
+ }
159
+ })
160
+
161
+ test("disabled setting → no proposed event", async () => {
162
+ await appSettings.setCloudflareTunnel({ enabled: false })
163
+ await gateway.handleBashResult({
164
+ command: "bun run dev",
165
+ stdout: "Local: http://localhost:5173",
166
+ chatId: "c1",
167
+ sourcePid: null,
168
+ })
169
+ expect(store.getTunnelEvents("c1")).toEqual([])
170
+ })
171
+
172
+ test("auto-expose mode triggers cloudflared without explicit accept", async () => {
173
+ await appSettings.setCloudflareTunnel({ mode: "auto-expose" })
174
+ await gateway.handleBashResult({
175
+ command: "bun run dev",
176
+ stdout: "Local: http://localhost:5173",
177
+ chatId: "c1",
178
+ sourcePid: null,
179
+ })
180
+
181
+ await waitFor(
182
+ () => store.getTunnelEvents("c1").some((e) => e.kind === "tunnel_accepted"),
183
+ 2000,
184
+ "tunnel_accepted event",
185
+ )
186
+
187
+ expect(pendingChildren).toHaveLength(1)
188
+ const accepted = store.getTunnelEvents("c1").find((e) => e.kind === "tunnel_accepted")
189
+ expect(accepted).toBeDefined()
190
+ if (accepted && accepted.kind === "tunnel_accepted") {
191
+ expect(accepted.source).toBe("auto_setting")
192
+ }
193
+ })
194
+ })
@@ -0,0 +1,43 @@
1
+ import { describe, expect, test } from "bun:test"
2
+ import { CLOUDFLARE_TUNNEL_EVENT_VERSION, type CloudflareTunnelEvent } from "./events"
3
+
4
+ describe("cloudflare tunnel events", () => {
5
+ test("event version is 1", () => {
6
+ expect(CLOUDFLARE_TUNNEL_EVENT_VERSION).toBe(1)
7
+ })
8
+
9
+ test("discriminated union allows all five kinds", () => {
10
+ const kinds: CloudflareTunnelEvent["kind"][] = [
11
+ "tunnel_proposed",
12
+ "tunnel_accepted",
13
+ "tunnel_active",
14
+ "tunnel_stopped",
15
+ "tunnel_failed",
16
+ ]
17
+ expect(kinds).toHaveLength(5)
18
+ })
19
+
20
+ test("tunnel_proposed event is well-typed and fields are accessible", () => {
21
+ const event: CloudflareTunnelEvent = {
22
+ v: CLOUDFLARE_TUNNEL_EVENT_VERSION,
23
+ kind: "tunnel_proposed",
24
+ timestamp: 1_000,
25
+ chatId: "c1",
26
+ tunnelId: "t1",
27
+ port: 5173,
28
+ sourcePid: null,
29
+ }
30
+ expect(event.port).toBe(5173)
31
+ expect(event.sourcePid).toBeNull()
32
+ })
33
+
34
+ test("tunnel_stopped reason covers all four lifecycle paths", () => {
35
+ const reasons: ("user" | "source_exited" | "session_closed" | "server_shutdown")[] = [
36
+ "user",
37
+ "source_exited",
38
+ "session_closed",
39
+ "server_shutdown",
40
+ ]
41
+ expect(reasons).toHaveLength(4)
42
+ })
43
+ })
@@ -0,0 +1,31 @@
1
+ export const CLOUDFLARE_TUNNEL_EVENT_VERSION = 1 as const
2
+
3
+ interface BaseTunnelEvent {
4
+ v: typeof CLOUDFLARE_TUNNEL_EVENT_VERSION
5
+ timestamp: number
6
+ chatId: string
7
+ tunnelId: string
8
+ }
9
+
10
+ export type CloudflareTunnelEvent =
11
+ | (BaseTunnelEvent & {
12
+ kind: "tunnel_proposed"
13
+ port: number
14
+ sourcePid: number | null
15
+ })
16
+ | (BaseTunnelEvent & {
17
+ kind: "tunnel_accepted"
18
+ source: "user" | "auto_setting"
19
+ })
20
+ | (BaseTunnelEvent & {
21
+ kind: "tunnel_active"
22
+ url: string
23
+ })
24
+ | (BaseTunnelEvent & {
25
+ kind: "tunnel_stopped"
26
+ reason: "user" | "source_exited" | "session_closed" | "server_shutdown"
27
+ })
28
+ | (BaseTunnelEvent & {
29
+ kind: "tunnel_failed"
30
+ error: string
31
+ })
@@ -0,0 +1,143 @@
1
+ import type { AppSettingsManager } from "../app-settings"
2
+ import type { EventStore } from "../event-store"
3
+ import { handleBashToolResult } from "./agent-integration"
4
+ import type { CloudflareTunnelEvent } from "./events"
5
+ import { CLOUDFLARE_TUNNEL_EVENT_VERSION } from "./events"
6
+ import { TunnelLifecycle } from "./lifecycle"
7
+ import { deriveChatTunnels } from "./read-model"
8
+ import { TunnelManager } from "./tunnel-manager"
9
+
10
+ export interface TunnelGatewayArgs {
11
+ manager: TunnelManager
12
+ lifecycle: TunnelLifecycle
13
+ settings: AppSettingsManager
14
+ store: EventStore
15
+ broadcast: (chatId: string) => void
16
+ now?: () => number
17
+ }
18
+
19
+ export class TunnelGateway {
20
+ private readonly manager: TunnelManager
21
+ private readonly lifecycle: TunnelLifecycle
22
+ private readonly settings: AppSettingsManager
23
+ private readonly store: EventStore
24
+ private readonly broadcast: (chatId: string) => void
25
+ private readonly now: () => number
26
+ // tunnelId → sourcePid for retry
27
+ private readonly proposedSourcePid = new Map<string, number | null>()
28
+
29
+ constructor(args: TunnelGatewayArgs) {
30
+ this.manager = args.manager
31
+ this.lifecycle = args.lifecycle
32
+ this.settings = args.settings
33
+ this.store = args.store
34
+ this.broadcast = args.broadcast
35
+ this.now = args.now ?? (() => Date.now())
36
+ }
37
+
38
+ async reapOrphanedTunnels(): Promise<void> {
39
+ const chatIds = this.store.listTunnelChats()
40
+ for (const chatId of chatIds) {
41
+ const projection = deriveChatTunnels(this.store.getTunnelEvents(chatId), chatId)
42
+ for (const record of Object.values(projection.tunnels)) {
43
+ if (record.state !== "proposed" && record.state !== "active") continue
44
+ await this.persist({
45
+ v: CLOUDFLARE_TUNNEL_EVENT_VERSION,
46
+ kind: "tunnel_stopped",
47
+ timestamp: this.now(),
48
+ chatId,
49
+ tunnelId: record.tunnelId,
50
+ reason: "server_shutdown",
51
+ })
52
+ }
53
+ }
54
+ }
55
+
56
+ async handleBashResult(args: { command: string; stdout: string; chatId: string; sourcePid: number | null }): Promise<void> {
57
+ const snapshot = this.settings.getSnapshot()
58
+ const livePorts = this.collectLivePorts(args.chatId)
59
+ const skippedTunnels = new Set<string>()
60
+ await handleBashToolResult({
61
+ command: args.command,
62
+ stdout: args.stdout,
63
+ chatId: args.chatId,
64
+ sourcePid: args.sourcePid,
65
+ settings: snapshot.cloudflareTunnel,
66
+ onEvent: (e: CloudflareTunnelEvent) => {
67
+ if (e.kind === "tunnel_proposed") {
68
+ if (livePorts.has(e.port)) {
69
+ skippedTunnels.add(e.tunnelId)
70
+ return
71
+ }
72
+ this.proposedSourcePid.set(e.tunnelId, e.sourcePid)
73
+ }
74
+ if (skippedTunnels.has(e.tunnelId)) return
75
+ void this.persist(e)
76
+ },
77
+ autoStart: async (a) => {
78
+ if (skippedTunnels.has(a.tunnelId)) return
79
+ await this.manager.start({ chatId: a.chatId, port: a.port, sourcePid: a.sourcePid, tunnelId: a.tunnelId })
80
+ this.lifecycle.watch(a.tunnelId, a.sourcePid)
81
+ },
82
+ now: this.now,
83
+ })
84
+ }
85
+
86
+ private collectLivePorts(chatId: string): Set<number> {
87
+ const events = this.store.getTunnelEvents(chatId)
88
+ const projection = deriveChatTunnels(events, chatId)
89
+ const ports = new Set<number>()
90
+ for (const record of Object.values(projection.tunnels)) {
91
+ if (record.state === "proposed" || record.state === "active") {
92
+ ports.add(record.port)
93
+ }
94
+ }
95
+ return ports
96
+ }
97
+
98
+ async accept(chatId: string, tunnelId: string): Promise<void> {
99
+ const sourcePid = this.proposedSourcePid.get(tunnelId) ?? null
100
+ const proposedEvents = this.store.getTunnelEvents(chatId).filter((e: CloudflareTunnelEvent) => e.tunnelId === tunnelId)
101
+ const proposed = proposedEvents.find((e: CloudflareTunnelEvent) => e.kind === "tunnel_proposed")
102
+ if (!proposed || proposed.kind !== "tunnel_proposed") return
103
+ await this.persist({
104
+ v: CLOUDFLARE_TUNNEL_EVENT_VERSION,
105
+ kind: "tunnel_accepted",
106
+ timestamp: this.now(),
107
+ chatId,
108
+ tunnelId,
109
+ source: "user",
110
+ })
111
+ await this.manager.start({ chatId, port: proposed.port, sourcePid, tunnelId })
112
+ this.lifecycle.watch(tunnelId, sourcePid)
113
+ }
114
+
115
+ async stop(chatId: string, tunnelId: string): Promise<void> {
116
+ this.lifecycle.unwatch(tunnelId)
117
+ await this.manager.stop(tunnelId, "user")
118
+ void chatId // chatId may be useful for logging/auditing
119
+ }
120
+
121
+ async retry(chatId: string, tunnelId: string): Promise<void> {
122
+ // For v1, retry just re-runs accept on the existing proposed record.
123
+ await this.accept(chatId, tunnelId)
124
+ }
125
+
126
+ closeChat(chatId: string): void {
127
+ const events = this.store.getTunnelEvents(chatId)
128
+ const live = deriveChatTunnels(events, chatId).liveTunnelId
129
+ if (!live) return
130
+ this.lifecycle.unwatch(live)
131
+ void this.manager.stop(live, "session_closed")
132
+ }
133
+
134
+ shutdown(): void {
135
+ this.lifecycle.shutdown()
136
+ this.manager.shutdown()
137
+ }
138
+
139
+ private async persist(event: CloudflareTunnelEvent): Promise<void> {
140
+ await this.store.appendTunnelEvent(event)
141
+ this.broadcast(event.chatId)
142
+ }
143
+ }
@@ -0,0 +1,48 @@
1
+ import { describe, expect, test } from "bun:test"
2
+ import { TunnelLifecycle } from "./lifecycle"
3
+
4
+ describe("TunnelLifecycle", () => {
5
+ test("polls source PID; calls onSourceExit when process gone", async () => {
6
+ const exited: string[] = []
7
+ let alive = true
8
+ const lc = new TunnelLifecycle({
9
+ pollIntervalMs: 5,
10
+ isPidAlive: () => alive,
11
+ onSourceExit: (id: string) => exited.push(id),
12
+ })
13
+ lc.watch("t1", 1234)
14
+ alive = false
15
+ await new Promise((r) => setTimeout(r, 30))
16
+ expect(exited).toContain("t1")
17
+ lc.shutdown()
18
+ })
19
+
20
+ test("unwatch stops polling for a tunnel", async () => {
21
+ const exited: string[] = []
22
+ let alive = true
23
+ const lc = new TunnelLifecycle({
24
+ pollIntervalMs: 5,
25
+ isPidAlive: () => alive,
26
+ onSourceExit: (id: string) => exited.push(id),
27
+ })
28
+ lc.watch("t1", 1234)
29
+ lc.unwatch("t1")
30
+ alive = false
31
+ await new Promise((r) => setTimeout(r, 30))
32
+ expect(exited).toEqual([])
33
+ lc.shutdown()
34
+ })
35
+
36
+ test("does not fire onSourceExit when sourcePid is null", async () => {
37
+ const exited: string[] = []
38
+ const lc = new TunnelLifecycle({
39
+ pollIntervalMs: 5,
40
+ isPidAlive: () => false,
41
+ onSourceExit: (id: string) => exited.push(id),
42
+ })
43
+ lc.watch("t1", null)
44
+ await new Promise((r) => setTimeout(r, 30))
45
+ expect(exited).toEqual([])
46
+ lc.shutdown()
47
+ })
48
+ })
@@ -0,0 +1,62 @@
1
+ export interface TunnelLifecycleArgs {
2
+ pollIntervalMs?: number
3
+ isPidAlive?: (pid: number) => boolean
4
+ onSourceExit: (tunnelId: string) => void
5
+ }
6
+
7
+ export class TunnelLifecycle {
8
+ private readonly pollIntervalMs: number
9
+ private readonly isPidAlive: (pid: number) => boolean
10
+ private readonly onSourceExit: (tunnelId: string) => void
11
+ private readonly watched = new Map<string, number | null>()
12
+ private timer: ReturnType<typeof setInterval> | null = null
13
+
14
+ constructor(args: TunnelLifecycleArgs) {
15
+ this.pollIntervalMs = args.pollIntervalMs ?? 1500
16
+ this.isPidAlive = args.isPidAlive ?? defaultIsPidAlive
17
+ this.onSourceExit = args.onSourceExit
18
+ }
19
+
20
+ watch(tunnelId: string, sourcePid: number | null) {
21
+ this.watched.set(tunnelId, sourcePid)
22
+ this.ensureTimer()
23
+ }
24
+
25
+ unwatch(tunnelId: string) {
26
+ this.watched.delete(tunnelId)
27
+ if (this.watched.size === 0 && this.timer) {
28
+ clearInterval(this.timer)
29
+ this.timer = null
30
+ }
31
+ }
32
+
33
+ shutdown() {
34
+ if (this.timer) clearInterval(this.timer)
35
+ this.timer = null
36
+ this.watched.clear()
37
+ }
38
+
39
+ private ensureTimer() {
40
+ if (this.timer) return
41
+ this.timer = setInterval(() => this.tick(), this.pollIntervalMs)
42
+ }
43
+
44
+ private tick() {
45
+ for (const [tunnelId, pid] of [...this.watched.entries()]) {
46
+ if (pid === null) continue
47
+ if (!this.isPidAlive(pid)) {
48
+ this.unwatch(tunnelId)
49
+ this.onSourceExit(tunnelId)
50
+ }
51
+ }
52
+ }
53
+ }
54
+
55
+ function defaultIsPidAlive(pid: number): boolean {
56
+ try {
57
+ process.kill(pid, 0)
58
+ return true
59
+ } catch {
60
+ return false
61
+ }
62
+ }