@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,313 @@
1
+ import { describe, expect, test } from "bun:test"
2
+ import { CLOUDFLARE_TUNNEL_DEFAULTS } from "../shared/types"
3
+ import { KannaAnalyticsReporter, getLaunchAnalyticsProperties } from "./analytics"
4
+
5
+ const originalLogAnalytics = process.env.KANNA_LOG_ANALYTICS
6
+
7
+ function restoreAnalyticsLoggingEnv() {
8
+ if (originalLogAnalytics === undefined) {
9
+ delete process.env.KANNA_LOG_ANALYTICS
10
+ return
11
+ }
12
+ process.env.KANNA_LOG_ANALYTICS = originalLogAnalytics
13
+ }
14
+
15
+ describe("getLaunchAnalyticsProperties", () => {
16
+ test("expands launch flags into app_launch properties", () => {
17
+ expect(getLaunchAnalyticsProperties({
18
+ port: 4000,
19
+ host: "0.0.0.0",
20
+ openBrowser: false,
21
+ share: "quick",
22
+ password: "secret",
23
+ strictPort: true,
24
+ })).toEqual({
25
+ custom_port_enabled: true,
26
+ no_open_enabled: true,
27
+ password_enabled: true,
28
+ strict_port_enabled: true,
29
+ remote_enabled: true,
30
+ host_enabled: false,
31
+ share_quick_enabled: true,
32
+ share_token_enabled: false,
33
+ })
34
+ })
35
+ })
36
+
37
+ describe("KannaAnalyticsReporter", () => {
38
+ test("posts the userId, event name, and shared properties", async () => {
39
+ const originalLog = console.log
40
+ const calls: Array<{ url: string; init?: RequestInit }> = []
41
+ console.log = () => {}
42
+
43
+ try {
44
+ const reporter = new KannaAnalyticsReporter({
45
+ endpoint: "https://kanna.sh/api/t",
46
+ currentVersion: "0.33.9",
47
+ environment: "dev",
48
+ settings: {
49
+ getState: () => ({
50
+ analyticsEnabled: true,
51
+ analyticsUserId: "anon_123",
52
+ cloudflareTunnel: CLOUDFLARE_TUNNEL_DEFAULTS,
53
+ warning: null,
54
+ filePathDisplay: "~/.kanna/data/settings.json",
55
+ }),
56
+ },
57
+ fetchImpl: async (url, init) => {
58
+ calls.push({ url: String(url), init })
59
+ return new Response(JSON.stringify({ ok: true }), { status: 200 })
60
+ },
61
+ })
62
+
63
+ reporter.track("message_sent")
64
+ await (reporter as any).queue
65
+
66
+ expect(calls).toHaveLength(1)
67
+ expect(calls[0]?.url).toBe("https://kanna.sh/api/t")
68
+ expect(calls[0]?.init?.method).toBe("POST")
69
+ expect(calls[0]?.init?.headers).toEqual({
70
+ "content-type": "application/json",
71
+ })
72
+ expect(JSON.parse(String(calls[0]?.init?.body))).toEqual({
73
+ userId: "anon_123",
74
+ environment: "dev",
75
+ event: {
76
+ name: "message_sent",
77
+ properties: {
78
+ current_version: "0.33.9",
79
+ environment: "dev",
80
+ },
81
+ },
82
+ })
83
+ } finally {
84
+ console.log = originalLog
85
+ }
86
+ })
87
+
88
+ test("posts app_launch with launch flags as properties", async () => {
89
+ const calls: Array<{ url: string; init?: RequestInit }> = []
90
+ const reporter = new KannaAnalyticsReporter({
91
+ endpoint: "https://kanna.sh/api/t",
92
+ currentVersion: "0.33.9",
93
+ environment: "prod",
94
+ settings: {
95
+ getState: () => ({
96
+ analyticsEnabled: true,
97
+ analyticsUserId: "anon_123",
98
+ cloudflareTunnel: CLOUDFLARE_TUNNEL_DEFAULTS,
99
+ warning: null,
100
+ filePathDisplay: "~/.kanna/data/settings.json",
101
+ }),
102
+ },
103
+ fetchImpl: async (url, init) => {
104
+ calls.push({ url: String(url), init })
105
+ return new Response(JSON.stringify({ ok: true }), { status: 200 })
106
+ },
107
+ })
108
+
109
+ reporter.trackLaunch({
110
+ port: 4000,
111
+ host: "localhost",
112
+ openBrowser: false,
113
+ share: false,
114
+ password: null,
115
+ strictPort: true,
116
+ })
117
+ await (reporter as any).queue
118
+
119
+ expect(JSON.parse(String(calls[0]?.init?.body))).toEqual({
120
+ userId: "anon_123",
121
+ environment: "prod",
122
+ event: {
123
+ name: "app_launch",
124
+ properties: {
125
+ current_version: "0.33.9",
126
+ environment: "prod",
127
+ custom_port_enabled: true,
128
+ no_open_enabled: true,
129
+ password_enabled: false,
130
+ strict_port_enabled: true,
131
+ remote_enabled: false,
132
+ host_enabled: false,
133
+ share_quick_enabled: false,
134
+ share_token_enabled: false,
135
+ },
136
+ },
137
+ })
138
+ })
139
+
140
+ test("skips requests when analytics is disabled", async () => {
141
+ let called = false
142
+ const reporter = new KannaAnalyticsReporter({
143
+ currentVersion: "0.33.9",
144
+ environment: "prod",
145
+ settings: {
146
+ getState: () => ({
147
+ analyticsEnabled: false,
148
+ analyticsUserId: "anon_123",
149
+ cloudflareTunnel: CLOUDFLARE_TUNNEL_DEFAULTS,
150
+ warning: null,
151
+ filePathDisplay: "~/.kanna/data/settings.json",
152
+ }),
153
+ },
154
+ fetchImpl: async () => {
155
+ called = true
156
+ return new Response(null, { status: 200 })
157
+ },
158
+ })
159
+
160
+ reporter.track("message_sent")
161
+ await (reporter as any).queue
162
+
163
+ expect(called).toBe(false)
164
+ })
165
+
166
+ test("does not warn when analytics request logging is disabled and the request fails", async () => {
167
+ const originalWarn = console.warn
168
+ const warnings: unknown[][] = []
169
+ console.warn = (...args: unknown[]) => {
170
+ warnings.push(args)
171
+ }
172
+ delete process.env.KANNA_LOG_ANALYTICS
173
+
174
+ try {
175
+ const reporter = new KannaAnalyticsReporter({
176
+ endpoint: "https://kanna.sh/api/t",
177
+ currentVersion: "0.33.9",
178
+ environment: "dev",
179
+ settings: {
180
+ getState: () => ({
181
+ analyticsEnabled: true,
182
+ analyticsUserId: "anon_123",
183
+ cloudflareTunnel: CLOUDFLARE_TUNNEL_DEFAULTS,
184
+ warning: null,
185
+ filePathDisplay: "~/.kanna/data/settings.json",
186
+ }),
187
+ },
188
+ fetchImpl: async () => new Response(JSON.stringify({ error: "bad request" }), { status: 400 }),
189
+ })
190
+
191
+ reporter.track("message_sent")
192
+ await (reporter as any).queue
193
+
194
+ expect(warnings).toHaveLength(0)
195
+ } finally {
196
+ console.warn = originalWarn
197
+ restoreAnalyticsLoggingEnv()
198
+ }
199
+ })
200
+
201
+ test("warns when analytics request logging is enabled and the request fails", async () => {
202
+ const originalWarn = console.warn
203
+ const warnings: unknown[][] = []
204
+ console.warn = (...args: unknown[]) => {
205
+ warnings.push(args)
206
+ }
207
+ process.env.KANNA_LOG_ANALYTICS = "1"
208
+
209
+ try {
210
+ const reporter = new KannaAnalyticsReporter({
211
+ endpoint: "https://kanna.sh/api/t",
212
+ currentVersion: "0.33.9",
213
+ environment: "prod",
214
+ settings: {
215
+ getState: () => ({
216
+ analyticsEnabled: true,
217
+ analyticsUserId: "anon_123",
218
+ cloudflareTunnel: CLOUDFLARE_TUNNEL_DEFAULTS,
219
+ warning: null,
220
+ filePathDisplay: "~/.kanna/data/settings.json",
221
+ }),
222
+ },
223
+ fetchImpl: async () => new Response(JSON.stringify({ error: "bad request" }), { status: 400 }),
224
+ })
225
+
226
+ reporter.track("message_sent")
227
+ await (reporter as any).queue
228
+
229
+ expect(warnings).toHaveLength(1)
230
+ expect(warnings[0]?.[0]).toBe("[kanna/analytics] Failed to send analytics event:")
231
+ expect(warnings[0]?.[1]).toBe("message_sent")
232
+ expect(warnings[0]?.[2]).toBeInstanceOf(Error)
233
+ } finally {
234
+ console.warn = originalWarn
235
+ restoreAnalyticsLoggingEnv()
236
+ }
237
+ })
238
+
239
+ test("logs when analytics request logging is enabled and the request succeeds", async () => {
240
+ const originalLog = console.log
241
+ const logs: unknown[][] = []
242
+ console.log = (...args: unknown[]) => {
243
+ logs.push(args)
244
+ }
245
+ process.env.KANNA_LOG_ANALYTICS = "1"
246
+
247
+ try {
248
+ const reporter = new KannaAnalyticsReporter({
249
+ endpoint: "https://kanna.sh/api/t",
250
+ currentVersion: "0.33.9",
251
+ environment: "dev",
252
+ settings: {
253
+ getState: () => ({
254
+ analyticsEnabled: true,
255
+ analyticsUserId: "anon_123",
256
+ cloudflareTunnel: CLOUDFLARE_TUNNEL_DEFAULTS,
257
+ warning: null,
258
+ filePathDisplay: "~/.kanna/data/settings.json",
259
+ }),
260
+ },
261
+ fetchImpl: async () => new Response(JSON.stringify({ ok: true }), { status: 200 }),
262
+ })
263
+
264
+ reporter.track("message_sent")
265
+ await (reporter as any).queue
266
+
267
+ expect(logs).toHaveLength(1)
268
+ expect(logs[0]).toEqual([
269
+ "[kanna/analytics] Sent analytics event:",
270
+ "message_sent",
271
+ 200,
272
+ ])
273
+ } finally {
274
+ console.log = originalLog
275
+ restoreAnalyticsLoggingEnv()
276
+ }
277
+ })
278
+
279
+ test("does not log when analytics request logging is disabled and the request succeeds", async () => {
280
+ const originalLog = console.log
281
+ const logs: unknown[][] = []
282
+ console.log = (...args: unknown[]) => {
283
+ logs.push(args)
284
+ }
285
+ delete process.env.KANNA_LOG_ANALYTICS
286
+
287
+ try {
288
+ const reporter = new KannaAnalyticsReporter({
289
+ endpoint: "https://kanna.sh/api/t",
290
+ currentVersion: "0.33.9",
291
+ environment: "prod",
292
+ settings: {
293
+ getState: () => ({
294
+ analyticsEnabled: true,
295
+ analyticsUserId: "anon_123",
296
+ cloudflareTunnel: CLOUDFLARE_TUNNEL_DEFAULTS,
297
+ warning: null,
298
+ filePathDisplay: "~/.kanna/data/settings.json",
299
+ }),
300
+ },
301
+ fetchImpl: async () => new Response(JSON.stringify({ ok: true }), { status: 200 }),
302
+ })
303
+
304
+ reporter.track("message_sent")
305
+ await (reporter as any).queue
306
+
307
+ expect(logs).toHaveLength(0)
308
+ } finally {
309
+ console.log = originalLog
310
+ restoreAnalyticsLoggingEnv()
311
+ }
312
+ })
313
+ })
@@ -0,0 +1,131 @@
1
+ import { ANALYTICS_ENDPOINT } from "../shared/analytics"
2
+ import { PROD_SERVER_PORT } from "../shared/ports"
3
+ import type { ShareMode } from "../shared/share"
4
+ import { isTokenShareMode } from "../shared/share"
5
+ interface AnalyticsRequestBody {
6
+ userId: string
7
+ environment: AnalyticsEnvironment
8
+ event: {
9
+ name: string
10
+ properties: Record<string, unknown>
11
+ }
12
+ }
13
+
14
+ export interface LaunchAnalyticsOptions {
15
+ port: number
16
+ host: string
17
+ openBrowser: boolean
18
+ share: ShareMode
19
+ password: string | null
20
+ strictPort: boolean
21
+ }
22
+
23
+ type FetchLike = (input: string | URL | Request, init?: RequestInit) => Promise<Response>
24
+ type AnalyticsEnvironment = "dev" | "prod"
25
+
26
+ function isAnalyticsLoggingEnabled() {
27
+ return process.env.KANNA_LOG_ANALYTICS === "1"
28
+ }
29
+
30
+ export interface AnalyticsReporter {
31
+ track: (eventName: string, properties?: Record<string, unknown>) => void
32
+ trackLaunch: (options: LaunchAnalyticsOptions) => void
33
+ }
34
+
35
+ interface AnalyticsSettings {
36
+ getState: () => {
37
+ analyticsEnabled: boolean
38
+ analyticsUserId: string
39
+ }
40
+ }
41
+
42
+ export class KannaAnalyticsReporter implements AnalyticsReporter {
43
+ private readonly settings: AnalyticsSettings
44
+ private readonly endpoint: string
45
+ private readonly fetchImpl: FetchLike
46
+ private readonly currentVersion: string
47
+ private readonly environment: AnalyticsEnvironment
48
+ private queue = Promise.resolve()
49
+
50
+ constructor(args: {
51
+ settings: AnalyticsSettings
52
+ currentVersion: string
53
+ environment: AnalyticsEnvironment
54
+ endpoint?: string
55
+ fetchImpl?: FetchLike
56
+ }) {
57
+ this.settings = args.settings
58
+ this.currentVersion = args.currentVersion
59
+ this.environment = args.environment
60
+ this.endpoint = args.endpoint ?? ANALYTICS_ENDPOINT
61
+ this.fetchImpl = args.fetchImpl ?? fetch
62
+ }
63
+
64
+ track(eventName: string, properties?: Record<string, unknown>) {
65
+ const { analyticsEnabled, analyticsUserId } = this.settings.getState()
66
+ if (!analyticsEnabled || !analyticsUserId) {
67
+ return
68
+ }
69
+
70
+ const body: AnalyticsRequestBody = {
71
+ userId: analyticsUserId,
72
+ environment: this.environment,
73
+ event: {
74
+ name: eventName,
75
+ properties: this.buildEventProperties(properties),
76
+ },
77
+ }
78
+
79
+ this.queue = this.queue
80
+ .then(async () => {
81
+ const response = await this.fetchImpl(this.endpoint, {
82
+ method: "POST",
83
+ headers: {
84
+ "content-type": "application/json",
85
+ },
86
+ body: JSON.stringify(body),
87
+ })
88
+ if (!response.ok) {
89
+ throw new Error(`Analytics request failed with status ${response.status}`)
90
+ }
91
+ if (isAnalyticsLoggingEnabled()) {
92
+ console.log("[kanna/analytics] Sent analytics event:", eventName, response.status)
93
+ }
94
+ })
95
+ .catch((error) => {
96
+ if (isAnalyticsLoggingEnabled()) {
97
+ console.warn("[kanna/analytics] Failed to send analytics event:", eventName, error)
98
+ }
99
+ })
100
+ }
101
+
102
+ trackLaunch(options: LaunchAnalyticsOptions) {
103
+ this.track("app_launch", getLaunchAnalyticsProperties(options))
104
+ }
105
+
106
+ private buildEventProperties(properties?: Record<string, unknown>) {
107
+ return {
108
+ current_version: this.currentVersion,
109
+ environment: this.environment,
110
+ ...(properties ?? {}),
111
+ }
112
+ }
113
+ }
114
+
115
+ export function getLaunchAnalyticsProperties(options: LaunchAnalyticsOptions) {
116
+ return {
117
+ custom_port_enabled: options.port !== PROD_SERVER_PORT,
118
+ no_open_enabled: !options.openBrowser,
119
+ password_enabled: Boolean(options.password),
120
+ strict_port_enabled: options.strictPort,
121
+ remote_enabled: options.host === "0.0.0.0",
122
+ host_enabled: options.host !== "0.0.0.0" && options.host !== "127.0.0.1" && options.host !== "localhost",
123
+ share_quick_enabled: options.share === "quick",
124
+ share_token_enabled: isTokenShareMode(options.share),
125
+ }
126
+ }
127
+
128
+ export const NoopAnalyticsReporter: AnalyticsReporter = {
129
+ track: () => {},
130
+ trackLaunch: () => {},
131
+ }
@@ -0,0 +1,233 @@
1
+ import { afterEach, describe, expect, test } from "bun:test"
2
+ import { mkdtemp, readFile, rm, writeFile } from "node:fs/promises"
3
+ import { tmpdir } from "node:os"
4
+ import path from "node:path"
5
+ import { CLOUDFLARE_TUNNEL_DEFAULTS } from "../shared/types"
6
+ import { AppSettingsManager, readAppSettingsSnapshot } from "./app-settings"
7
+ import type { AppSettingsSnapshot } from "../shared/types"
8
+
9
+ let tempDirs: string[] = []
10
+
11
+ afterEach(async () => {
12
+ await Promise.all(tempDirs.map((dir) => rm(dir, { recursive: true, force: true })))
13
+ tempDirs = []
14
+ })
15
+
16
+ async function createTempFilePath() {
17
+ const dir = await mkdtemp(path.join(tmpdir(), "kanna-settings-"))
18
+ tempDirs.push(dir)
19
+ return path.join(dir, "settings.json")
20
+ }
21
+
22
+ async function writeSettingsFile(content: Record<string, unknown>) {
23
+ const filePath = await createTempFilePath()
24
+ await writeFile(filePath, JSON.stringify(content), "utf8")
25
+ return filePath
26
+ }
27
+
28
+ function expectedSettingsSnapshot(filePath: string, overrides: Partial<AppSettingsSnapshot> = {}): AppSettingsSnapshot {
29
+ return {
30
+ analyticsEnabled: true,
31
+ browserSettingsMigrated: false,
32
+ theme: "system",
33
+ chatSoundPreference: "always",
34
+ chatSoundId: "funk",
35
+ terminal: {
36
+ scrollbackLines: 1_000,
37
+ minColumnWidth: 450,
38
+ },
39
+ editor: {
40
+ preset: "cursor",
41
+ commandTemplate: "cursor {path}",
42
+ },
43
+ defaultProvider: "last_used",
44
+ providerDefaults: {
45
+ claude: {
46
+ model: "claude-opus-4-7",
47
+ modelOptions: {
48
+ reasoningEffort: "high",
49
+ contextWindow: "200k",
50
+ },
51
+ planMode: false,
52
+ },
53
+ codex: {
54
+ model: "gpt-5.5",
55
+ modelOptions: {
56
+ reasoningEffort: "high",
57
+ fastMode: false,
58
+ },
59
+ planMode: false,
60
+ },
61
+ },
62
+ warning: null,
63
+ filePathDisplay: filePath,
64
+ cloudflareTunnel: CLOUDFLARE_TUNNEL_DEFAULTS,
65
+ ...overrides,
66
+ }
67
+ }
68
+
69
+ describe("readAppSettingsSnapshot", () => {
70
+ test("returns defaults when the file does not exist", async () => {
71
+ const filePath = await createTempFilePath()
72
+ const snapshot = await readAppSettingsSnapshot(filePath)
73
+
74
+ expect(snapshot).toEqual(expectedSettingsSnapshot(filePath))
75
+ })
76
+
77
+ test("returns a warning when the file contains invalid json", async () => {
78
+ const filePath = await createTempFilePath()
79
+ await writeFile(filePath, "{not-json", "utf8")
80
+
81
+ const snapshot = await readAppSettingsSnapshot(filePath)
82
+ expect(snapshot.analyticsEnabled).toBe(true)
83
+ expect(snapshot.warning).toContain("invalid JSON")
84
+ })
85
+ })
86
+
87
+ describe("AppSettingsManager", () => {
88
+ test("creates a settings file with analytics enabled and a stable anonymous id", async () => {
89
+ const filePath = await createTempFilePath()
90
+ const manager = new AppSettingsManager(filePath)
91
+
92
+ await manager.initialize()
93
+
94
+ const payload = JSON.parse(await readFile(filePath, "utf8")) as {
95
+ analyticsEnabled: boolean
96
+ analyticsUserId: string
97
+ }
98
+ expect(payload.analyticsEnabled).toBe(true)
99
+ expect(payload.analyticsUserId).toMatch(/^anon_/)
100
+ expect(manager.getSnapshot()).toEqual(expectedSettingsSnapshot(filePath))
101
+
102
+ manager.dispose()
103
+ })
104
+
105
+ test("writes analyticsEnabled without replacing the stored user id", async () => {
106
+ const filePath = await createTempFilePath()
107
+ const manager = new AppSettingsManager(filePath)
108
+
109
+ await manager.initialize()
110
+ const initialPayload = JSON.parse(await readFile(filePath, "utf8")) as {
111
+ analyticsEnabled: boolean
112
+ analyticsUserId: string
113
+ }
114
+
115
+ const snapshot = await manager.write({ analyticsEnabled: false })
116
+ const nextPayload = JSON.parse(await readFile(filePath, "utf8")) as {
117
+ analyticsEnabled: boolean
118
+ analyticsUserId: string
119
+ }
120
+
121
+ expect(snapshot).toEqual(expectedSettingsSnapshot(filePath, { analyticsEnabled: false }))
122
+ expect(nextPayload.analyticsEnabled).toBe(false)
123
+ expect(nextPayload.analyticsUserId).toBe(initialPayload.analyticsUserId)
124
+
125
+ manager.dispose()
126
+ })
127
+
128
+ test("patches expanded settings without replacing the stored user id", async () => {
129
+ const filePath = await createTempFilePath()
130
+ const manager = new AppSettingsManager(filePath)
131
+
132
+ await manager.initialize()
133
+ const initialPayload = JSON.parse(await readFile(filePath, "utf8")) as {
134
+ analyticsUserId: string
135
+ }
136
+
137
+ const snapshot = await manager.writePatch({
138
+ theme: "dark",
139
+ chatSoundId: "glass",
140
+ terminal: { scrollbackLines: 2_500 },
141
+ editor: { preset: "vscode" },
142
+ providerDefaults: {
143
+ codex: {
144
+ modelOptions: { reasoningEffort: "high", fastMode: true },
145
+ },
146
+ },
147
+ })
148
+ const nextPayload = JSON.parse(await readFile(filePath, "utf8")) as {
149
+ analyticsUserId: string
150
+ theme: string
151
+ chatSoundId: string
152
+ terminal: { scrollbackLines: number; minColumnWidth: number }
153
+ editor: { preset: string; commandTemplate: string }
154
+ providerDefaults: { codex: { modelOptions: { fastMode: boolean } } }
155
+ }
156
+
157
+ expect(snapshot.theme).toBe("dark")
158
+ expect(snapshot.chatSoundId).toBe("glass")
159
+ expect(snapshot.terminal.scrollbackLines).toBe(2_500)
160
+ expect(snapshot.terminal.minColumnWidth).toBe(450)
161
+ expect(snapshot.editor.preset).toBe("vscode")
162
+ expect(snapshot.editor.commandTemplate).toBe("cursor {path}")
163
+ expect(snapshot.providerDefaults.codex.modelOptions.fastMode).toBe(true)
164
+ expect(nextPayload.analyticsUserId).toBe(initialPayload.analyticsUserId)
165
+ expect(nextPayload.theme).toBe("dark")
166
+ expect(nextPayload.chatSoundId).toBe("glass")
167
+
168
+ manager.dispose()
169
+ })
170
+ })
171
+
172
+ describe("cloudflareTunnel normalization", () => {
173
+ test("normalizes missing cloudflareTunnel block to defaults", async () => {
174
+ const filePath = await writeSettingsFile({ analyticsEnabled: true })
175
+ const snapshot = await readAppSettingsSnapshot(filePath)
176
+ expect(snapshot.cloudflareTunnel).toEqual({
177
+ enabled: false,
178
+ cloudflaredPath: "cloudflared",
179
+ mode: "always-ask",
180
+ })
181
+ })
182
+
183
+ test("preserves valid cloudflareTunnel settings", async () => {
184
+ const filePath = await writeSettingsFile({
185
+ cloudflareTunnel: { enabled: true, cloudflaredPath: "/usr/local/bin/cloudflared", mode: "auto-expose" },
186
+ })
187
+ const snapshot = await readAppSettingsSnapshot(filePath)
188
+ expect(snapshot.cloudflareTunnel).toEqual({
189
+ enabled: true,
190
+ cloudflaredPath: "/usr/local/bin/cloudflared",
191
+ mode: "auto-expose",
192
+ })
193
+ })
194
+
195
+ test("rejects invalid mode and resets to default with warning", async () => {
196
+ const filePath = await writeSettingsFile({
197
+ cloudflareTunnel: { enabled: true, cloudflaredPath: "cloudflared", mode: "garbage" },
198
+ })
199
+ const snapshot = await readAppSettingsSnapshot(filePath)
200
+ expect(snapshot.cloudflareTunnel.mode).toBe("always-ask")
201
+ expect(snapshot.warning).toContain("cloudflareTunnel.mode")
202
+ })
203
+
204
+ test("setCloudflareTunnel persists patch to disk and round-trips through readAppSettingsSnapshot", async () => {
205
+ const filePath = await writeSettingsFile({ analyticsEnabled: true })
206
+ const manager = new AppSettingsManager(filePath)
207
+ await manager.initialize()
208
+ await manager.setCloudflareTunnel({ enabled: true, mode: "auto-expose" })
209
+ const reloaded = await readAppSettingsSnapshot(filePath)
210
+ expect(reloaded.cloudflareTunnel).toEqual({
211
+ enabled: true,
212
+ cloudflaredPath: "cloudflared",
213
+ mode: "auto-expose",
214
+ })
215
+ })
216
+
217
+ test("write() preserves cloudflareTunnel across analytics-only updates", async () => {
218
+ const filePath = await writeSettingsFile({
219
+ analyticsEnabled: true,
220
+ cloudflareTunnel: { enabled: true, cloudflaredPath: "/opt/cloudflared", mode: "auto-expose" },
221
+ })
222
+ const manager = new AppSettingsManager(filePath)
223
+ await manager.initialize()
224
+ // Simulate analytics toggle — must NOT erase tunnel block
225
+ await manager.write({ analyticsEnabled: false })
226
+ const reloaded = await readAppSettingsSnapshot(filePath)
227
+ expect(reloaded.cloudflareTunnel).toEqual({
228
+ enabled: true,
229
+ cloudflaredPath: "/opt/cloudflared",
230
+ mode: "auto-expose",
231
+ })
232
+ })
233
+ })