@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,241 @@
1
+ import { compareVersions } from "./cli-runtime"
2
+ import type { UpdateInstallAttemptResult } from "./cli-runtime"
3
+ import { PACKAGE_NAME } from "../shared/branding"
4
+ import type { UpdateInstallErrorCode } from "../shared/types"
5
+
6
+ export interface UpdateChecker {
7
+ check(): Promise<{ latestVersion: string; updateAvailable: boolean }>
8
+ }
9
+
10
+ // Implemented by SupervisorExitReloader (Task 2) and Pm2Reloader (Task 8).
11
+ export interface UpdateReloader {
12
+ reload(): Promise<void>
13
+ }
14
+
15
+ export interface NpmCheckerDeps {
16
+ currentVersion: string
17
+ fetchLatestVersion: (packageName: string) => Promise<string>
18
+ }
19
+
20
+ export class UpdateInstallError extends Error {
21
+ constructor(
22
+ message: string,
23
+ public readonly errorCode: UpdateInstallErrorCode | null,
24
+ public readonly userTitle: string | null,
25
+ ) {
26
+ super(message)
27
+ this.name = "UpdateInstallError"
28
+ }
29
+ }
30
+
31
+ export interface SupervisorExitReloaderDeps {
32
+ targetVersion: () => string | null
33
+ installVersion: (packageName: string, version: string) => UpdateInstallAttemptResult
34
+ }
35
+
36
+ export class SupervisorExitReloader implements UpdateReloader {
37
+ constructor(private deps: SupervisorExitReloaderDeps) {}
38
+
39
+ async reload() {
40
+ const version = this.deps.targetVersion()
41
+ if (!version) {
42
+ throw new UpdateInstallError(
43
+ "Unable to determine target version.",
44
+ "install_failed",
45
+ "Update failed",
46
+ )
47
+ }
48
+ const result = this.deps.installVersion(PACKAGE_NAME, version)
49
+ if (!result.ok) {
50
+ throw new UpdateInstallError(
51
+ result.userMessage ?? "Unable to install the latest version.",
52
+ result.errorCode,
53
+ result.userTitle,
54
+ )
55
+ }
56
+ }
57
+ }
58
+
59
+ export class NpmChecker implements UpdateChecker {
60
+ constructor(private deps: NpmCheckerDeps) {}
61
+
62
+ async check() {
63
+ const latestVersion = await this.deps.fetchLatestVersion(PACKAGE_NAME)
64
+ const updateAvailable = compareVersions(this.deps.currentVersion, latestVersion) < 0
65
+ return { latestVersion, updateAvailable }
66
+ }
67
+ }
68
+
69
+ export interface GitCheckerDeps {
70
+ repoDir: string
71
+ branch: string
72
+ runGit: (args: string[]) => Promise<string>
73
+ }
74
+
75
+ export class GitChecker implements UpdateChecker {
76
+ constructor(private deps: GitCheckerDeps) {}
77
+
78
+ async check() {
79
+ await this.deps.runGit(["fetch", "origin", this.deps.branch])
80
+ const headRaw = await this.deps.runGit(["rev-parse", "HEAD"])
81
+ const upstreamRaw = await this.deps.runGit(["rev-parse", `origin/${this.deps.branch}`])
82
+ const head = headRaw.trim()
83
+ const upstream = upstreamRaw.trim()
84
+ return {
85
+ latestVersion: upstream.slice(0, 7),
86
+ updateAvailable: head !== upstream,
87
+ }
88
+ }
89
+ }
90
+
91
+ export interface Pm2ReloaderDeps {
92
+ repoDir: string
93
+ processName: string
94
+ runCommand: (command: string, args: string[]) => Promise<void>
95
+ lockfileChanged: () => Promise<boolean>
96
+ triggerPm2Reload: (processName: string) => Promise<void>
97
+ }
98
+
99
+ export class Pm2Reloader implements UpdateReloader {
100
+ constructor(private deps: Pm2ReloaderDeps) {}
101
+
102
+ async reload() {
103
+ await this.step("git pull", ["git", "pull", "--ff-only"])
104
+ if (await this.deps.lockfileChanged()) {
105
+ await this.step("bun install", ["bun", "install"])
106
+ }
107
+ await this.step("bun run build", ["bun", "run", "build"])
108
+ try {
109
+ await this.deps.triggerPm2Reload(this.deps.processName)
110
+ } catch (error) {
111
+ const message = error instanceof Error ? error.message : String(error)
112
+ throw new UpdateInstallError(
113
+ `pm2 reload failed: ${message}`,
114
+ "install_failed",
115
+ "Update failed",
116
+ )
117
+ }
118
+ }
119
+
120
+ private async step(label: string, argv: string[]) {
121
+ const [command, ...args] = argv
122
+ try {
123
+ await this.deps.runCommand(command, args)
124
+ } catch (error) {
125
+ const message = error instanceof Error ? error.message : String(error)
126
+ throw new UpdateInstallError(
127
+ `${label} failed: ${message}`,
128
+ "install_failed",
129
+ "Update failed",
130
+ )
131
+ }
132
+ }
133
+ }
134
+
135
+ export interface CreateUpdateStrategyDeps {
136
+ reloaderEnv: string | undefined
137
+ currentVersion: string
138
+ fetchLatestVersion: (packageName: string) => Promise<string>
139
+ installVersion: (packageName: string, version: string) => UpdateInstallAttemptResult
140
+ latestVersionHint: () => string | null
141
+ // Required for pm2 branch (KANNA_REPO_DIR).
142
+ repoDir?: string
143
+ // Optional pm2 process name override (KANNA_PM2_PROCESS_NAME). Defaults to "kanna".
144
+ pm2ProcessName?: string
145
+ }
146
+
147
+ export interface UpdateStrategy {
148
+ checker: UpdateChecker
149
+ reloader: UpdateReloader
150
+ }
151
+
152
+ export function createUpdateStrategy(deps: CreateUpdateStrategyDeps): UpdateStrategy {
153
+ const mode = deps.reloaderEnv ?? "supervisor"
154
+ if (mode === "supervisor") {
155
+ return {
156
+ checker: new NpmChecker({
157
+ currentVersion: deps.currentVersion,
158
+ fetchLatestVersion: deps.fetchLatestVersion,
159
+ }),
160
+ reloader: new SupervisorExitReloader({
161
+ targetVersion: deps.latestVersionHint,
162
+ installVersion: deps.installVersion,
163
+ }),
164
+ }
165
+ }
166
+ if (mode === "pm2") {
167
+ if (!deps.repoDir) {
168
+ throw new Error("KANNA_RELOADER=pm2 requires KANNA_REPO_DIR to be set")
169
+ }
170
+ const repoDir = deps.repoDir
171
+ return {
172
+ checker: new GitChecker({
173
+ repoDir,
174
+ branch: "main",
175
+ runGit: (args) => runCommandCapture("git", args, repoDir),
176
+ }),
177
+ reloader: new Pm2Reloader({
178
+ repoDir,
179
+ processName: deps.pm2ProcessName ?? "kanna",
180
+ runCommand: (command, args) => runCommandThrow(command, args, repoDir),
181
+ lockfileChanged: () => detectLockfileChange(repoDir),
182
+ triggerPm2Reload,
183
+ }),
184
+ }
185
+ }
186
+ throw new Error(`Unknown KANNA_RELOADER value "${mode}". Supported values: supervisor, pm2`)
187
+ }
188
+
189
+ async function runCommandCapture(command: string, args: string[], cwd: string): Promise<string> {
190
+ const proc = Bun.spawn({ cmd: [command, ...args], cwd, stdout: "pipe", stderr: "pipe" })
191
+ const [stdout, stderr, exitCode] = await Promise.all([
192
+ new Response(proc.stdout).text(),
193
+ new Response(proc.stderr).text(),
194
+ proc.exited,
195
+ ])
196
+ if (exitCode !== 0) {
197
+ const tail = stderr.trim().slice(-500)
198
+ throw new Error(tail || `${command} exited with code ${exitCode}`)
199
+ }
200
+ return stdout
201
+ }
202
+
203
+ async function runCommandThrow(command: string, args: string[], cwd: string): Promise<void> {
204
+ await runCommandCapture(command, args, cwd)
205
+ }
206
+
207
+ async function detectLockfileChange(repoDir: string): Promise<boolean> {
208
+ try {
209
+ const output = await runCommandCapture(
210
+ "git",
211
+ ["diff", "--name-only", "HEAD@{1}", "HEAD", "--", "bun.lock", "package.json"],
212
+ repoDir,
213
+ )
214
+ return output.trim().length > 0
215
+ } catch {
216
+ // No prior HEAD@{1} (fresh clone) or other git error — install to be safe
217
+ return true
218
+ }
219
+ }
220
+
221
+ async function triggerPm2Reload(processName: string): Promise<void> {
222
+ const pm2Module = await import("pm2")
223
+ const pm2 = pm2Module.default ?? pm2Module
224
+ await new Promise<void>((resolve, reject) => {
225
+ pm2.connect((connectErr) => {
226
+ if (connectErr) {
227
+ pm2.disconnect()
228
+ reject(connectErr instanceof Error ? connectErr : new Error(String(connectErr)))
229
+ return
230
+ }
231
+ pm2.reload(processName, (reloadErr) => {
232
+ pm2.disconnect()
233
+ if (reloadErr) {
234
+ reject(reloadErr instanceof Error ? reloadErr : new Error(String(reloadErr)))
235
+ return
236
+ }
237
+ resolve()
238
+ })
239
+ })
240
+ })
241
+ }
@@ -0,0 +1,292 @@
1
+ import { afterEach, describe, expect, test } from "bun:test"
2
+ import { mkdtemp, rm } from "node:fs/promises"
3
+ import { tmpdir } from "node:os"
4
+ import path from "node:path"
5
+ import { deleteProjectUpload, inferAttachmentContentType, persistProjectUpload } from "./uploads"
6
+ import { getProjectUploadDir } from "./paths"
7
+ import { persistUploadedFiles, startKannaServer } from "./server"
8
+
9
+ const PNG_BASE64 =
10
+ "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/x8AAwMCAO+yF9sAAAAASUVORK5CYII="
11
+
12
+ const tempDirs: string[] = []
13
+
14
+ afterEach(async () => {
15
+ await Promise.all(tempDirs.splice(0).map((dir) => rm(dir, { recursive: true, force: true })))
16
+ })
17
+
18
+ async function startIsolatedServer(options: { port: number; strictPort?: boolean }) {
19
+ const dataDir = await mkdtemp(path.join(tmpdir(), "kanna-server-data-"))
20
+ tempDirs.push(dataDir)
21
+ return startKannaServer({
22
+ dataDir,
23
+ port: options.port,
24
+ strictPort: options.strictPort ?? true,
25
+ })
26
+ }
27
+
28
+ describe("uploads", () => {
29
+ test("stores uploads in .kanna/uploads and keeps duplicate filenames", async () => {
30
+ const projectDir = await mkdtemp(path.join(tmpdir(), "kanna-upload-test-"))
31
+ tempDirs.push(projectDir)
32
+
33
+ const first = await persistProjectUpload({
34
+ projectId: "project-1",
35
+ localPath: projectDir,
36
+ fileName: "notes.txt",
37
+ bytes: new TextEncoder().encode("hello"),
38
+ fallbackMimeType: "text/plain",
39
+ })
40
+ const second = await persistProjectUpload({
41
+ projectId: "project-1",
42
+ localPath: projectDir,
43
+ fileName: "notes.txt",
44
+ bytes: new TextEncoder().encode("world"),
45
+ fallbackMimeType: "text/plain",
46
+ })
47
+
48
+ expect(first.absolutePath).toBe(path.join(projectDir, ".kanna/uploads/notes.txt"))
49
+ expect(first.relativePath).toBe("./.kanna/uploads/notes.txt")
50
+ expect(first.contentUrl).toBe("/api/projects/project-1/uploads/notes.txt/content")
51
+ expect(second.absolutePath).toBe(path.join(projectDir, ".kanna/uploads/notes-1.txt"))
52
+ expect(second.relativePath).toBe("./.kanna/uploads/notes-1.txt")
53
+ expect(second.contentUrl).toBe("/api/projects/project-1/uploads/notes-1.txt/content")
54
+ expect(await Bun.file(path.join(projectDir, ".kanna/uploads/notes.txt")).text()).toBe("hello")
55
+ expect(await Bun.file(path.join(projectDir, ".kanna/uploads/notes-1.txt")).text()).toBe("world")
56
+ })
57
+
58
+ test("stores concurrent same-name uploads without overwriting existing content", async () => {
59
+ const projectDir = await mkdtemp(path.join(tmpdir(), "kanna-upload-concurrent-"))
60
+ tempDirs.push(projectDir)
61
+
62
+ const attachments = await Promise.all([
63
+ persistProjectUpload({
64
+ projectId: "project-1",
65
+ localPath: projectDir,
66
+ fileName: "notes.txt",
67
+ bytes: new TextEncoder().encode("first"),
68
+ fallbackMimeType: "text/plain",
69
+ }),
70
+ persistProjectUpload({
71
+ projectId: "project-1",
72
+ localPath: projectDir,
73
+ fileName: "notes.txt",
74
+ bytes: new TextEncoder().encode("second"),
75
+ fallbackMimeType: "text/plain",
76
+ }),
77
+ persistProjectUpload({
78
+ projectId: "project-1",
79
+ localPath: projectDir,
80
+ fileName: "notes.txt",
81
+ bytes: new TextEncoder().encode("third"),
82
+ fallbackMimeType: "text/plain",
83
+ }),
84
+ ])
85
+
86
+ const storedNames = attachments.map((attachment) => path.basename(attachment.absolutePath)).sort()
87
+ expect(storedNames).toEqual(["notes-1.txt", "notes-2.txt", "notes.txt"])
88
+
89
+ const contents = await Promise.all(attachments.map((attachment) => Bun.file(attachment.absolutePath).text()))
90
+ expect(new Set(contents)).toEqual(new Set(["first", "second", "third"]))
91
+ })
92
+
93
+ test("detects image uploads and returns absolute plus project-relative paths", async () => {
94
+ const projectDir = await mkdtemp(path.join(tmpdir(), "kanna-upload-image-"))
95
+ tempDirs.push(projectDir)
96
+
97
+ const attachment = await persistProjectUpload({
98
+ projectId: "project-2",
99
+ localPath: projectDir,
100
+ fileName: "pixel.png",
101
+ bytes: Buffer.from(PNG_BASE64, "base64"),
102
+ })
103
+
104
+ expect(attachment.kind).toBe("image")
105
+ expect(attachment.mimeType).toBe("image/png")
106
+ expect(getProjectUploadDir(projectDir)).toBe(path.join(projectDir, ".kanna", "uploads"))
107
+ expect(attachment.absolutePath).toBe(path.join(projectDir, ".kanna/uploads/pixel.png"))
108
+ expect(attachment.relativePath).toBe("./.kanna/uploads/pixel.png")
109
+ expect(attachment.contentUrl).toBe("/api/projects/project-2/uploads/pixel.png/content")
110
+ })
111
+
112
+ test("serves uploaded attachment content through the project content URL", async () => {
113
+ const projectDir = await mkdtemp(path.join(tmpdir(), "kanna-project-"))
114
+ tempDirs.push(projectDir)
115
+
116
+ const server = await startIsolatedServer({ port: 4310 })
117
+
118
+ try {
119
+ const project = await server.store.openProject(projectDir, "Project")
120
+ const attachment = await persistProjectUpload({
121
+ projectId: project.id,
122
+ localPath: projectDir,
123
+ fileName: "hello.txt",
124
+ bytes: new TextEncoder().encode("hello from upload"),
125
+ fallbackMimeType: "text/plain",
126
+ })
127
+
128
+ const response = await fetch(`http://localhost:${server.port}${attachment.contentUrl}`)
129
+ expect(response.status).toBe(200)
130
+ expect(response.headers.get("content-type")).toBe("text/plain; charset=utf-8")
131
+ expect(await response.text()).toBe("hello from upload")
132
+ } finally {
133
+ await server.stop()
134
+ }
135
+ })
136
+
137
+ test("serves TypeScript uploads as text content", async () => {
138
+ const projectDir = await mkdtemp(path.join(tmpdir(), "kanna-project-typescript-"))
139
+ tempDirs.push(projectDir)
140
+
141
+ const server = await startIsolatedServer({ port: 4314 })
142
+
143
+ try {
144
+ const project = await server.store.openProject(projectDir, "Project")
145
+ const attachment = await persistProjectUpload({
146
+ projectId: project.id,
147
+ localPath: projectDir,
148
+ fileName: "main.ts",
149
+ bytes: new TextEncoder().encode("export const value = 1\n"),
150
+ fallbackMimeType: "video/mp2t",
151
+ })
152
+
153
+ const response = await fetch(`http://localhost:${server.port}${attachment.contentUrl}`)
154
+ expect(response.status).toBe(200)
155
+ expect(response.headers.get("content-type")).toBe("text/plain; charset=utf-8")
156
+ expect(await response.text()).toContain("export const value = 1")
157
+ } finally {
158
+ await server.stop()
159
+ }
160
+ })
161
+
162
+ test("rejects non-GET requests for attachment content", async () => {
163
+ const projectDir = await mkdtemp(path.join(tmpdir(), "kanna-project-content-method-"))
164
+ tempDirs.push(projectDir)
165
+
166
+ const server = await startIsolatedServer({ port: 4312 })
167
+
168
+ try {
169
+ const project = await server.store.openProject(projectDir, "Project")
170
+ const attachment = await persistProjectUpload({
171
+ projectId: project.id,
172
+ localPath: projectDir,
173
+ fileName: "hello.txt",
174
+ bytes: new TextEncoder().encode("hello from upload"),
175
+ fallbackMimeType: "text/plain",
176
+ })
177
+
178
+ const response = await fetch(`http://localhost:${server.port}${attachment.contentUrl}`, { method: "POST" })
179
+ expect(response.status).toBe(405)
180
+ expect(response.headers.get("allow")).toBe("GET")
181
+ } finally {
182
+ await server.stop()
183
+ }
184
+ })
185
+
186
+ test("rejects oversized uploads before reading them into memory", async () => {
187
+ const projectDir = await mkdtemp(path.join(tmpdir(), "kanna-project-oversize-"))
188
+ tempDirs.push(projectDir)
189
+
190
+ const server = await startIsolatedServer({ port: 4313 })
191
+
192
+ try {
193
+ const project = await server.store.openProject(projectDir, "Project")
194
+ const formData = new FormData()
195
+ formData.append("files", new File([new Uint8Array(100 * 1024 * 1024 + 1)], "big.bin", { type: "application/octet-stream" }))
196
+
197
+ const response = await fetch(`http://localhost:${server.port}/api/projects/${project.id}/uploads`, {
198
+ method: "POST",
199
+ body: formData,
200
+ })
201
+
202
+ expect(response.status).toBe(413)
203
+ expect(await response.json()).toEqual({
204
+ error: "File \"big.bin\" exceeds the 100 MB limit.",
205
+ })
206
+ } finally {
207
+ await server.stop()
208
+ }
209
+ })
210
+
211
+ test("cleans up already-persisted files when a later file in the batch fails", async () => {
212
+ const projectDir = await mkdtemp(path.join(tmpdir(), "kanna-project-cleanup-"))
213
+ tempDirs.push(projectDir)
214
+
215
+ const files = [
216
+ new File(["first"], "first.txt", { type: "text/plain" }),
217
+ new File(["second"], "second.txt", { type: "text/plain" }),
218
+ ]
219
+
220
+ await expect(
221
+ persistUploadedFiles({
222
+ projectId: "project-4",
223
+ localPath: projectDir,
224
+ files,
225
+ persistUpload: async (args) => {
226
+ if (args.fileName === "second.txt") {
227
+ throw new Error("disk full")
228
+ }
229
+
230
+ return persistProjectUpload(args)
231
+ },
232
+ })
233
+ ).rejects.toThrow("disk full")
234
+
235
+ expect(await Bun.file(path.join(projectDir, ".kanna/uploads/first.txt")).exists()).toBe(false)
236
+ expect(await Bun.file(path.join(projectDir, ".kanna/uploads/second.txt")).exists()).toBe(false)
237
+ })
238
+
239
+ test("deletes uploaded attachments from the project uploads directory", async () => {
240
+ const projectDir = await mkdtemp(path.join(tmpdir(), "kanna-upload-delete-"))
241
+ tempDirs.push(projectDir)
242
+
243
+ const attachment = await persistProjectUpload({
244
+ projectId: "project-3",
245
+ localPath: projectDir,
246
+ fileName: "delete-me.txt",
247
+ bytes: new TextEncoder().encode("bye"),
248
+ fallbackMimeType: "text/plain",
249
+ })
250
+
251
+ const deleted = await deleteProjectUpload({
252
+ localPath: projectDir,
253
+ storedName: "delete-me.txt",
254
+ })
255
+
256
+ expect(deleted).toBe(true)
257
+ expect(await Bun.file(attachment.absolutePath).exists()).toBe(false)
258
+ })
259
+
260
+ test("deletes uploaded attachment content through the project delete URL", async () => {
261
+ const projectDir = await mkdtemp(path.join(tmpdir(), "kanna-project-delete-"))
262
+ tempDirs.push(projectDir)
263
+
264
+ const server = await startIsolatedServer({ port: 4311 })
265
+
266
+ try {
267
+ const project = await server.store.openProject(projectDir, "Project")
268
+ const attachment = await persistProjectUpload({
269
+ projectId: project.id,
270
+ localPath: projectDir,
271
+ fileName: "bye.txt",
272
+ bytes: new TextEncoder().encode("delete over http"),
273
+ fallbackMimeType: "text/plain",
274
+ })
275
+
276
+ const deleteUrl = `http://localhost:${server.port}${attachment.contentUrl.replace(/\/content$/, "")}`
277
+ const response = await fetch(deleteUrl, { method: "DELETE" })
278
+ expect(response.status).toBe(200)
279
+ expect(await response.json()).toEqual({ ok: true })
280
+ expect(await Bun.file(attachment.absolutePath).exists()).toBe(false)
281
+ } finally {
282
+ await server.stop()
283
+ }
284
+ })
285
+
286
+ test("infers text-friendly content types for previewable source files", () => {
287
+ expect(inferAttachmentContentType("notes.txt")).toBe("text/plain; charset=utf-8")
288
+ expect(inferAttachmentContentType("README.md")).toBe("text/markdown; charset=utf-8")
289
+ expect(inferAttachmentContentType("main.ts", "video/mp2t")).toBe("text/plain; charset=utf-8")
290
+ expect(inferAttachmentContentType("archive.zip", "application/zip")).toBe("application/zip")
291
+ })
292
+ })
@@ -0,0 +1,131 @@
1
+ import { randomUUID } from "node:crypto"
2
+ import { mkdir, open, rm } from "node:fs/promises"
3
+ import path from "node:path"
4
+ import { fileTypeFromBuffer } from "file-type"
5
+ import type { ChatAttachment } from "../shared/types"
6
+ import { getProjectUploadDir } from "./paths"
7
+
8
+ const DEFAULT_BINARY_MIME_TYPE = "application/octet-stream"
9
+ const IMAGE_MIME_PREFIX = "image/"
10
+ const TEXT_PLAIN_CONTENT_TYPE = "text/plain; charset=utf-8"
11
+
12
+ const TEXT_CONTENT_TYPE_BY_EXTENSION = new Map<string, string>([
13
+ [".csv", "text/csv; charset=utf-8"],
14
+ [".json", "application/json; charset=utf-8"],
15
+ [".jsonc", TEXT_PLAIN_CONTENT_TYPE],
16
+ [".md", "text/markdown; charset=utf-8"],
17
+ [".tsv", "text/tab-separated-values; charset=utf-8"],
18
+ ])
19
+
20
+ const TEXT_LIKE_EXTENSIONS = new Set([
21
+ ".c", ".cc", ".cfg", ".conf", ".cpp", ".cs", ".css", ".env", ".go", ".graphql", ".h", ".hpp", ".html",
22
+ ".ini", ".java", ".js", ".jsx", ".kt", ".lua", ".mjs", ".php", ".pl", ".properties", ".py", ".rb", ".rs",
23
+ ".scss", ".sh", ".sql", ".swift", ".toml", ".ts", ".tsx", ".txt", ".vue", ".xml", ".yaml", ".yml", ".zsh",
24
+ ])
25
+
26
+ function sanitizeFileName(fileName: string) {
27
+ const baseName = path.basename(fileName).trim()
28
+ const cleaned = baseName.replace(/[^\w.-]+/g, "-").replace(/^-+|-+$/g, "")
29
+ return cleaned || "upload"
30
+ }
31
+
32
+ function getUploadCandidateNames(originalName: string) {
33
+ const sanitizedName = sanitizeFileName(originalName)
34
+ const parsed = path.parse(sanitizedName)
35
+ const extension = parsed.ext
36
+ const name = parsed.name || "upload"
37
+
38
+ return {
39
+ first: sanitizedName,
40
+ withCounter(counter: number) {
41
+ return `${name}-${counter}${extension}`
42
+ },
43
+ }
44
+ }
45
+
46
+ export async function persistProjectUpload(args: {
47
+ projectId: string
48
+ localPath: string
49
+ fileName: string
50
+ bytes: Uint8Array
51
+ fallbackMimeType?: string
52
+ }): Promise<ChatAttachment> {
53
+ const uploadDir = getProjectUploadDir(args.localPath)
54
+ await mkdir(uploadDir, { recursive: true })
55
+
56
+ const detectedType = await fileTypeFromBuffer(args.bytes)
57
+ const mimeType = detectedType?.mime ?? args.fallbackMimeType ?? DEFAULT_BINARY_MIME_TYPE
58
+ const candidates = getUploadCandidateNames(args.fileName)
59
+
60
+ let storedName = candidates.first
61
+ let absolutePath = path.join(uploadDir, storedName)
62
+ let counter = 1
63
+
64
+ while (true) {
65
+ try {
66
+ const handle = await open(absolutePath, "wx")
67
+ try {
68
+ await handle.writeFile(args.bytes)
69
+ } finally {
70
+ await handle.close()
71
+ }
72
+ break
73
+ } catch (error) {
74
+ const code = error instanceof Error && "code" in error ? (error as NodeJS.ErrnoException).code : undefined
75
+ if (code !== "EEXIST") {
76
+ throw error
77
+ }
78
+
79
+ storedName = candidates.withCounter(counter)
80
+ absolutePath = path.join(uploadDir, storedName)
81
+ counter += 1
82
+ }
83
+ }
84
+
85
+ return {
86
+ id: randomUUID(),
87
+ kind: mimeType.startsWith(IMAGE_MIME_PREFIX) ? "image" : "file",
88
+ displayName: args.fileName,
89
+ absolutePath,
90
+ relativePath: `./.kanna/uploads/${storedName}`,
91
+ contentUrl: `/api/projects/${args.projectId}/uploads/${encodeURIComponent(storedName)}/content`,
92
+ mimeType,
93
+ size: args.bytes.byteLength,
94
+ }
95
+ }
96
+
97
+ export function inferAttachmentContentType(fileName: string, fallbackType?: string): string {
98
+ const extension = path.extname(fileName).toLowerCase()
99
+ const mappedType = TEXT_CONTENT_TYPE_BY_EXTENSION.get(extension)
100
+ if (mappedType) {
101
+ return mappedType
102
+ }
103
+
104
+ if (TEXT_LIKE_EXTENSIONS.has(extension)) {
105
+ return TEXT_PLAIN_CONTENT_TYPE
106
+ }
107
+
108
+ return fallbackType || DEFAULT_BINARY_MIME_TYPE
109
+ }
110
+
111
+ export function inferProjectFileContentType(fileName: string, fallbackType?: string): string {
112
+ return inferAttachmentContentType(fileName, fallbackType)
113
+ }
114
+
115
+ export async function deleteProjectUpload(args: {
116
+ localPath: string
117
+ storedName: string
118
+ }): Promise<boolean> {
119
+ const storedName = args.storedName
120
+ if (!storedName || storedName.includes("/") || storedName.includes("\\") || storedName === "." || storedName === "..") {
121
+ return false
122
+ }
123
+
124
+ const absolutePath = path.join(getProjectUploadDir(args.localPath), storedName)
125
+ try {
126
+ await rm(absolutePath, { force: true })
127
+ return true
128
+ } catch {
129
+ return false
130
+ }
131
+ }