codemini-cli 0.5.1 → 0.5.3

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 (384) hide show
  1. package/OPERATIONS.md +1 -1
  2. package/README.md +25 -25
  3. package/codemini-web/dist/assets/abap-9R7RjkQK.js +1 -0
  4. package/codemini-web/dist/assets/actionscript-3-CNowah7a.js +1 -0
  5. package/codemini-web/dist/assets/ada-D5HmrmpD.js +1 -0
  6. package/codemini-web/dist/assets/andromeeda-i_b3g2bn.js +1 -0
  7. package/codemini-web/dist/assets/{angular-html-Cde9qVP5.js → angular-html-CHsdtmkx.js} +1 -1
  8. package/codemini-web/dist/assets/angular-html-DlSp9HPG.js +1 -0
  9. package/codemini-web/dist/assets/{angular-ts-DefScAWP.js → angular-ts-B29Ypg6B.js} +1 -1
  10. package/codemini-web/dist/assets/angular-ts-JNDGGWj0.js +1 -0
  11. package/codemini-web/dist/assets/apache-D37U8o0r.js +1 -0
  12. package/codemini-web/dist/assets/apex-CszkG4lQ.js +1 -0
  13. package/codemini-web/dist/assets/apl-BASZ7uB_.js +1 -0
  14. package/codemini-web/dist/assets/{apl-w753UHCW.js → apl-Byuv-g-O.js} +1 -1
  15. package/codemini-web/dist/assets/applescript-Ajt9gmC_.js +1 -0
  16. package/codemini-web/dist/assets/ara-DpGk8XWf.js +1 -0
  17. package/codemini-web/dist/assets/asciidoc-BdDORSgx.js +1 -0
  18. package/codemini-web/dist/assets/asm-DGLRM3OU.js +1 -0
  19. package/codemini-web/dist/assets/{astro-DBA9Da7s.js → astro-CTzEugGW.js} +1 -1
  20. package/codemini-web/dist/assets/astro-l-XABzN2.js +1 -0
  21. package/codemini-web/dist/assets/aurora-x-DdfqxvM_.js +1 -0
  22. package/codemini-web/dist/assets/awk-B-J9te9x.js +1 -0
  23. package/codemini-web/dist/assets/ayu-dark-BdNJk5qF.js +1 -0
  24. package/codemini-web/dist/assets/ayu-light-DkLNbAkT.js +1 -0
  25. package/codemini-web/dist/assets/ayu-mirage-BoyGjGHX.js +1 -0
  26. package/codemini-web/dist/assets/ballerina-CZ8CAKmi.js +1 -0
  27. package/codemini-web/dist/assets/bat-fNtaIqKE.js +1 -0
  28. package/codemini-web/dist/assets/beancount-9imagnS2.js +1 -0
  29. package/codemini-web/dist/assets/berry-QTBYpUPn.js +1 -0
  30. package/codemini-web/dist/assets/bibtex-CzFWvn9u.js +1 -0
  31. package/codemini-web/dist/assets/bicep-DLWEubga.js +1 -0
  32. package/codemini-web/dist/assets/bird2-V7dWeQhQ.js +1 -0
  33. package/codemini-web/dist/assets/blade-B3_bxlV2.js +1 -0
  34. package/codemini-web/dist/assets/{blade-B-u5p0Rp.js → blade-Dt6km9YT.js} +1 -1
  35. package/codemini-web/dist/assets/bsl-D3u1l2dW.js +1 -0
  36. package/codemini-web/dist/assets/c-CgTSul4N.js +1 -0
  37. package/codemini-web/dist/assets/c3-SmmwBAzR.js +1 -0
  38. package/codemini-web/dist/assets/cadence-DVNAI4pJ.js +1 -0
  39. package/codemini-web/dist/assets/cairo-CoQzhJ_o.js +1 -0
  40. package/codemini-web/dist/assets/catppuccin-frappe-DXCXlnyU.js +1 -0
  41. package/codemini-web/dist/assets/catppuccin-latte-BXW2qYGv.js +1 -0
  42. package/codemini-web/dist/assets/catppuccin-macchiato--32cesHH.js +1 -0
  43. package/codemini-web/dist/assets/catppuccin-mocha-BBRFilGN.js +1 -0
  44. package/codemini-web/dist/assets/clarity-BJs8u8xG.js +1 -0
  45. package/codemini-web/dist/assets/clojure-DpYoDXXl.js +1 -0
  46. package/codemini-web/dist/assets/cmake-CdseusyP.js +1 -0
  47. package/codemini-web/dist/assets/{cobol-CkQrskif.js → cobol-D_kesMA-.js} +1 -1
  48. package/codemini-web/dist/assets/cobol-DqhY5el-.js +1 -0
  49. package/codemini-web/dist/assets/codeowners-5EAeZxPo.js +1 -0
  50. package/codemini-web/dist/assets/codeql-OkUuMGe9.js +1 -0
  51. package/codemini-web/dist/assets/coffee-DF15_0rk.js +1 -0
  52. package/codemini-web/dist/assets/common-lisp-DCRDlc4V.js +1 -0
  53. package/codemini-web/dist/assets/coq-acwo70uY.js +1 -0
  54. package/codemini-web/dist/assets/cpp-Cudwe5mG.js +1 -0
  55. package/codemini-web/dist/assets/{crystal-Dh09vXLx.js → crystal-Bw1_1hFA.js} +1 -1
  56. package/codemini-web/dist/assets/crystal-DV9Nijjb.js +1 -0
  57. package/codemini-web/dist/assets/csharp-Cqz84DXU.js +1 -0
  58. package/codemini-web/dist/assets/css-z2ijrDWT.js +1 -0
  59. package/codemini-web/dist/assets/csv-CqpUSPyF.js +1 -0
  60. package/codemini-web/dist/assets/cue-D4-Hq7Jx.js +1 -0
  61. package/codemini-web/dist/assets/cypher-CXMVfSpG.js +1 -0
  62. package/codemini-web/dist/assets/d-Cc_KoyiC.js +1 -0
  63. package/codemini-web/dist/assets/dark-plus-C0kLyDb3.js +1 -0
  64. package/codemini-web/dist/assets/dart-CVd7TDni.js +1 -0
  65. package/codemini-web/dist/assets/dax-DtoJMuz5.js +1 -0
  66. package/codemini-web/dist/assets/desktop-BkIJ2CMP.js +1 -0
  67. package/codemini-web/dist/assets/diff-RUsawely.js +1 -0
  68. package/codemini-web/dist/assets/docker-CDpVZYy7.js +1 -0
  69. package/codemini-web/dist/assets/dotenv-DWnlamm2.js +1 -0
  70. package/codemini-web/dist/assets/dracula-QP8aKUFB.js +1 -0
  71. package/codemini-web/dist/assets/dracula-soft-BNzyYlRI.js +1 -0
  72. package/codemini-web/dist/assets/dream-maker-MNBsfIGU.js +1 -0
  73. package/codemini-web/dist/assets/{edge-CCRtknu5.js → edge-BosfXYh1.js} +1 -1
  74. package/codemini-web/dist/assets/edge-r5NIe9Lp.js +1 -0
  75. package/codemini-web/dist/assets/{elixir-5EYbFEoC.js → elixir-B04apOJS.js} +1 -1
  76. package/codemini-web/dist/assets/elixir-Dw_vsdUl.js +1 -0
  77. package/codemini-web/dist/assets/elm-DPfFNNkG.js +1 -0
  78. package/codemini-web/dist/assets/emacs-lisp-BHIDiQRU.js +1 -0
  79. package/codemini-web/dist/assets/{erb-C6pPz4fX.js → erb-BNioEFKQ.js} +1 -1
  80. package/codemini-web/dist/assets/erb-DZuoCU5t.js +1 -0
  81. package/codemini-web/dist/assets/erlang-CqHCISqs.js +1 -0
  82. package/codemini-web/dist/assets/everforest-dark-DSK976eP.js +1 -0
  83. package/codemini-web/dist/assets/everforest-light-BkaBVm1r.js +1 -0
  84. package/codemini-web/dist/assets/fennel-BsP4yvxI.js +1 -0
  85. package/codemini-web/dist/assets/fish-CimYqIk1.js +1 -0
  86. package/codemini-web/dist/assets/fluent-O-Iic89O.js +1 -0
  87. package/codemini-web/dist/assets/fortran-fixed-form-60ZQAHDk.js +1 -0
  88. package/codemini-web/dist/assets/fortran-free-form-BVNtEstX.js +1 -0
  89. package/codemini-web/dist/assets/fsharp-Ch5ZVinn.js +1 -0
  90. package/codemini-web/dist/assets/gdresource-CqNLhJW1.js +1 -0
  91. package/codemini-web/dist/assets/gdscript-D-FJCTl3.js +1 -0
  92. package/codemini-web/dist/assets/gdshader-Czvzt8dR.js +1 -0
  93. package/codemini-web/dist/assets/genie-Mnf_jXpg.js +1 -0
  94. package/codemini-web/dist/assets/gherkin-DzkYuFEO.js +1 -0
  95. package/codemini-web/dist/assets/git-commit-CSWECvqq.js +1 -0
  96. package/codemini-web/dist/assets/git-rebase-MUvuytPe.js +1 -0
  97. package/codemini-web/dist/assets/github-dark-C98YWDhk.js +1 -0
  98. package/codemini-web/dist/assets/github-dark-default-CDGuVkyn.js +1 -0
  99. package/codemini-web/dist/assets/github-dark-dimmed-CarTohA3.js +1 -0
  100. package/codemini-web/dist/assets/github-dark-high-contrast-CsjOICAd.js +1 -0
  101. package/codemini-web/dist/assets/github-light-D17m3GP4.js +1 -0
  102. package/codemini-web/dist/assets/github-light-default-DiMoa1ZI.js +1 -0
  103. package/codemini-web/dist/assets/github-light-high-contrast-DGwHD8Ty.js +1 -0
  104. package/codemini-web/dist/assets/gleam-MAXpOqqF.js +1 -0
  105. package/codemini-web/dist/assets/{glimmer-js-C4PTXfaW.js → glimmer-js-Dn6vjgHP.js} +1 -1
  106. package/codemini-web/dist/assets/glimmer-js-HDDEErW7.js +1 -0
  107. package/codemini-web/dist/assets/glimmer-ts-Dq0_BflX.js +1 -0
  108. package/codemini-web/dist/assets/{glimmer-ts-Df1V6YK_.js → glimmer-ts-mq-Q_zEE.js} +1 -1
  109. package/codemini-web/dist/assets/glsl-C4rvog10.js +1 -0
  110. package/codemini-web/dist/assets/gn-BNADkWgR.js +1 -0
  111. package/codemini-web/dist/assets/gnuplot-BRQvwTQM.js +1 -0
  112. package/codemini-web/dist/assets/go-iV8pwUYW.js +1 -0
  113. package/codemini-web/dist/assets/graphql-Dg0Ewf8Q.js +1 -0
  114. package/codemini-web/dist/assets/groovy-CxG1cXrE.js +1 -0
  115. package/codemini-web/dist/assets/gruvbox-dark-hard-Cnb1fzry.js +1 -0
  116. package/codemini-web/dist/assets/gruvbox-dark-medium-Cu5j8v_d.js +1 -0
  117. package/codemini-web/dist/assets/gruvbox-dark-soft-BghHDwrM.js +1 -0
  118. package/codemini-web/dist/assets/gruvbox-light-hard-BUhLUxUB.js +1 -0
  119. package/codemini-web/dist/assets/gruvbox-light-medium-GXeiXGL9.js +1 -0
  120. package/codemini-web/dist/assets/gruvbox-light-soft-pTZgKx7j.js +1 -0
  121. package/codemini-web/dist/assets/{hack-zJL_tEH0.js → hack-DeD259o6.js} +1 -1
  122. package/codemini-web/dist/assets/hack-DjE76gIx.js +1 -0
  123. package/codemini-web/dist/assets/{haml-qjVLLTZk.js → haml-B8MRe8CS.js} +1 -1
  124. package/codemini-web/dist/assets/haml-DyIc_iO2.js +1 -0
  125. package/codemini-web/dist/assets/handlebars-DQ4X08Zf.js +1 -0
  126. package/codemini-web/dist/assets/{handlebars-B3RyMMk0.js → handlebars-GoJyzhp1.js} +1 -1
  127. package/codemini-web/dist/assets/haskell-BZ8bGMYA.js +1 -0
  128. package/codemini-web/dist/assets/haxe-1eAUKQ0B.js +1 -0
  129. package/codemini-web/dist/assets/hcl-DQqvKq2I.js +1 -0
  130. package/codemini-web/dist/assets/{highlighted-body-OFNGDK62-0FuBVb0_.js → highlighted-body-OFNGDK62-CDUecv8k.js} +1 -1
  131. package/codemini-web/dist/assets/hjson-DNWMSWLN.js +1 -0
  132. package/codemini-web/dist/assets/hlsl-Df5oFh9i.js +1 -0
  133. package/codemini-web/dist/assets/horizon-BCbhfxa1.js +1 -0
  134. package/codemini-web/dist/assets/horizon-bright-B0vmEjwb.js +1 -0
  135. package/codemini-web/dist/assets/houston-DHnjBE42.js +1 -0
  136. package/codemini-web/dist/assets/{html-DP-V-rfg.js → html-B7kk5NVw.js} +1 -1
  137. package/codemini-web/dist/assets/html-GfHBuoLs.js +1 -0
  138. package/codemini-web/dist/assets/{html-derivative-DB9UhNDN.js → html-derivative-Ba28nVB3.js} +1 -1
  139. package/codemini-web/dist/assets/html-derivative-ChSmSZXY.js +1 -0
  140. package/codemini-web/dist/assets/http-DMTBhQm8.js +1 -0
  141. package/codemini-web/dist/assets/hurl-XIpACwjS.js +1 -0
  142. package/codemini-web/dist/assets/hxml-PnV_FpS-.js +1 -0
  143. package/codemini-web/dist/assets/hy-DaILHjW8.js +1 -0
  144. package/codemini-web/dist/assets/imba-BA6_JyKt.js +1 -0
  145. package/codemini-web/dist/assets/index-Cfk9ARKs.js +426 -0
  146. package/codemini-web/dist/assets/index-Dqq2DCEb.css +2 -0
  147. package/codemini-web/dist/assets/ini-DqXMcqXm.js +1 -0
  148. package/codemini-web/dist/assets/java-Ds1cyVrp.js +1 -0
  149. package/codemini-web/dist/assets/javascript-Cx8CXBJR.js +1 -0
  150. package/codemini-web/dist/assets/{jinja-BcRTlqOI.js → jinja-8ATGhxJO.js} +1 -1
  151. package/codemini-web/dist/assets/jinja-Dx5IcFSt.js +1 -0
  152. package/codemini-web/dist/assets/jison-CSsYnr7O.js +1 -0
  153. package/codemini-web/dist/assets/json-CY14kIzN.js +1 -0
  154. package/codemini-web/dist/assets/json5-Cus7zL-r.js +1 -0
  155. package/codemini-web/dist/assets/jsonc-RzqrtWTz.js +1 -0
  156. package/codemini-web/dist/assets/jsonl-CLcLUtvq.js +1 -0
  157. package/codemini-web/dist/assets/jsonnet--7yRuAzt.js +1 -0
  158. package/codemini-web/dist/assets/jssm-Cid5Rr2Y.js +1 -0
  159. package/codemini-web/dist/assets/jsx-DmH5dwMF.js +1 -0
  160. package/codemini-web/dist/assets/julia-vjG00jwO.js +1 -0
  161. package/codemini-web/dist/assets/{just-O9z-zIlY.js → just-BlUYrdKb.js} +1 -1
  162. package/codemini-web/dist/assets/just-C7G3KFgN.js +1 -0
  163. package/codemini-web/dist/assets/kanagawa-dragon-BxDDNs-7.js +1 -0
  164. package/codemini-web/dist/assets/kanagawa-lotus-BQyifewU.js +1 -0
  165. package/codemini-web/dist/assets/kanagawa-wave-BsJGVIZT.js +1 -0
  166. package/codemini-web/dist/assets/kdl-D2MbAIKj.js +1 -0
  167. package/codemini-web/dist/assets/kotlin-fpq87Jdo.js +1 -0
  168. package/codemini-web/dist/assets/kusto-BcKmLn2p.js +1 -0
  169. package/codemini-web/dist/assets/laserwave-CrjhMkHv.js +1 -0
  170. package/codemini-web/dist/assets/latex-D8-NgtPj.js +1 -0
  171. package/codemini-web/dist/assets/lean-B9Xp-kaJ.js +1 -0
  172. package/codemini-web/dist/assets/less-BKjOc0Gl.js +1 -0
  173. package/codemini-web/dist/assets/light-plus-DFt9HI_B.js +1 -0
  174. package/codemini-web/dist/assets/{liquid-BK2PyMjt.js → liquid-BUh7yINm.js} +1 -1
  175. package/codemini-web/dist/assets/liquid-D2m0XXrV.js +1 -0
  176. package/codemini-web/dist/assets/llvm-XDy97SdL.js +1 -0
  177. package/codemini-web/dist/assets/log-tjGJZ7jx.js +1 -0
  178. package/codemini-web/dist/assets/logo-rCQwad-6.js +1 -0
  179. package/codemini-web/dist/assets/lua-BeSXusec.js +1 -0
  180. package/codemini-web/dist/assets/luau-C0wE4YeK.js +1 -0
  181. package/codemini-web/dist/assets/make-DZENZd8z.js +1 -0
  182. package/codemini-web/dist/assets/markdown-CbgVmtS5.js +1 -0
  183. package/codemini-web/dist/assets/marko-DPnRthJQ.js +1 -0
  184. package/codemini-web/dist/assets/{marko-CTEKS39A.js → marko-Dm8xPXkG.js} +1 -1
  185. package/codemini-web/dist/assets/material-theme-YhiuxyiP.js +1 -0
  186. package/codemini-web/dist/assets/material-theme-darker-CKeuyK7B.js +1 -0
  187. package/codemini-web/dist/assets/material-theme-lighter-C56RzTfM.js +1 -0
  188. package/codemini-web/dist/assets/material-theme-ocean-DUos8xKe.js +1 -0
  189. package/codemini-web/dist/assets/material-theme-palenight-CZBKx4B1.js +1 -0
  190. package/codemini-web/dist/assets/matlab-BJwkjxyL.js +1 -0
  191. package/codemini-web/dist/assets/mdc-BLdFHYKv.js +1 -0
  192. package/codemini-web/dist/assets/{mdc-BOaCYzzL.js → mdc-Dzipcsdb.js} +1 -1
  193. package/codemini-web/dist/assets/mdx-BvkytFG9.js +1 -0
  194. package/codemini-web/dist/assets/mermaid-Ctx7uNrT.js +1 -0
  195. package/codemini-web/dist/assets/mermaid-GHXKKRXX-BWSxGKNn.js +1 -0
  196. package/codemini-web/dist/assets/min-dark-BA-rwgF3.js +1 -0
  197. package/codemini-web/dist/assets/min-light-DHUVhXnm.js +1 -0
  198. package/codemini-web/dist/assets/mipsasm-ByaU0dEX.js +1 -0
  199. package/codemini-web/dist/assets/mojo-CqhYERP_.js +1 -0
  200. package/codemini-web/dist/assets/monokai-D3MyQCxG.js +1 -0
  201. package/codemini-web/dist/assets/moonbit-C4BfwLm6.js +1 -0
  202. package/codemini-web/dist/assets/move-DOj5FawL.js +1 -0
  203. package/codemini-web/dist/assets/narrat-CDHYPJWr.js +1 -0
  204. package/codemini-web/dist/assets/{nextflow-Cto1mpL3.js → nextflow-BRiWOV-p.js} +1 -1
  205. package/codemini-web/dist/assets/nextflow-c8YCPRRb.js +1 -0
  206. package/codemini-web/dist/assets/nextflow-groovy-D6rYlJlL.js +1 -0
  207. package/codemini-web/dist/assets/nginx-u-hq5m-a.js +1 -0
  208. package/codemini-web/dist/assets/night-owl-NHZjGVLB.js +1 -0
  209. package/codemini-web/dist/assets/night-owl-light-DcKosvl7.js +1 -0
  210. package/codemini-web/dist/assets/nim-B5b6_jZr.js +1 -0
  211. package/codemini-web/dist/assets/{nim-BZyx58NM.js → nim-CvgRYLO2.js} +1 -1
  212. package/codemini-web/dist/assets/nix-Csr5leD-.js +1 -0
  213. package/codemini-web/dist/assets/nord-DomFMWpF.js +1 -0
  214. package/codemini-web/dist/assets/nushell-IZVQdI7Z.js +1 -0
  215. package/codemini-web/dist/assets/objective-c-DD1i5sf2.js +1 -0
  216. package/codemini-web/dist/assets/objective-cpp-CtzOCe2w.js +1 -0
  217. package/codemini-web/dist/assets/ocaml-BlMDMjEj.js +1 -0
  218. package/codemini-web/dist/assets/odin-CoWSDSNi.js +1 -0
  219. package/codemini-web/dist/assets/one-dark-pro-DWENJxB_.js +1 -0
  220. package/codemini-web/dist/assets/one-light-CEeF6aqP.js +1 -0
  221. package/codemini-web/dist/assets/openscad-D3gRRPUh.js +1 -0
  222. package/codemini-web/dist/assets/pascal-G3gJIkHz.js +1 -0
  223. package/codemini-web/dist/assets/{perl-BW8t13Xq.js → perl-BIcNvyL6.js} +1 -1
  224. package/codemini-web/dist/assets/perl-DdF12AsD.js +1 -0
  225. package/codemini-web/dist/assets/php-BP-3TkW_.js +1 -0
  226. package/codemini-web/dist/assets/{php-Cq5FOhfR.js → php-CyQZnE0l.js} +1 -1
  227. package/codemini-web/dist/assets/pierre-dark-CBqOR10c.js +1 -0
  228. package/codemini-web/dist/assets/pierre-light-sgwdVCEP.js +1 -0
  229. package/codemini-web/dist/assets/pkl-DdsryWcC.js +1 -0
  230. package/codemini-web/dist/assets/plastic-CwUA_4-2.js +1 -0
  231. package/codemini-web/dist/assets/plsql-C2yEtnCG.js +1 -0
  232. package/codemini-web/dist/assets/po-Dy7ytuR1.js +1 -0
  233. package/codemini-web/dist/assets/poimandres-u68WOWhl.js +1 -0
  234. package/codemini-web/dist/assets/polar-CjLVWIyd.js +1 -0
  235. package/codemini-web/dist/assets/postcss-DiNtjF5e.js +1 -0
  236. package/codemini-web/dist/assets/powerquery-iC4Ra9LJ.js +1 -0
  237. package/codemini-web/dist/assets/powershell-Ch2YpFAx.js +1 -0
  238. package/codemini-web/dist/assets/prisma-HWctQCpn.js +1 -0
  239. package/codemini-web/dist/assets/prolog-DHAsH4Hf.js +1 -0
  240. package/codemini-web/dist/assets/proto-mTghX89X.js +1 -0
  241. package/codemini-web/dist/assets/pug-HwoxZo9M.js +1 -0
  242. package/codemini-web/dist/assets/{pug-B4WHazil.js → pug-_XoIcKf3.js} +1 -1
  243. package/codemini-web/dist/assets/puppet-D1Zcnrr5.js +1 -0
  244. package/codemini-web/dist/assets/purescript-s82m5f0Y.js +1 -0
  245. package/codemini-web/dist/assets/python-BhsI1K6K.js +1 -0
  246. package/codemini-web/dist/assets/qml-CysuucQ6.js +1 -0
  247. package/codemini-web/dist/assets/qmldir-Bzkc2Njy.js +1 -0
  248. package/codemini-web/dist/assets/qss-CKbozdtF.js +1 -0
  249. package/codemini-web/dist/assets/r-BKej5r_k.js +1 -0
  250. package/codemini-web/dist/assets/racket-CUDoXUwy.js +1 -0
  251. package/codemini-web/dist/assets/raku-XPAjN9OU.js +1 -0
  252. package/codemini-web/dist/assets/{razor-CQPYUgex.js → razor-DQ6FV4WL.js} +1 -1
  253. package/codemini-web/dist/assets/razor-ZpEr_C9H.js +1 -0
  254. package/codemini-web/dist/assets/red-8aRo80UF.js +1 -0
  255. package/codemini-web/dist/assets/reg-B0B_plbq.js +1 -0
  256. package/codemini-web/dist/assets/regexp-CK-1EP63.js +1 -0
  257. package/codemini-web/dist/assets/rel-DhoFe2Y7.js +1 -0
  258. package/codemini-web/dist/assets/riscv-CKx_Jisq.js +1 -0
  259. package/codemini-web/dist/assets/ron-qyvgw9vu.js +1 -0
  260. package/codemini-web/dist/assets/rose-pine-R3f76h4_.js +1 -0
  261. package/codemini-web/dist/assets/rose-pine-dawn-BS09rtpK.js +1 -0
  262. package/codemini-web/dist/assets/rose-pine-moon-B4Q4DiPG.js +1 -0
  263. package/codemini-web/dist/assets/rosmsg-Dy_ItAfo.js +1 -0
  264. package/codemini-web/dist/assets/rst-CjO4XFKz.js +1 -0
  265. package/codemini-web/dist/assets/{rst-B-OS2pa5.js → rst-DDrWKQ9c.js} +1 -1
  266. package/codemini-web/dist/assets/ruby-BOVYgV5O.js +1 -0
  267. package/codemini-web/dist/assets/{ruby-BM15fmxz.js → ruby-CZgIYPlP.js} +1 -1
  268. package/codemini-web/dist/assets/rust-CNEVkiWF.js +1 -0
  269. package/codemini-web/dist/assets/sas-DPGT0TjQ.js +1 -0
  270. package/codemini-web/dist/assets/sass-D2nye32F.js +1 -0
  271. package/codemini-web/dist/assets/scala-B8greAnE.js +1 -0
  272. package/codemini-web/dist/assets/scheme-Ci_aquns.js +1 -0
  273. package/codemini-web/dist/assets/{scss-C0BQ8YVM.js → scss-CaciJ0Gl.js} +1 -1
  274. package/codemini-web/dist/assets/scss-YARKxu2i.js +1 -0
  275. package/codemini-web/dist/assets/sdbl-D4tJqBiC.js +1 -0
  276. package/codemini-web/dist/assets/shaderlab-JWcsxey4.js +1 -0
  277. package/codemini-web/dist/assets/shellscript-CxhArCCJ.js +1 -0
  278. package/codemini-web/dist/assets/shellsession-DYmuifdt.js +1 -0
  279. package/codemini-web/dist/assets/slack-dark-BcN7KJ02.js +1 -0
  280. package/codemini-web/dist/assets/slack-ochin-D4iInQ9w.js +1 -0
  281. package/codemini-web/dist/assets/smalltalk-BwSireXb.js +1 -0
  282. package/codemini-web/dist/assets/snazzy-light-B45TfPsv.js +1 -0
  283. package/codemini-web/dist/assets/solarized-dark-8yYW1wLs.js +1 -0
  284. package/codemini-web/dist/assets/solarized-light-BY3Hr0Ud.js +1 -0
  285. package/codemini-web/dist/assets/solidity-CIUlntDs.js +1 -0
  286. package/codemini-web/dist/assets/{soy-C6D0Ibo9.js → soy-7iHZG9Jz.js} +1 -1
  287. package/codemini-web/dist/assets/soy-h0dQKbQs.js +1 -0
  288. package/codemini-web/dist/assets/sparql-BGRSvFxd.js +1 -0
  289. package/codemini-web/dist/assets/splunk-DueVPk5k.js +1 -0
  290. package/codemini-web/dist/assets/sql-Q6zkgi_w.js +1 -0
  291. package/codemini-web/dist/assets/ssh-config-DKIMErlp.js +1 -0
  292. package/codemini-web/dist/assets/stata-lXN9BRaz.js +1 -0
  293. package/codemini-web/dist/assets/stylus-oqceM0yt.js +1 -0
  294. package/codemini-web/dist/assets/surrealql-l0SGzwOs.js +1 -0
  295. package/codemini-web/dist/assets/svelte-BM89dRu0.js +1 -0
  296. package/codemini-web/dist/assets/{svelte-DJ-0FpL2.js → svelte-CijnRGul.js} +1 -1
  297. package/codemini-web/dist/assets/swift-foA31xPc.js +1 -0
  298. package/codemini-web/dist/assets/synthwave-84-B002ZU4p.js +1 -0
  299. package/codemini-web/dist/assets/system-verilog-CCSGcM60.js +1 -0
  300. package/codemini-web/dist/assets/systemd-BymS_eIJ.js +1 -0
  301. package/codemini-web/dist/assets/talonscript-Ccc0122j.js +1 -0
  302. package/codemini-web/dist/assets/tasl-o38sfjeU.js +1 -0
  303. package/codemini-web/dist/assets/tcl-DISPE-5Q.js +1 -0
  304. package/codemini-web/dist/assets/{templ-7Bvm-rei.js → templ-CNjJl440.js} +1 -1
  305. package/codemini-web/dist/assets/templ-D1GoDFsp.js +1 -0
  306. package/codemini-web/dist/assets/terraform-V85Kewcz.js +1 -0
  307. package/codemini-web/dist/assets/tex-CxEkh6NI.js +1 -0
  308. package/codemini-web/dist/assets/tokyo-night-BLn5KNCP.js +1 -0
  309. package/codemini-web/dist/assets/toml-uEjeW8NC.js +1 -0
  310. package/codemini-web/dist/assets/{ts-tags-BfuR6EQ0.js → ts-tags-B88Nbzcl.js} +1 -1
  311. package/codemini-web/dist/assets/ts-tags-DV6nguf9.js +1 -0
  312. package/codemini-web/dist/assets/tsv-DjGwUc04.js +1 -0
  313. package/codemini-web/dist/assets/tsx-5O-M05V0.js +1 -0
  314. package/codemini-web/dist/assets/turtle-PTxTluYn.js +1 -0
  315. package/codemini-web/dist/assets/twig-B3Kws0Fb.js +1 -0
  316. package/codemini-web/dist/assets/{twig-_uxJ-QmX.js → twig-BROkIZLg.js} +1 -1
  317. package/codemini-web/dist/assets/typescript-hRweav8r.js +1 -0
  318. package/codemini-web/dist/assets/typespec-CKEcF6Kw.js +1 -0
  319. package/codemini-web/dist/assets/typst-DSQh9HII.js +1 -0
  320. package/codemini-web/dist/assets/v-obK6h_YW.js +1 -0
  321. package/codemini-web/dist/assets/vala-DXa9jSo-.js +1 -0
  322. package/codemini-web/dist/assets/vb-DEPbtYq-.js +1 -0
  323. package/codemini-web/dist/assets/verilog-Bs-185hK.js +1 -0
  324. package/codemini-web/dist/assets/vesper-B7BneC5G.js +1 -0
  325. package/codemini-web/dist/assets/vhdl-CBukgB2Z.js +1 -0
  326. package/codemini-web/dist/assets/viml-C1cRY0x6.js +1 -0
  327. package/codemini-web/dist/assets/vitesse-black-D3_12zni.js +1 -0
  328. package/codemini-web/dist/assets/vitesse-dark-B_iF7INE.js +1 -0
  329. package/codemini-web/dist/assets/vitesse-light-DbrrMtxY.js +1 -0
  330. package/codemini-web/dist/assets/{vue-DkOBu49o.js → vue-BUQP_8VF.js} +1 -1
  331. package/codemini-web/dist/assets/vue-CT36qSUr.js +1 -0
  332. package/codemini-web/dist/assets/vue-html-bJyjqZnI.js +1 -0
  333. package/codemini-web/dist/assets/vue-vine-BLqV15Pm.js +1 -0
  334. package/codemini-web/dist/assets/{vue-vine-veyFk6GJ.js → vue-vine-RK3AHrhp.js} +1 -1
  335. package/codemini-web/dist/assets/vyper-BxVot6rR.js +1 -0
  336. package/codemini-web/dist/assets/wasm-BNTPmfEI.js +1 -0
  337. package/codemini-web/dist/assets/wasm-o5uYkU9G.js +1 -0
  338. package/codemini-web/dist/assets/wenyan-MnC4h24X.js +1 -0
  339. package/codemini-web/dist/assets/wgsl-BgfkHnVi.js +1 -0
  340. package/codemini-web/dist/assets/wikitext-DCOnP3yk.js +1 -0
  341. package/codemini-web/dist/assets/wit-5UDEI7sp.js +1 -0
  342. package/codemini-web/dist/assets/wolfram-vvgPlu2n.js +1 -0
  343. package/codemini-web/dist/assets/xml-DyJ3D9rW.js +1 -0
  344. package/codemini-web/dist/assets/xsl-BOsdDgv_.js +1 -0
  345. package/codemini-web/dist/assets/yaml-C-G727Y6.js +1 -0
  346. package/codemini-web/dist/assets/zenscript-BPjqX0BY.js +1 -0
  347. package/codemini-web/dist/assets/zig-Bd91oU8r.js +1 -0
  348. package/codemini-web/dist/codemini_logo.png +0 -0
  349. package/codemini-web/dist/favicon.ico +0 -0
  350. package/codemini-web/dist/index.html +6 -3
  351. package/codemini-web/lib/runtime-bridge.js +22 -1
  352. package/codemini-web/server.js +919 -880
  353. package/deployment.md +5 -5
  354. package/package.json +1 -1
  355. package/src/commands/chat.js +1 -1
  356. package/src/commands/run.js +22 -7
  357. package/src/core/agent-loop.js +11 -7
  358. package/src/core/chat-runtime.js +314 -86
  359. package/src/core/command-evaluator.js +9 -3
  360. package/src/core/command-risk.js +9 -1
  361. package/src/core/config-store.js +4 -2
  362. package/src/core/context-compact.js +65 -3
  363. package/src/core/default-system-prompt.js +12 -4
  364. package/src/core/fff-adapter.js +1 -1
  365. package/src/core/reflect-skill.js +1 -1
  366. package/src/core/reply-language.js +6 -0
  367. package/src/core/session-store.js +18 -0
  368. package/src/core/shell-profile.js +14 -1
  369. package/src/core/soul.js +1 -3
  370. package/src/core/system-prompt-composer.js +42 -0
  371. package/src/core/tools.js +2 -2
  372. package/src/tui/chat-app.js +13 -3
  373. package/codemini-web/dist/assets/index-CSYMjBeT.css +0 -2
  374. package/codemini-web/dist/assets/index-DtiSt5vh.js +0 -206
  375. package/codemini-web/dist/assets/mermaid-GHXKKRXX-BQrwnoIu.js +0 -1
  376. /package/codemini-web/dist/assets/{bird2-Bs7YDhA3.js → bird2-VVa6lZ01.js} +0 -0
  377. /package/codemini-web/dist/assets/{css-BTi88BmP.js → css-Dr0v2l-V.js} +0 -0
  378. /package/codemini-web/dist/assets/{dart-Czrjv-nr.js → dart-BIyV3H7V.js} +0 -0
  379. /package/codemini-web/dist/assets/{emacs-lisp-D4W-_rAk.js → emacs-lisp-CVlPXEEa.js} +0 -0
  380. /package/codemini-web/dist/assets/{go-BTh90-YS.js → go-D6k5URxL.js} +0 -0
  381. /package/codemini-web/dist/assets/{horizon-bright-CpoqdULz.js → horizon-bright-CXcxBcih.js} +0 -0
  382. /package/codemini-web/dist/assets/{kusto-ByPWIc1b.js → kusto-B1r4va-8.js} +0 -0
  383. /package/codemini-web/dist/assets/{nextflow-groovy-D1X5Yd05.js → nextflow-groovy-o7ZjyFmf.js} +0 -0
  384. /package/codemini-web/dist/assets/{typespec-8nkhqWz6.js → typespec-N7-U_jBZ.js} +0 -0
@@ -1,880 +1,919 @@
1
- import http from 'node:http';
2
- import fs from 'node:fs/promises';
3
- import path from 'node:path';
4
- import { fileURLToPath } from 'node:url';
5
- import { execSync } from 'node:child_process';
6
-
7
- import { loadConfig, saveConfig, setConfigValue, getConfigValue } from '../src/core/config-store.js';
8
- import { createChatRuntime } from '../src/core/chat-runtime.js';
9
- import { createSession, loadSession, listSessions, resolveSession, deleteSession } from '../src/core/session-store.js';
10
- import { buildDefaultSystemPrompt } from '../src/core/default-system-prompt.js';
11
- import { RuntimeBridge } from './lib/runtime-bridge.js';
12
- import { listSkillEntries } from '../src/commands/skill.js';
13
- import { readSkillRegistry, writeSkillRegistry, upsertSkillRegistryEntry } from '../src/core/skill-registry.js';
14
- import { getReplyLanguage } from '../src/core/reply-language.js';
15
- import { createRequire } from 'node:module';
16
- const require = createRequire(import.meta.url);
17
- const pkg = require('../package.json');
18
- import { getSkillsDir, getBaseConfigDir } from '../src/core/paths.js';
19
-
20
- const __dirname = path.dirname(fileURLToPath(import.meta.url));
21
- const CLIENT_SOURCE_DIR = path.join(__dirname, 'client');
22
- let CLIENT_DIR = CLIENT_SOURCE_DIR;
23
- try {
24
- const distDir = path.join(__dirname, 'dist');
25
- const stat = await fs.stat(distDir);
26
- if (stat.isDirectory()) CLIENT_DIR = distDir;
27
- } catch {}
28
-
29
- const MIME_TYPES = {
30
- '.html': 'text/html; charset=utf-8',
31
- '.css': 'text/css; charset=utf-8',
32
- '.js': 'text/javascript; charset=utf-8',
33
- '.json': 'application/json',
34
- '.svg': 'image/svg+xml',
35
- '.png': 'image/png',
36
- '.ico': 'image/x-icon'
37
- };
38
-
39
- function parseArgs(argv) {
40
- const parsed = { port: 3210, session: undefined, model: undefined, project: undefined, open: true };
41
- for (let i = 2; i < argv.length; i++) {
42
- const arg = argv[i];
43
- if (arg === '--port' || arg === '-p') { parsed.port = parseInt(argv[++i], 10) || 3210; continue; }
44
- if (arg === '--session' || arg === '-s') { parsed.session = argv[++i]; continue; }
45
- if (arg === '--model' || arg === '-m') { parsed.model = argv[++i]; continue; }
46
- if (arg === '--project' || arg === '-d') { parsed.project = argv[++i]; continue; }
47
- if (arg === '--no-open') { parsed.open = false; continue; }
48
- }
49
- return parsed;
50
- }
51
-
52
- function readBody(req) {
53
- return new Promise((resolve, reject) => {
54
- const chunks = [];
55
- req.on('data', (c) => chunks.push(c));
56
- req.on('end', () => {
57
- try { resolve(JSON.parse(Buffer.concat(chunks).toString())); }
58
- catch { resolve({}); }
59
- });
60
- req.on('error', reject);
61
- });
62
- }
63
-
64
- function jsonResponse(res, data, status = 200) {
65
- const body = JSON.stringify(data);
66
- res.writeHead(status, { 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(body) });
67
- res.end(body);
68
- }
69
-
70
- function buildCodeWikiAskPrompt({ question, reportPath, projectDir, replyLanguage }) {
71
- if (getReplyLanguage(replyLanguage) === 'en') {
72
- return [
73
- 'Answer the following question based on the current project and the CodeWiki / project-requirements HTML report.',
74
- `Project path: ${projectDir}`,
75
- `Report path: ${reportPath}`,
76
- '',
77
- 'Requirements:',
78
- '- Prefer reading and citing the HTML report above.',
79
- '- If the report is insufficient, use read-only project inspection to gather supporting evidence.',
80
- '- Do not modify files, generate a new report, or write memory.',
81
- '- Respond in English unless the user explicitly asks for another language.',
82
- '',
83
- `Question: ${question.trim()}`
84
- ].join('\n');
85
- }
86
- return [
87
- '请基于当前项目和 CodeWiki / project-requirements HTML 报告回答下面的问题。',
88
- `项目路径:${projectDir}`,
89
- `报告路径:${reportPath}`,
90
- '',
91
- '要求:',
92
- '- 优先读取并参考上述 HTML 报告。',
93
- '- 如果报告信息不足,可以只读检索项目文件补充证据。',
94
- '- 不要修改文件,不要生成新报告,不要写入记忆。',
95
- '- 除非用户明确要求其他语言,否则使用简体中文回答。',
96
- '',
97
- `问题:${question.trim()}`
98
- ].join('\n');
99
- }
100
-
101
- async function serveStatic(res, filePath) {
102
- const ext = path.extname(filePath);
103
- const mime = MIME_TYPES[ext] || 'application/octet-stream';
104
- try {
105
- const data = await fs.readFile(filePath);
106
- res.writeHead(200, { 'Content-Type': mime, 'Content-Length': data.length });
107
- res.end(data);
108
- } catch {
109
- res.writeHead(404, { 'Content-Type': 'text/plain' });
110
- res.end('Not found');
111
- }
112
- }
113
-
114
- function normalizeProjectPath(value) {
115
- const raw = String(value || '').trim();
116
- if (!raw) return '';
117
- const win = raw.match(/^([A-Za-z]):[\\/](.*)$/);
118
- if (win && process.platform !== 'win32') {
119
- return path.join('/mnt', win[1].toLowerCase(), win[2].replace(/[\\/]+/g, '/'));
120
- }
121
- return path.resolve(raw);
122
- }
123
-
124
- function tryParseJson(value) {
125
- try { return JSON.parse(String(value || '')); } catch { return null; }
126
- }
127
-
128
- function collectSessionPathHints(session) {
129
- const hints = [];
130
- const messages = Array.isArray(session?.messages) ? session.messages : [];
131
- for (const msg of messages) {
132
- if (Array.isArray(msg?.tool_calls)) {
133
- for (const call of msg.tool_calls) {
134
- const args = tryParseJson(call?.function?.arguments ?? call?.arguments);
135
- for (const key of ['path', 'file', 'filePath', 'cwd']) {
136
- if (typeof args?.[key] === 'string') hints.push(args[key]);
137
- }
138
- }
139
- }
140
- const content = typeof msg?.content === 'string' ? msg.content : '';
141
- for (const match of content.matchAll(/[A-Za-z]:[\\/][^\n\r"'`<>|]+/g)) hints.push(match[0]);
142
- for (const match of content.matchAll(/\/mnt\/[A-Za-z]\/[^\n\r"'`<>|]+/g)) hints.push(match[0]);
143
- }
144
- return hints;
145
- }
146
-
147
- async function existingDirectoryForHint(rawHint) {
148
- let candidate = normalizeProjectPath(rawHint);
149
- if (!candidate) return '';
150
- candidate = candidate.replace(/[),\].。;;:]+$/g, '');
151
- for (let i = 0; i < 8 && candidate && candidate !== path.dirname(candidate); i += 1) {
152
- try {
153
- const stat = await fs.stat(candidate);
154
- return stat.isDirectory() ? candidate : path.dirname(candidate);
155
- } catch {
156
- candidate = path.dirname(candidate);
157
- }
158
- }
159
- return '';
160
- }
161
-
162
- const CODEWIKI_REPORT_RE = /^[^/\\]+-project-requirements\.html$/;
163
-
164
- function getRequirementsDir(projectDir) {
165
- return path.join(projectDir, 'docs', 'requirements');
166
- }
167
-
168
- function isCodeWikiReportFile(fileName) {
169
- return CODEWIKI_REPORT_RE.test(String(fileName || ''));
170
- }
171
-
172
- function codeWikiReportTitle(fileName) {
173
- return String(fileName || '')
174
- .replace(/-project-requirements\.html$/, '')
175
- .replace(/-/g, ' ');
176
- }
177
-
178
- function commonPathPrefix(paths) {
179
- const normalized = paths.map((p) => path.resolve(p).split(path.sep).filter(Boolean));
180
- if (!normalized.length) return '';
181
- const prefix = [];
182
- for (let i = 0; i < normalized[0].length; i += 1) {
183
- const part = normalized[0][i];
184
- if (normalized.every((parts) => parts[i] === part)) prefix.push(part);
185
- else break;
186
- }
187
- if (!prefix.length) return path.parse(paths[0]).root || '';
188
- return `${path.sep}${prefix.join(path.sep)}`;
189
- }
190
-
191
- async function inferSessionProjectDir(session) {
192
- const explicit = normalizeProjectPath(session?.projectDir);
193
- if (explicit) {
194
- try {
195
- if ((await fs.stat(explicit)).isDirectory()) return explicit;
196
- } catch {}
197
- }
198
-
199
- const dirs = [];
200
- for (const hint of collectSessionPathHints(session)) {
201
- const dir = await existingDirectoryForHint(hint);
202
- if (dir) dirs.push(dir);
203
- }
204
- if (dirs.length === 0) return '';
205
-
206
- const common = commonPathPrefix(dirs);
207
- let candidate = common;
208
- while (candidate && candidate !== path.dirname(candidate)) {
209
- try {
210
- const stat = await fs.stat(candidate);
211
- if (stat.isDirectory()) return candidate;
212
- } catch {}
213
- candidate = path.dirname(candidate);
214
- }
215
- return dirs[0];
216
- }
217
-
218
- async function buildRuntimeForSession({ sessionId, model, projectDir }) {
219
- const config = await loadConfig();
220
- const session = sessionId ? await loadSession(sessionId) : await createSession(projectDir || process.cwd());
221
- const sessionProjectDir = projectDir ? normalizeProjectPath(projectDir) : await inferSessionProjectDir(session);
222
- if (sessionProjectDir) {
223
- try {
224
- const stat = await fs.stat(sessionProjectDir);
225
- if (stat.isDirectory()) process.chdir(sessionProjectDir);
226
- } catch {}
227
- }
228
- session.projectDir = process.cwd();
229
- const systemPrompt = buildDefaultSystemPrompt(config);
230
- const runtime = await createChatRuntime({
231
- session,
232
- config,
233
- model: model || config.model?.name,
234
- systemPrompt
235
- });
236
- return { runtime, config, session, cwd: process.cwd() };
237
- }
238
-
239
- async function main() {
240
- const args = parseArgs(process.argv);
241
-
242
- // Set initial project directory
243
- if (args.project) {
244
- try {
245
- const resolved = path.resolve(args.project);
246
- process.chdir(resolved);
247
- } catch {}
248
- }
249
-
250
- const { runtime: initialRuntime, config } = await buildRuntimeForSession({
251
- sessionId: args.session,
252
- model: args.model
253
- });
254
- let bridge = new RuntimeBridge(initialRuntime);
255
- let currentProjectDir = process.cwd();
256
-
257
- const server = http.createServer(async (req, res) => {
258
- const url = new URL(req.url, `http://localhost:${args.port}`);
259
-
260
- res.setHeader('Access-Control-Allow-Origin', '*');
261
- res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS');
262
- res.setHeader('Access-Control-Allow-Headers', 'Content-Type');
263
- if (req.method === 'OPTIONS') { res.writeHead(204); res.end(); return; }
264
-
265
- // SSE
266
- if (url.pathname === '/api/events' && req.method === 'GET') {
267
- bridge.addClient(res);
268
- return;
269
- }
270
-
271
- // Static files
272
- if (req.method === 'GET' && !url.pathname.startsWith('/api/')) {
273
- let filePath;
274
- if (url.pathname === '/') {
275
- filePath = path.join(CLIENT_DIR, 'index.html');
276
- } else {
277
- const relative = url.pathname.replace(/^\//, '');
278
- filePath = path.extname(relative)
279
- ? path.join(CLIENT_DIR, relative)
280
- : path.join(CLIENT_DIR, 'index.html');
281
- }
282
- if (!filePath.startsWith(CLIENT_DIR)) { res.writeHead(403); res.end(); return; }
283
- await serveStatic(res, filePath);
284
- return;
285
- }
286
-
287
- // ── Submit / Abort / Approval ──
288
- if (req.method === 'POST' && url.pathname === '/api/submit') {
289
- const { line, readOnlyCodeWiki } = await readBody(req);
290
- if (!line || typeof line !== 'string') { jsonResponse(res, { error: true, message: 'Missing "line" field' }, 400); return; }
291
- const result = bridge.handleSubmit(line, { readOnlyCodeWiki: readOnlyCodeWiki === true });
292
- jsonResponse(res, result);
293
- return;
294
- }
295
- if (req.method === 'POST' && url.pathname === '/api/abort') {
296
- bridge.handleAbort();
297
- jsonResponse(res, { ok: true });
298
- return;
299
- }
300
- if (req.method === 'POST' && url.pathname === '/api/execution-mode') {
301
- const { mode } = await readBody(req);
302
- if (!mode || !['normal', 'auto', 'plan'].includes(mode)) {
303
- jsonResponse(res, { error: true, message: 'Invalid mode' }, 400);
304
- return;
305
- }
306
- if (bridge.isBusy()) {
307
- jsonResponse(res, { error: true, message: 'Cannot switch execution mode while a request is running' }, 409);
308
- return;
309
- }
310
- const ok = await bridge.setExecutionMode(mode);
311
- jsonResponse(res, { ok });
312
- return;
313
- }
314
- if (req.method === 'POST' && url.pathname === '/api/approval') {
315
- const { id, approved } = await readBody(req);
316
- jsonResponse(res, { ok: bridge.handleApproval(id, !!approved) });
317
- return;
318
- }
319
-
320
- // ── Version ──
321
- if (req.method === 'GET' && url.pathname === '/api/version') {
322
- let latest = null;
323
- try {
324
- latest = execSync('npm view codemini-cli version', { encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'] }).trim();
325
- } catch {}
326
- jsonResponse(res, { current: pkg.version, latest });
327
- return;
328
- }
329
- if (req.method === 'POST' && url.pathname === '/api/update') {
330
- try {
331
- const output = execSync('npm update -g codemini-cli', { encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'], timeout: 60000 });
332
- jsonResponse(res, { ok: true, output: output.trim() });
333
- } catch (err) {
334
- jsonResponse(res, { ok: false, error: err.message }, 500);
335
- }
336
- return;
337
- }
338
-
339
- // ── Runtime state ──
340
- if (req.method === 'GET' && url.pathname === '/api/state') {
341
- jsonResponse(res, { ...bridge.getState(), cwd: currentProjectDir });
342
- return;
343
- }
344
- if (req.method === 'GET' && url.pathname === '/api/completions') {
345
- jsonResponse(res, bridge.getCompletions(url.searchParams.get('q') || ''));
346
- return;
347
- }
348
- if (req.method === 'GET' && url.pathname === '/api/history') {
349
- jsonResponse(res, bridge.getHistory());
350
- return;
351
- }
352
- if (req.method === 'GET' && url.pathname === '/api/commands') {
353
- jsonResponse(res, bridge.getCommands());
354
- return;
355
- }
356
- if (req.method === 'GET' && url.pathname === '/api/startup-events') {
357
- jsonResponse(res, await bridge.handleStartupEvents());
358
- return;
359
- }
360
- if (req.method === 'GET' && url.pathname === '/api/session/messages') {
361
- jsonResponse(res, bridge.getSessionMessages());
362
- return;
363
- }
364
- if (req.method === 'GET' && url.pathname === '/api/session/ui-messages') {
365
- jsonResponse(res, await bridge.getUiMessages());
366
- return;
367
- }
368
-
369
- // ── CodeWiki / project requirements reports ──
370
- if (req.method === 'GET' && url.pathname === '/api/codewiki/reports') {
371
- const requirementsDir = getRequirementsDir(currentProjectDir);
372
- try {
373
- const entries = await fs.readdir(requirementsDir, { withFileTypes: true });
374
- const reports = [];
375
- for (const entry of entries) {
376
- if (!entry.isFile() || !isCodeWikiReportFile(entry.name)) continue;
377
- const reportPath = path.join(requirementsDir, entry.name);
378
- const stat = await fs.stat(reportPath);
379
- reports.push({
380
- file: entry.name,
381
- title: codeWikiReportTitle(entry.name),
382
- size: stat.size,
383
- mtime: stat.mtime.toISOString()
384
- });
385
- }
386
- reports.sort((a, b) => new Date(b.mtime) - new Date(a.mtime));
387
- jsonResponse(res, { reports });
388
- } catch (err) {
389
- if (err?.code === 'ENOENT') jsonResponse(res, { reports: [] });
390
- else jsonResponse(res, { error: true, message: err.message }, 500);
391
- }
392
- return;
393
- }
394
-
395
- if (req.method === 'GET' && url.pathname.startsWith('/api/codewiki/report/')) {
396
- const fileName = decodeURIComponent(url.pathname.slice('/api/codewiki/report/'.length));
397
- if (!isCodeWikiReportFile(fileName)) {
398
- jsonResponse(res, { error: true, message: 'Invalid report file' }, 400);
399
- return;
400
- }
401
- const requirementsDir = path.resolve(getRequirementsDir(currentProjectDir));
402
- const reportPath = path.resolve(requirementsDir, fileName);
403
- if (!reportPath.startsWith(`${requirementsDir}${path.sep}`)) {
404
- jsonResponse(res, { error: true, message: 'Invalid report path' }, 403);
405
- return;
406
- }
407
- await serveStatic(res, reportPath);
408
- return;
409
- }
410
-
411
- if (req.method === 'DELETE' && url.pathname.startsWith('/api/codewiki/report/')) {
412
- const fileName = decodeURIComponent(url.pathname.slice('/api/codewiki/report/'.length));
413
- if (!isCodeWikiReportFile(fileName)) {
414
- jsonResponse(res, { error: true, message: 'Invalid report file' }, 400);
415
- return;
416
- }
417
- const requirementsDir = path.resolve(getRequirementsDir(currentProjectDir));
418
- const reportPath = path.resolve(requirementsDir, fileName);
419
- if (!reportPath.startsWith(`${requirementsDir}${path.sep}`)) {
420
- jsonResponse(res, { error: true, message: 'Invalid report path' }, 403);
421
- return;
422
- }
423
- try {
424
- await fs.unlink(reportPath);
425
- jsonResponse(res, { ok: true, file: fileName });
426
- } catch (err) {
427
- if (err?.code === 'ENOENT') jsonResponse(res, { error: true, message: 'Report not found' }, 404);
428
- else jsonResponse(res, { error: true, message: err.message }, 500);
429
- }
430
- return;
431
- }
432
-
433
- if (req.method === 'POST' && url.pathname === '/api/codewiki/generate') {
434
- if (bridge.isBusy()) {
435
- jsonResponse(res, { error: true, message: 'Runtime is busy' }, 409);
436
- return;
437
- }
438
- const { depth } = await readBody(req);
439
- const normalizedDepth = ['fast', 'standard', 'deep'].includes(String(depth || '').toLowerCase())
440
- ? String(depth).toLowerCase()
441
- : 'standard';
442
- const result = bridge.handleSubmit(`/project-requirements --${normalizedDepth}`);
443
- jsonResponse(res, result);
444
- return;
445
- }
446
-
447
- if (req.method === 'POST' && url.pathname === '/api/codewiki/ask') {
448
- const { question, reportFile } = await readBody(req);
449
- if (!question || typeof question !== 'string') {
450
- jsonResponse(res, { error: true, message: 'Missing "question" field' }, 400);
451
- return;
452
- }
453
- const selectedReport = isCodeWikiReportFile(reportFile) ? reportFile : '';
454
- if (bridge.isBusy()) {
455
- jsonResponse(res, { error: true, message: 'Runtime is busy' }, 409);
456
- return;
457
- }
458
- const reportPath = selectedReport
459
- ? path.join(getRequirementsDir(currentProjectDir), selectedReport)
460
- : getRequirementsDir(currentProjectDir);
461
- const prompt = buildCodeWikiAskPrompt({
462
- question,
463
- reportPath,
464
- projectDir: currentProjectDir,
465
- replyLanguage: bridge.getState()?.replyLanguage
466
- });
467
-
468
- res.writeHead(200, {
469
- 'Content-Type': 'application/x-ndjson; charset=utf-8',
470
- 'Cache-Control': 'no-cache, no-transform',
471
- 'X-Accel-Buffering': 'no'
472
- });
473
- const writeEvent = (event) => {
474
- try {
475
- res.write(`${JSON.stringify(event)}\n`);
476
- } catch {}
477
- };
478
- await bridge.handleCodeWikiAsk(prompt, writeEvent);
479
- res.end();
480
- return;
481
- }
482
-
483
- // ── Session management ──
484
- if (req.method === 'GET' && url.pathname === '/api/sessions') {
485
- const sessions = await listSessions(1000);
486
- jsonResponse(res, sessions);
487
- return;
488
- }
489
- if (req.method === 'POST' && url.pathname === '/api/sessions/new') {
490
- try {
491
- const currentMessages = bridge.getSessionMessages();
492
- if (!Array.isArray(currentMessages) || currentMessages.length === 0) {
493
- jsonResponse(res, {
494
- ok: true,
495
- reused: true,
496
- sessionId: bridge.getSessionId(),
497
- cwd: currentProjectDir
498
- });
499
- return;
500
- }
501
- const { runtime: newRuntime, session } = await buildRuntimeForSession({
502
- model: bridge.getState().model
503
- });
504
- await bridge.switchRuntime(newRuntime);
505
- currentProjectDir = process.cwd();
506
- jsonResponse(res, { ok: true, sessionId: session.id, cwd: currentProjectDir });
507
- } catch (err) {
508
- jsonResponse(res, { error: true, message: err.message }, 500);
509
- }
510
- return;
511
- }
512
- if (req.method === 'POST' && url.pathname === '/api/sessions/switch') {
513
- const { sessionId } = await readBody(req);
514
- if (!sessionId) { jsonResponse(res, { error: true, message: 'Missing sessionId' }, 400); return; }
515
- try {
516
- const { runtime: newRuntime } = await buildRuntimeForSession({
517
- sessionId,
518
- model: bridge.getState().model
519
- });
520
- await bridge.switchRuntime(newRuntime);
521
- currentProjectDir = process.cwd();
522
- jsonResponse(res, { ok: true, sessionId, cwd: currentProjectDir });
523
- } catch (err) {
524
- jsonResponse(res, { error: true, message: err.message }, 500);
525
- }
526
- return;
527
- }
528
-
529
- if (req.method === 'DELETE' && url.pathname.startsWith('/api/sessions/')) {
530
- const sessionId = decodeURIComponent(url.pathname.slice('/api/sessions/'.length));
531
- if (!sessionId) { jsonResponse(res, { error: true, message: 'Missing sessionId' }, 400); return; }
532
- const deletingCurrent = sessionId === bridge.getSessionId();
533
- if (deletingCurrent && bridge.isBusy()) {
534
- jsonResponse(res, { error: true, message: 'Current session is busy' }, 409);
535
- return;
536
- }
537
- try {
538
- const result = await deleteSession(sessionId);
539
- let nextSessionId = bridge.getSessionId();
540
- let cwd = currentProjectDir;
541
- if (deletingCurrent) {
542
- const remaining = await listSessions(1000);
543
- const next = remaining.find((session) => session.id !== sessionId);
544
- const built = next
545
- ? await buildRuntimeForSession({ sessionId: next.id, model: bridge.getState().model })
546
- : await buildRuntimeForSession({ model: bridge.getState().model });
547
- await bridge.switchRuntime(built.runtime);
548
- currentProjectDir = process.cwd();
549
- nextSessionId = built.session.id;
550
- cwd = currentProjectDir;
551
- }
552
- jsonResponse(res, { ok: true, removed: result.removed, sessionId: nextSessionId, cwd });
553
- } catch (err) {
554
- jsonResponse(res, { error: true, message: err.message }, 500);
555
- }
556
- return;
557
- }
558
-
559
- // ── Project management ──
560
- if (req.method === 'GET' && url.pathname === '/api/project') {
561
- jsonResponse(res, { cwd: currentProjectDir });
562
- return;
563
- }
564
- if (req.method === 'GET' && url.pathname === '/api/git') {
565
- try {
566
- const branch = execSync('git rev-parse --abbrev-ref HEAD', { cwd: currentProjectDir, encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'] }).trim();
567
- const porcelain = execSync('git status --porcelain', { cwd: currentProjectDir, encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'] }).trim();
568
- const lines = porcelain ? porcelain.split('\n') : [];
569
- let staged = 0, modified = 0, untracked = 0;
570
- for (const line of lines) {
571
- const x = line[0], y = line[1];
572
- if (x === '?' && y === '?') { untracked++; continue; }
573
- if (x !== ' ' && x !== '?') staged++;
574
- if (y === 'M' || y === 'D') modified++;
575
- }
576
- jsonResponse(res, { isGit: true, branch, dirty: lines.length > 0, staged, modified, untracked });
577
- } catch {
578
- jsonResponse(res, { isGit: false, branch: null, dirty: false, staged: 0, modified: 0, untracked: 0 });
579
- }
580
- return;
581
- }
582
- if (req.method === 'POST' && url.pathname === '/api/git-batch') {
583
- const { dirs } = await readBody(req);
584
- const result = {};
585
- for (const dir of (Array.isArray(dirs) ? dirs : [])) {
586
- try {
587
- const resolved = path.resolve(dir);
588
- const branch = execSync('git rev-parse --abbrev-ref HEAD', { cwd: resolved, encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'] }).trim();
589
- result[dir] = { isGit: true, branch };
590
- } catch {
591
- result[dir] = { isGit: false, branch: null };
592
- }
593
- }
594
- jsonResponse(res, result);
595
- return;
596
- }
597
- if (req.method === 'POST' && url.pathname === '/api/project/open') {
598
- const { path: projectPath } = await readBody(req);
599
- if (!projectPath) { jsonResponse(res, { error: true, message: 'Missing path' }, 400); return; }
600
- try {
601
- const resolved = path.resolve(projectPath);
602
- const stat = await fs.stat(resolved);
603
- if (!stat.isDirectory()) throw new Error('Not a directory');
604
- process.chdir(resolved);
605
- currentProjectDir = process.cwd();
606
- // Re-init runtime in new project
607
- const { runtime: newRuntime, session } = await buildRuntimeForSession({
608
- model: bridge.getState().model
609
- });
610
- await bridge.switchRuntime(newRuntime);
611
- jsonResponse(res, { ok: true, cwd: currentProjectDir, sessionId: session.id });
612
- } catch (err) {
613
- jsonResponse(res, { error: true, message: err.message }, 400);
614
- }
615
- return;
616
- }
617
- if (req.method === 'POST' && url.pathname === '/api/project/browse') {
618
- const { dir } = await readBody(req);
619
- const base = dir ? path.resolve(dir) : path.resolve('/');
620
- try {
621
- const entries = await fs.readdir(base, { withFileTypes: true });
622
- const dirs = entries
623
- .filter(e => e.isDirectory() && !e.name.startsWith('.'))
624
- .sort((a, b) => a.name.localeCompare(b.name))
625
- .map(e => ({
626
- name: e.name,
627
- path: path.join(base, e.name),
628
- isGit: false
629
- }));
630
- // Check for .git directories asynchronously
631
- await Promise.all(dirs.map(async (d) => {
632
- try { await fs.access(path.join(d.path, '.git')); d.isGit = true; } catch {}
633
- }));
634
- jsonResponse(res, { path: base, dirs });
635
- } catch (err) {
636
- jsonResponse(res, { path: base, dirs: [], error: err.message });
637
- }
638
- return;
639
- }
640
-
641
- // ── Config management ──
642
- if (req.method === 'GET' && url.pathname === '/api/config') {
643
- const config = await loadConfig();
644
- jsonResponse(res, config);
645
- return;
646
- }
647
- if (req.method === 'POST' && url.pathname === '/api/config/set') {
648
- const { key, value } = await readBody(req);
649
- if (!key) { jsonResponse(res, { error: true, message: 'Missing key' }, 400); return; }
650
- try {
651
- await setConfigValue(key, value);
652
- const config = await loadConfig();
653
- jsonResponse(res, { ok: true, config });
654
- } catch (err) {
655
- jsonResponse(res, { error: true, message: err.message }, 500);
656
- }
657
- return;
658
- }
659
- if (req.method === 'GET' && url.pathname.startsWith('/api/config/get/')) {
660
- const key = url.pathname.slice('/api/config/get/'.length);
661
- const value = await getConfigValue(key);
662
- jsonResponse(res, { key, value });
663
- return;
664
- }
665
-
666
- // ── Skills management ──
667
- if (req.method === 'GET' && url.pathname === '/api/skills') {
668
- try {
669
- const skills = await listSkillEntries({ scope: 'all' });
670
- jsonResponse(res, skills);
671
- } catch (err) { jsonResponse(res, { error: true, message: err.message }, 500); }
672
- return;
673
- }
674
- if (req.method === 'GET' && url.pathname.startsWith('/api/skills/') && url.pathname.endsWith('/content')) {
675
- const name = decodeURIComponent(url.pathname.slice('/api/skills/'.length, -'/content'.length));
676
- try {
677
- const entries = await listSkillEntries({ scope: 'all' });
678
- const skill = entries.find(s => s.name === name);
679
- if (!skill) { jsonResponse(res, { error: true, message: 'Skill not found' }, 404); return; }
680
- const content = await fs.readFile(skill.path, 'utf8');
681
- jsonResponse(res, { name: skill.name, content, scope: skill.scope });
682
- } catch (err) { jsonResponse(res, { error: true, message: err.message }, 500); }
683
- return;
684
- }
685
- if (req.method === 'POST' && url.pathname === '/api/skills/create') {
686
- const { name, description, content } = await readBody(req);
687
- if (!name || !content) { jsonResponse(res, { error: true, message: 'Missing name or content' }, 400); return; }
688
- try {
689
- const skillDir = path.join(getSkillsDir(), name);
690
- await fs.mkdir(skillDir, { recursive: true });
691
- const skillFile = path.join(skillDir, 'SKILL.md');
692
- await fs.writeFile(skillFile, content, 'utf8');
693
- const { createHash } = await import('node:crypto');
694
- const hash = createHash('sha256').update(content).digest('hex');
695
- await upsertSkillRegistryEntry(undefined, {
696
- name, version: '0.1.0', description: description || '', enabled: true,
697
- source: 'web-ui', entryFile: 'SKILL.md', sha256: hash, installedAt: new Date().toISOString()
698
- });
699
- const config = await loadConfig();
700
- config.skills = config.skills || {};
701
- config.skills.enabled = config.skills.enabled || {};
702
- config.skills.enabled[name] = true;
703
- await saveConfig(config);
704
- jsonResponse(res, { ok: true, name });
705
- } catch (err) { jsonResponse(res, { error: true, message: err.message }, 500); }
706
- return;
707
- }
708
- if (req.method === 'PUT' && url.pathname.startsWith('/api/skills/') && url.pathname.endsWith('/content')) {
709
- const name = decodeURIComponent(url.pathname.slice('/api/skills/'.length, -'/content'.length));
710
- const { content } = await readBody(req);
711
- if (!content) { jsonResponse(res, { error: true, message: 'Missing content' }, 400); return; }
712
- try {
713
- const entries = await listSkillEntries({ scope: 'all' });
714
- const skill = entries.find(s => s.name === name);
715
- if (!skill) { jsonResponse(res, { error: true, message: 'Skill not found' }, 404); return; }
716
- if (skill.scope === 'builtin') { jsonResponse(res, { error: true, message: 'Cannot edit builtin skill' }, 403); return; }
717
- await fs.writeFile(skill.path, content, 'utf8');
718
- jsonResponse(res, { ok: true });
719
- } catch (err) { jsonResponse(res, { error: true, message: err.message }, 500); }
720
- return;
721
- }
722
- if (req.method === 'DELETE' && url.pathname.startsWith('/api/skills/')) {
723
- const name = decodeURIComponent(url.pathname.slice('/api/skills/'.length));
724
- try {
725
- const entries = await listSkillEntries({ scope: 'all' });
726
- const skill = entries.find(s => s.name === name);
727
- if (!skill) { jsonResponse(res, { error: true, message: 'Skill not found' }, 404); return; }
728
- if (skill.scope === 'builtin') { jsonResponse(res, { error: true, message: 'Cannot delete builtin skill' }, 403); return; }
729
- const dir = path.dirname(skill.path);
730
- await fs.rm(dir, { recursive: true, force: true });
731
- const registry = await readSkillRegistry();
732
- registry.skills = (registry.skills || []).filter(s => s.name !== name);
733
- await writeSkillRegistry(undefined, registry);
734
- const config = await loadConfig();
735
- if (config.skills?.enabled) delete config.skills.enabled[name];
736
- await saveConfig(config);
737
- jsonResponse(res, { ok: true });
738
- } catch (err) { jsonResponse(res, { error: true, message: err.message }, 500); }
739
- return;
740
- }
741
- if (req.method === 'POST' && url.pathname.startsWith('/api/skills/') && url.pathname.endsWith('/toggle')) {
742
- const name = decodeURIComponent(url.pathname.slice('/api/skills/'.length, -'/toggle'.length));
743
- const { enabled } = await readBody(req);
744
- try {
745
- const entries = await listSkillEntries({ scope: 'all' });
746
- const skill = entries.find(s => s.name === name);
747
- if (!skill) { jsonResponse(res, { error: true, message: 'Skill not found' }, 404); return; }
748
- if (skill.scope === 'builtin') { jsonResponse(res, { error: true, message: 'Cannot toggle builtin skill' }, 403); return; }
749
- const config = await loadConfig();
750
- config.skills = config.skills || {};
751
- config.skills.enabled = config.skills.enabled || {};
752
- config.skills.enabled[name] = !!enabled;
753
- await saveConfig(config);
754
- const registry = await readSkillRegistry();
755
- const idx = registry.skills.findIndex(s => s.name === name);
756
- if (idx !== -1) { registry.skills[idx].enabled = !!enabled; await writeSkillRegistry(undefined, registry); }
757
- jsonResponse(res, { ok: true });
758
- } catch (err) { jsonResponse(res, { error: true, message: err.message }, 500); }
759
- return;
760
- }
761
-
762
- // ── Souls management ──
763
- const _BUNDLED_SOULS_DIR = path.resolve(__dirname, '..', 'souls');
764
- const _CUSTOM_SOULS_DIR = path.join(getBaseConfigDir(), 'souls');
765
-
766
- if (req.method === 'GET' && url.pathname === '/api/souls') {
767
- try {
768
- const config = await loadConfig();
769
- const activePreset = config?.soul?.preset || 'default';
770
- const souls = [];
771
- const bundledEntries = await fs.readdir(_BUNDLED_SOULS_DIR);
772
- for (const file of bundledEntries) {
773
- if (!file.endsWith('.md')) continue;
774
- const sname = file.slice(0, -3);
775
- const scontent = await fs.readFile(path.join(_BUNDLED_SOULS_DIR, file), 'utf8');
776
- souls.push({ name: sname, scope: 'builtin', preview: scontent.split('\n').slice(0, 3).join('\n').slice(0, 120), active: sname === activePreset });
777
- }
778
- try {
779
- const customEntries = await fs.readdir(_CUSTOM_SOULS_DIR);
780
- for (const file of customEntries) {
781
- if (!file.endsWith('.md')) continue;
782
- const sname = file.slice(0, -3);
783
- const scontent = await fs.readFile(path.join(_CUSTOM_SOULS_DIR, file), 'utf8');
784
- souls.push({ name: sname, scope: 'custom', preview: scontent.split('\n').slice(0, 3).join('\n').slice(0, 120), active: sname === activePreset });
785
- }
786
- } catch {}
787
- jsonResponse(res, souls);
788
- } catch (err) { jsonResponse(res, { error: true, message: err.message }, 500); }
789
- return;
790
- }
791
- if (req.method === 'GET' && url.pathname.startsWith('/api/souls/') && url.pathname.endsWith('/content')) {
792
- const sname = decodeURIComponent(url.pathname.slice('/api/souls/'.length, -'/content'.length));
793
- try {
794
- const customPath = path.join(_CUSTOM_SOULS_DIR, `${sname}.md`);
795
- try { const scontent = await fs.readFile(customPath, 'utf8'); jsonResponse(res, { name: sname, content: scontent, scope: 'custom' }); return; } catch {}
796
- const bundledPath = path.join(_BUNDLED_SOULS_DIR, `${sname}.md`);
797
- const scontent = await fs.readFile(bundledPath, 'utf8');
798
- jsonResponse(res, { name: sname, content: scontent, scope: 'builtin' });
799
- } catch (err) { jsonResponse(res, { error: true, message: err.message }, 500); }
800
- return;
801
- }
802
- if (req.method === 'POST' && url.pathname === '/api/souls/create') {
803
- const { name: rawName, content: soulContent } = await readBody(req);
804
- if (!rawName || !soulContent) { jsonResponse(res, { error: true, message: 'Missing name or content' }, 400); return; }
805
- try {
806
- const safeName = String(rawName).replace(/[^a-zA-Z0-9_-]/g, '');
807
- if (!safeName) { jsonResponse(res, { error: true, message: 'Invalid name' }, 400); return; }
808
- const bundledCheck = path.join(_BUNDLED_SOULS_DIR, `${safeName}.md`);
809
- try { await fs.access(bundledCheck); jsonResponse(res, { error: true, message: 'Name conflicts with builtin soul' }, 409); return; } catch {}
810
- await fs.mkdir(_CUSTOM_SOULS_DIR, { recursive: true });
811
- await fs.writeFile(path.join(_CUSTOM_SOULS_DIR, `${safeName}.md`), soulContent, 'utf8');
812
- jsonResponse(res, { ok: true, name: safeName });
813
- } catch (err) { jsonResponse(res, { error: true, message: err.message }, 500); }
814
- return;
815
- }
816
- if (req.method === 'PUT' && url.pathname.startsWith('/api/souls/') && url.pathname.endsWith('/content')) {
817
- const sname = decodeURIComponent(url.pathname.slice('/api/souls/'.length, -'/content'.length));
818
- const { content: soulContent } = await readBody(req);
819
- if (!soulContent) { jsonResponse(res, { error: true, message: 'Missing content' }, 400); return; }
820
- try {
821
- const customPath = path.join(_CUSTOM_SOULS_DIR, `${sname}.md`);
822
- try { await fs.access(customPath); } catch { jsonResponse(res, { error: true, message: 'Custom soul not found' }, 404); return; }
823
- await fs.writeFile(customPath, soulContent, 'utf8');
824
- jsonResponse(res, { ok: true });
825
- } catch (err) { jsonResponse(res, { error: true, message: err.message }, 500); }
826
- return;
827
- }
828
- if (req.method === 'DELETE' && url.pathname.startsWith('/api/souls/')) {
829
- const sname = decodeURIComponent(url.pathname.slice('/api/souls/'.length));
830
- try {
831
- const bundledPath = path.join(_BUNDLED_SOULS_DIR, `${sname}.md`);
832
- try { await fs.access(bundledPath); jsonResponse(res, { error: true, message: 'Cannot delete builtin soul' }, 403); return; } catch {}
833
- const customPath = path.join(_CUSTOM_SOULS_DIR, `${sname}.md`);
834
- await fs.unlink(customPath);
835
- const config = await loadConfig();
836
- if (config.soul?.preset === sname) { config.soul.preset = 'default'; await saveConfig(config); }
837
- jsonResponse(res, { ok: true });
838
- } catch (err) { jsonResponse(res, { error: true, message: err.message }, 500); }
839
- return;
840
- }
841
- if (req.method === 'POST' && url.pathname === '/api/souls/activate') {
842
- const { name: sname } = await readBody(req);
843
- if (!sname) { jsonResponse(res, { error: true, message: 'Missing name' }, 400); return; }
844
- try {
845
- const config = await loadConfig();
846
- config.soul = config.soul || {};
847
- config.soul.preset = sname;
848
- config.soul.custom_path = '';
849
- await saveConfig(config);
850
- jsonResponse(res, { ok: true });
851
- } catch (err) { jsonResponse(res, { error: true, message: err.message }, 500); }
852
- return;
853
- }
854
-
855
- res.writeHead(404, { 'Content-Type': 'text/plain' });
856
- res.end('Not found');
857
- });
858
-
859
- server.listen(args.port, () => {
860
- console.log(`\n CodeMini Web UI\n http://localhost:${args.port}\n Project: ${currentProjectDir}\n`);
861
- if (!args.open) return;
862
- const openCmd = process.platform === 'darwin' ? 'open' : process.platform === 'win32' ? 'start' : 'xdg-open';
863
- import('node:child_process').then(({ exec }) => {
864
- exec(`${openCmd} http://localhost:${args.port}`, (err) => { if (err) console.log(' Could not auto-open browser.'); });
865
- });
866
- });
867
-
868
- const cleanup = async () => {
869
- await bridge.dispose();
870
- server.close();
871
- process.exit(0);
872
- };
873
- process.on('SIGINT', cleanup);
874
- process.on('SIGTERM', cleanup);
875
- }
876
-
877
- main().catch((err) => {
878
- console.error('Failed to start:', err);
879
- process.exit(1);
880
- });
1
+ import http from 'node:http';
2
+ import fs from 'node:fs/promises';
3
+ import path from 'node:path';
4
+ import { fileURLToPath } from 'node:url';
5
+ import { execSync } from 'node:child_process';
6
+
7
+ import { loadConfig, saveConfig, setConfigValue, getConfigValue } from '../src/core/config-store.js';
8
+ import { createChatRuntime } from '../src/core/chat-runtime.js';
9
+ import { createSession, loadSession, listSessions, resolveSession, deleteSession } from '../src/core/session-store.js';
10
+ import { buildDefaultSystemPrompt } from '../src/core/default-system-prompt.js';
11
+ import { RuntimeBridge } from './lib/runtime-bridge.js';
12
+ import { listSkillEntries } from '../src/commands/skill.js';
13
+ import { readSkillRegistry, writeSkillRegistry, upsertSkillRegistryEntry } from '../src/core/skill-registry.js';
14
+ import { getReplyLanguage } from '../src/core/reply-language.js';
15
+ import { createRequire } from 'node:module';
16
+ const require = createRequire(import.meta.url);
17
+ const pkg = require('../package.json');
18
+ import { getSkillsDir, getBaseConfigDir } from '../src/core/paths.js';
19
+
20
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
21
+ const CLIENT_SOURCE_DIR = path.join(__dirname, 'client');
22
+ let CLIENT_DIR = CLIENT_SOURCE_DIR;
23
+ try {
24
+ const distDir = path.join(__dirname, 'dist');
25
+ const stat = await fs.stat(distDir);
26
+ if (stat.isDirectory()) CLIENT_DIR = distDir;
27
+ } catch {}
28
+
29
+ const MIME_TYPES = {
30
+ '.html': 'text/html; charset=utf-8',
31
+ '.css': 'text/css; charset=utf-8',
32
+ '.js': 'text/javascript; charset=utf-8',
33
+ '.json': 'application/json',
34
+ '.svg': 'image/svg+xml',
35
+ '.png': 'image/png',
36
+ '.ico': 'image/x-icon'
37
+ };
38
+
39
+ function parseArgs(argv) {
40
+ const parsed = { port: 3210, session: undefined, model: undefined, project: undefined, open: true };
41
+ for (let i = 2; i < argv.length; i++) {
42
+ const arg = argv[i];
43
+ if (arg === '--port' || arg === '-p') { parsed.port = parseInt(argv[++i], 10) || 3210; continue; }
44
+ if (arg === '--session' || arg === '-s') { parsed.session = argv[++i]; continue; }
45
+ if (arg === '--model' || arg === '-m') { parsed.model = argv[++i]; continue; }
46
+ if (arg === '--project' || arg === '-d') { parsed.project = argv[++i]; continue; }
47
+ if (arg === '--no-open') { parsed.open = false; continue; }
48
+ }
49
+ return parsed;
50
+ }
51
+
52
+ function readBody(req) {
53
+ return new Promise((resolve, reject) => {
54
+ const chunks = [];
55
+ req.on('data', (c) => chunks.push(c));
56
+ req.on('end', () => {
57
+ try { resolve(JSON.parse(Buffer.concat(chunks).toString())); }
58
+ catch { resolve({}); }
59
+ });
60
+ req.on('error', reject);
61
+ });
62
+ }
63
+
64
+ function jsonResponse(res, data, status = 200) {
65
+ const body = JSON.stringify(data);
66
+ res.writeHead(status, { 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(body) });
67
+ res.end(body);
68
+ }
69
+
70
+ function buildCodeWikiAskPrompt({ question, reportPath, projectDir, replyLanguage }) {
71
+ if (getReplyLanguage(replyLanguage) === 'en') {
72
+ return [
73
+ 'Answer the following question based on the current project and the CodeWiki / project-requirements HTML report.',
74
+ `Project path: ${projectDir}`,
75
+ `Report path: ${reportPath}`,
76
+ '',
77
+ 'Requirements:',
78
+ '- Prefer reading and citing the HTML report above.',
79
+ '- If the report is insufficient, use read-only project inspection to gather supporting evidence.',
80
+ '- Do not modify files, generate a new report, or write memory.',
81
+ '- Respond in English unless the user explicitly asks for another language.',
82
+ '',
83
+ `Question: ${question.trim()}`
84
+ ].join('\n');
85
+ }
86
+ return [
87
+ '请基于当前项目和 CodeWiki / project-requirements HTML 报告回答下面的问题。',
88
+ `项目路径:${projectDir}`,
89
+ `报告路径:${reportPath}`,
90
+ '',
91
+ '要求:',
92
+ '- 优先读取并参考上述 HTML 报告。',
93
+ '- 如果报告信息不足,可以只读检索项目文件补充证据。',
94
+ '- 不要修改文件,不要生成新报告,不要写入记忆。',
95
+ '- 除非用户明确要求其他语言,否则使用简体中文回答。',
96
+ '',
97
+ `问题:${question.trim()}`
98
+ ].join('\n');
99
+ }
100
+
101
+ async function serveStatic(res, filePath) {
102
+ const ext = path.extname(filePath);
103
+ const mime = MIME_TYPES[ext] || 'application/octet-stream';
104
+ try {
105
+ const data = await fs.readFile(filePath);
106
+ res.writeHead(200, { 'Content-Type': mime, 'Content-Length': data.length });
107
+ res.end(data);
108
+ } catch {
109
+ res.writeHead(404, { 'Content-Type': 'text/plain' });
110
+ res.end('Not found');
111
+ }
112
+ }
113
+
114
+ function normalizeProjectPath(value) {
115
+ const raw = String(value || '').trim();
116
+ if (!raw) return '';
117
+ const win = raw.match(/^([A-Za-z]):[\\/](.*)$/);
118
+ if (win && process.platform !== 'win32') {
119
+ return path.join('/mnt', win[1].toLowerCase(), win[2].replace(/[\\/]+/g, '/'));
120
+ }
121
+ return path.resolve(raw);
122
+ }
123
+
124
+ function tryParseJson(value) {
125
+ try { return JSON.parse(String(value || '')); } catch { return null; }
126
+ }
127
+
128
+ function collectSessionPathHints(session) {
129
+ const hints = [];
130
+ const messages = Array.isArray(session?.messages) ? session.messages : [];
131
+ for (const msg of messages) {
132
+ if (Array.isArray(msg?.tool_calls)) {
133
+ for (const call of msg.tool_calls) {
134
+ const args = tryParseJson(call?.function?.arguments ?? call?.arguments);
135
+ for (const key of ['path', 'file', 'filePath', 'cwd']) {
136
+ if (typeof args?.[key] === 'string') hints.push(args[key]);
137
+ }
138
+ }
139
+ }
140
+ const content = typeof msg?.content === 'string' ? msg.content : '';
141
+ for (const match of content.matchAll(/[A-Za-z]:[\\/][^\n\r"'`<>|]+/g)) hints.push(match[0]);
142
+ for (const match of content.matchAll(/\/mnt\/[A-Za-z]\/[^\n\r"'`<>|]+/g)) hints.push(match[0]);
143
+ }
144
+ return hints;
145
+ }
146
+
147
+ async function existingDirectoryForHint(rawHint) {
148
+ let candidate = normalizeProjectPath(rawHint);
149
+ if (!candidate) return '';
150
+ candidate = candidate.replace(/[),\].。;;:]+$/g, '');
151
+ for (let i = 0; i < 8 && candidate && candidate !== path.dirname(candidate); i += 1) {
152
+ try {
153
+ const stat = await fs.stat(candidate);
154
+ return stat.isDirectory() ? candidate : path.dirname(candidate);
155
+ } catch {
156
+ candidate = path.dirname(candidate);
157
+ }
158
+ }
159
+ return '';
160
+ }
161
+
162
+ const CODEWIKI_REPORT_RE = /^[^/\\]+-project-requirements\.html$/;
163
+
164
+ function getRequirementsDir(projectDir) {
165
+ return path.join(projectDir, 'docs', 'requirements');
166
+ }
167
+
168
+ function isCodeWikiReportFile(fileName) {
169
+ return CODEWIKI_REPORT_RE.test(String(fileName || ''));
170
+ }
171
+
172
+ function codeWikiReportTitle(fileName) {
173
+ return String(fileName || '')
174
+ .replace(/-project-requirements\.html$/, '')
175
+ .replace(/-/g, ' ');
176
+ }
177
+
178
+ function commonPathPrefix(paths) {
179
+ const normalized = paths.map((p) => path.resolve(p).split(path.sep).filter(Boolean));
180
+ if (!normalized.length) return '';
181
+ const prefix = [];
182
+ for (let i = 0; i < normalized[0].length; i += 1) {
183
+ const part = normalized[0][i];
184
+ if (normalized.every((parts) => parts[i] === part)) prefix.push(part);
185
+ else break;
186
+ }
187
+ if (!prefix.length) return path.parse(paths[0]).root || '';
188
+ return `${path.sep}${prefix.join(path.sep)}`;
189
+ }
190
+
191
+ async function inferSessionProjectDir(session) {
192
+ const explicit = normalizeProjectPath(session?.projectDir);
193
+ if (explicit) {
194
+ try {
195
+ if ((await fs.stat(explicit)).isDirectory()) return explicit;
196
+ } catch {}
197
+ }
198
+
199
+ const dirs = [];
200
+ for (const hint of collectSessionPathHints(session)) {
201
+ const dir = await existingDirectoryForHint(hint);
202
+ if (dir) dirs.push(dir);
203
+ }
204
+ if (dirs.length === 0) return '';
205
+
206
+ const common = commonPathPrefix(dirs);
207
+ let candidate = common;
208
+ while (candidate && candidate !== path.dirname(candidate)) {
209
+ try {
210
+ const stat = await fs.stat(candidate);
211
+ if (stat.isDirectory()) return candidate;
212
+ } catch {}
213
+ candidate = path.dirname(candidate);
214
+ }
215
+ return dirs[0];
216
+ }
217
+
218
+ async function buildRuntimeForSession({ sessionId, model, projectDir }) {
219
+ const config = await loadConfig();
220
+ const session = sessionId ? await loadSession(sessionId) : await createSession(projectDir || process.cwd());
221
+ const sessionProjectDir = projectDir ? normalizeProjectPath(projectDir) : await inferSessionProjectDir(session);
222
+ if (sessionProjectDir) {
223
+ try {
224
+ const stat = await fs.stat(sessionProjectDir);
225
+ if (stat.isDirectory()) process.chdir(sessionProjectDir);
226
+ } catch {}
227
+ }
228
+ session.projectDir = process.cwd();
229
+ const systemPrompt = buildDefaultSystemPrompt(config);
230
+ const runtime = await createChatRuntime({
231
+ session,
232
+ config,
233
+ model: model || config.model?.name,
234
+ systemPrompt
235
+ });
236
+ return { runtime, config, session, cwd: process.cwd() };
237
+ }
238
+
239
+ async function main() {
240
+ const args = parseArgs(process.argv);
241
+
242
+ // Set initial project directory
243
+ if (args.project) {
244
+ try {
245
+ const resolved = path.resolve(args.project);
246
+ process.chdir(resolved);
247
+ } catch {}
248
+ }
249
+
250
+ const { runtime: initialRuntime, config } = await buildRuntimeForSession({
251
+ sessionId: args.session,
252
+ model: args.model
253
+ });
254
+ let bridge = new RuntimeBridge(initialRuntime);
255
+ let currentProjectDir = process.cwd();
256
+
257
+ const server = http.createServer(async (req, res) => {
258
+ const url = new URL(req.url, `http://localhost:${args.port}`);
259
+
260
+ res.setHeader('Access-Control-Allow-Origin', '*');
261
+ res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS');
262
+ res.setHeader('Access-Control-Allow-Headers', 'Content-Type');
263
+ if (req.method === 'OPTIONS') { res.writeHead(204); res.end(); return; }
264
+
265
+ // SSE
266
+ if (url.pathname === '/api/events' && req.method === 'GET') {
267
+ bridge.addClient(res);
268
+ return;
269
+ }
270
+
271
+ // Static files
272
+ if (req.method === 'GET' && !url.pathname.startsWith('/api/')) {
273
+ let filePath;
274
+ if (url.pathname === '/') {
275
+ filePath = path.join(CLIENT_DIR, 'index.html');
276
+ } else {
277
+ const relative = url.pathname.replace(/^\//, '');
278
+ filePath = path.extname(relative)
279
+ ? path.join(CLIENT_DIR, relative)
280
+ : path.join(CLIENT_DIR, 'index.html');
281
+ }
282
+ if (!filePath.startsWith(CLIENT_DIR)) { res.writeHead(403); res.end(); return; }
283
+ await serveStatic(res, filePath);
284
+ return;
285
+ }
286
+
287
+ // ── Submit / Abort / Approval ──
288
+ if (req.method === 'POST' && url.pathname === '/api/submit') {
289
+ const { line, readOnlyCodeWiki } = await readBody(req);
290
+ if (!line || typeof line !== 'string') { jsonResponse(res, { error: true, message: 'Missing "line" field' }, 400); return; }
291
+ const result = bridge.handleSubmit(line, { readOnlyCodeWiki: readOnlyCodeWiki === true });
292
+ jsonResponse(res, result);
293
+ return;
294
+ }
295
+ if (req.method === 'POST' && url.pathname === '/api/abort') {
296
+ bridge.handleAbort();
297
+ jsonResponse(res, { ok: true });
298
+ return;
299
+ }
300
+ if (req.method === 'POST' && url.pathname === '/api/execution-mode') {
301
+ const { mode } = await readBody(req);
302
+ if (!mode || !['normal', 'auto', 'plan'].includes(mode)) {
303
+ jsonResponse(res, { error: true, message: 'Invalid mode' }, 400);
304
+ return;
305
+ }
306
+ if (bridge.isBusy()) {
307
+ jsonResponse(res, { error: true, message: 'Cannot switch execution mode while a request is running' }, 409);
308
+ return;
309
+ }
310
+ const ok = await bridge.setExecutionMode(mode);
311
+ jsonResponse(res, { ok });
312
+ return;
313
+ }
314
+ if (req.method === 'POST' && url.pathname === '/api/approval') {
315
+ const { id, approved } = await readBody(req);
316
+ jsonResponse(res, { ok: bridge.handleApproval(id, !!approved) });
317
+ return;
318
+ }
319
+
320
+ // ── Version ──
321
+ if (req.method === 'GET' && url.pathname === '/api/version') {
322
+ let latest = null;
323
+ try {
324
+ latest = execSync('npm view codemini-cli version', { encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'] }).trim();
325
+ } catch {}
326
+ jsonResponse(res, { current: pkg.version, latest });
327
+ return;
328
+ }
329
+ if (req.method === 'POST' && url.pathname === '/api/update') {
330
+ try {
331
+ const output = execSync('npm update -g codemini-cli', { encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'], timeout: 60000 });
332
+ jsonResponse(res, { ok: true, output: output.trim() });
333
+ } catch (err) {
334
+ jsonResponse(res, { ok: false, error: err.message }, 500);
335
+ }
336
+ return;
337
+ }
338
+
339
+ // ── Runtime state ──
340
+ if (req.method === 'GET' && url.pathname === '/api/state') {
341
+ jsonResponse(res, { ...bridge.getState(), cwd: currentProjectDir });
342
+ return;
343
+ }
344
+ if (req.method === 'GET' && url.pathname === '/api/completions') {
345
+ jsonResponse(res, bridge.getCompletions(url.searchParams.get('q') || ''));
346
+ return;
347
+ }
348
+ if (req.method === 'GET' && url.pathname === '/api/history') {
349
+ jsonResponse(res, bridge.getHistory());
350
+ return;
351
+ }
352
+ if (req.method === 'GET' && url.pathname === '/api/commands') {
353
+ jsonResponse(res, bridge.getCommands());
354
+ return;
355
+ }
356
+ if (req.method === 'GET' && url.pathname === '/api/startup-events') {
357
+ jsonResponse(res, await bridge.handleStartupEvents());
358
+ return;
359
+ }
360
+ if (req.method === 'GET' && url.pathname === '/api/session/messages') {
361
+ jsonResponse(res, { messages: bridge.getSessionMessages(), compact: bridge.getSessionCompactMeta() });
362
+ return;
363
+ }
364
+ if (req.method === 'GET' && url.pathname === '/api/session/ui-messages') {
365
+ jsonResponse(res, await bridge.getUiMessages());
366
+ return;
367
+ }
368
+
369
+ // ── CodeWiki / project requirements reports ──
370
+ if (req.method === 'GET' && url.pathname === '/api/codewiki/reports') {
371
+ const requirementsDir = getRequirementsDir(currentProjectDir);
372
+ try {
373
+ const entries = await fs.readdir(requirementsDir, { withFileTypes: true });
374
+ const reports = [];
375
+ for (const entry of entries) {
376
+ if (!entry.isFile() || !isCodeWikiReportFile(entry.name)) continue;
377
+ const reportPath = path.join(requirementsDir, entry.name);
378
+ const stat = await fs.stat(reportPath);
379
+ reports.push({
380
+ file: entry.name,
381
+ title: codeWikiReportTitle(entry.name),
382
+ size: stat.size,
383
+ mtime: stat.mtime.toISOString()
384
+ });
385
+ }
386
+ reports.sort((a, b) => new Date(b.mtime) - new Date(a.mtime));
387
+ jsonResponse(res, { reports });
388
+ } catch (err) {
389
+ if (err?.code === 'ENOENT') jsonResponse(res, { reports: [] });
390
+ else jsonResponse(res, { error: true, message: err.message }, 500);
391
+ }
392
+ return;
393
+ }
394
+
395
+ if (req.method === 'GET' && url.pathname.startsWith('/api/codewiki/report/')) {
396
+ const fileName = decodeURIComponent(url.pathname.slice('/api/codewiki/report/'.length));
397
+ if (!isCodeWikiReportFile(fileName)) {
398
+ jsonResponse(res, { error: true, message: 'Invalid report file' }, 400);
399
+ return;
400
+ }
401
+ const requirementsDir = path.resolve(getRequirementsDir(currentProjectDir));
402
+ const reportPath = path.resolve(requirementsDir, fileName);
403
+ if (!reportPath.startsWith(`${requirementsDir}${path.sep}`)) {
404
+ jsonResponse(res, { error: true, message: 'Invalid report path' }, 403);
405
+ return;
406
+ }
407
+ await serveStatic(res, reportPath);
408
+ return;
409
+ }
410
+
411
+ if (req.method === 'DELETE' && url.pathname.startsWith('/api/codewiki/report/')) {
412
+ const fileName = decodeURIComponent(url.pathname.slice('/api/codewiki/report/'.length));
413
+ if (!isCodeWikiReportFile(fileName)) {
414
+ jsonResponse(res, { error: true, message: 'Invalid report file' }, 400);
415
+ return;
416
+ }
417
+ const requirementsDir = path.resolve(getRequirementsDir(currentProjectDir));
418
+ const reportPath = path.resolve(requirementsDir, fileName);
419
+ if (!reportPath.startsWith(`${requirementsDir}${path.sep}`)) {
420
+ jsonResponse(res, { error: true, message: 'Invalid report path' }, 403);
421
+ return;
422
+ }
423
+ try {
424
+ await fs.unlink(reportPath);
425
+ jsonResponse(res, { ok: true, file: fileName });
426
+ } catch (err) {
427
+ if (err?.code === 'ENOENT') jsonResponse(res, { error: true, message: 'Report not found' }, 404);
428
+ else jsonResponse(res, { error: true, message: err.message }, 500);
429
+ }
430
+ return;
431
+ }
432
+
433
+ if (req.method === 'POST' && url.pathname === '/api/codewiki/generate') {
434
+ if (bridge.isBusy()) {
435
+ jsonResponse(res, { error: true, message: 'Runtime is busy' }, 409);
436
+ return;
437
+ }
438
+ const { depth } = await readBody(req);
439
+ const normalizedDepth = ['fast', 'standard', 'deep'].includes(String(depth || '').toLowerCase())
440
+ ? String(depth).toLowerCase()
441
+ : 'standard';
442
+ const result = bridge.handleSubmit(`/project-requirements --${normalizedDepth}`);
443
+ jsonResponse(res, result);
444
+ return;
445
+ }
446
+
447
+ if (req.method === 'POST' && url.pathname === '/api/codewiki/ask') {
448
+ const { question, reportFile } = await readBody(req);
449
+ if (!question || typeof question !== 'string') {
450
+ jsonResponse(res, { error: true, message: 'Missing "question" field' }, 400);
451
+ return;
452
+ }
453
+ const selectedReport = isCodeWikiReportFile(reportFile) ? reportFile : '';
454
+ if (bridge.isBusy()) {
455
+ jsonResponse(res, { error: true, message: 'Runtime is busy' }, 409);
456
+ return;
457
+ }
458
+ const reportPath = selectedReport
459
+ ? path.join(getRequirementsDir(currentProjectDir), selectedReport)
460
+ : getRequirementsDir(currentProjectDir);
461
+ const prompt = buildCodeWikiAskPrompt({
462
+ question,
463
+ reportPath,
464
+ projectDir: currentProjectDir,
465
+ replyLanguage: bridge.getState()?.replyLanguage
466
+ });
467
+
468
+ res.writeHead(200, {
469
+ 'Content-Type': 'application/x-ndjson; charset=utf-8',
470
+ 'Cache-Control': 'no-cache, no-transform',
471
+ 'X-Accel-Buffering': 'no'
472
+ });
473
+ const writeEvent = (event) => {
474
+ try {
475
+ res.write(`${JSON.stringify(event)}\n`);
476
+ } catch {}
477
+ };
478
+ await bridge.handleCodeWikiAsk(prompt, writeEvent);
479
+ res.end();
480
+ return;
481
+ }
482
+
483
+ // ── Session management ──
484
+ if (req.method === 'GET' && url.pathname === '/api/sessions') {
485
+ const sessions = await listSessions(1000);
486
+ jsonResponse(res, sessions);
487
+ return;
488
+ }
489
+ if (req.method === 'POST' && url.pathname === '/api/sessions/new') {
490
+ try {
491
+ const currentMessages = bridge.getSessionMessages();
492
+ if (!Array.isArray(currentMessages) || currentMessages.length === 0) {
493
+ jsonResponse(res, {
494
+ ok: true,
495
+ reused: true,
496
+ sessionId: bridge.getSessionId(),
497
+ cwd: currentProjectDir
498
+ });
499
+ return;
500
+ }
501
+ const { runtime: newRuntime, session } = await buildRuntimeForSession({
502
+ model: bridge.getState().model
503
+ });
504
+ await bridge.switchRuntime(newRuntime);
505
+ currentProjectDir = process.cwd();
506
+ jsonResponse(res, { ok: true, sessionId: session.id, cwd: currentProjectDir });
507
+ } catch (err) {
508
+ jsonResponse(res, { error: true, message: err.message }, 500);
509
+ }
510
+ return;
511
+ }
512
+ if (req.method === 'POST' && url.pathname === '/api/sessions/switch') {
513
+ const { sessionId } = await readBody(req);
514
+ if (!sessionId) { jsonResponse(res, { error: true, message: 'Missing sessionId' }, 400); return; }
515
+ try {
516
+ const { runtime: newRuntime } = await buildRuntimeForSession({
517
+ sessionId,
518
+ model: bridge.getState().model
519
+ });
520
+ await bridge.switchRuntime(newRuntime);
521
+ currentProjectDir = process.cwd();
522
+ jsonResponse(res, { ok: true, sessionId, cwd: currentProjectDir });
523
+ } catch (err) {
524
+ jsonResponse(res, { error: true, message: err.message }, 500);
525
+ }
526
+ return;
527
+ }
528
+
529
+ if (req.method === 'DELETE' && url.pathname.startsWith('/api/sessions/')) {
530
+ const sessionId = decodeURIComponent(url.pathname.slice('/api/sessions/'.length));
531
+ if (!sessionId) { jsonResponse(res, { error: true, message: 'Missing sessionId' }, 400); return; }
532
+ const deletingCurrent = sessionId === bridge.getSessionId();
533
+ if (deletingCurrent && bridge.isBusy()) {
534
+ jsonResponse(res, { error: true, message: 'Current session is busy' }, 409);
535
+ return;
536
+ }
537
+ try {
538
+ const result = await deleteSession(sessionId);
539
+ let nextSessionId = bridge.getSessionId();
540
+ let cwd = currentProjectDir;
541
+ if (deletingCurrent) {
542
+ const remaining = await listSessions(1000);
543
+ const next = remaining.find((session) => session.id !== sessionId);
544
+ const built = next
545
+ ? await buildRuntimeForSession({ sessionId: next.id, model: bridge.getState().model })
546
+ : await buildRuntimeForSession({ model: bridge.getState().model });
547
+ await bridge.switchRuntime(built.runtime);
548
+ currentProjectDir = process.cwd();
549
+ nextSessionId = built.session.id;
550
+ cwd = currentProjectDir;
551
+ }
552
+ jsonResponse(res, { ok: true, removed: result.removed, sessionId: nextSessionId, cwd });
553
+ } catch (err) {
554
+ jsonResponse(res, { error: true, message: err.message }, 500);
555
+ }
556
+ return;
557
+ }
558
+
559
+ // ── Project management ──
560
+ if (req.method === 'GET' && url.pathname === '/api/project') {
561
+ jsonResponse(res, { cwd: currentProjectDir });
562
+ return;
563
+ }
564
+ if (req.method === 'GET' && url.pathname === '/api/git') {
565
+ try {
566
+ const branch = execSync('git rev-parse --abbrev-ref HEAD', { cwd: currentProjectDir, encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'] }).trim();
567
+ const porcelain = execSync('git status --porcelain', { cwd: currentProjectDir, encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'] }).trim();
568
+ const lines = porcelain ? porcelain.split('\n') : [];
569
+ let staged = 0, modified = 0, untracked = 0;
570
+ for (const line of lines) {
571
+ const x = line[0], y = line[1];
572
+ if (x === '?' && y === '?') { untracked++; continue; }
573
+ if (x !== ' ' && x !== '?') staged++;
574
+ if (y === 'M' || y === 'D') modified++;
575
+ }
576
+ jsonResponse(res, { isGit: true, branch, dirty: lines.length > 0, staged, modified, untracked });
577
+ } catch {
578
+ jsonResponse(res, { isGit: false, branch: null, dirty: false, staged: 0, modified: 0, untracked: 0 });
579
+ }
580
+ return;
581
+ }
582
+ if (req.method === 'GET' && url.pathname === '/api/git-diff') {
583
+ try {
584
+ let patch;
585
+ try {
586
+ patch = execSync('git diff HEAD --no-color', { cwd: currentProjectDir, encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'] });
587
+ } catch {
588
+ patch = execSync('git diff --cached --no-color', { cwd: currentProjectDir, encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'] });
589
+ }
590
+ const patchFiles = [];
591
+ const seenPatchFiles = new Set();
592
+ for (const line of patch.split('\n')) {
593
+ const match = line.match(/^diff --git a\/(.+) b\/(.+)$/);
594
+ if (!match) continue;
595
+ const filePath = match[2] || match[1];
596
+ if (!filePath || seenPatchFiles.has(filePath)) continue;
597
+ seenPatchFiles.add(filePath);
598
+ patchFiles.push(filePath);
599
+ }
600
+ const porcelain = execSync('git status --porcelain', { cwd: currentProjectDir, encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'] }).trim();
601
+ const statusByPath = new Map();
602
+ if (porcelain) {
603
+ for (const line of porcelain.split('\n')) {
604
+ const x = line[0], y = line[1], filePath = line.slice(3);
605
+ let status;
606
+ if (x === '?' && y === '?') status = '?';
607
+ else if (x === 'A' || y === 'A') status = 'A';
608
+ else if (x === 'D' || y === 'D') status = 'D';
609
+ else status = 'M';
610
+ const staged = (x !== ' ' && x !== '?');
611
+ statusByPath.set(filePath, { path: filePath, status, staged });
612
+ }
613
+ }
614
+ const files = patchFiles.map(filePath => statusByPath.get(filePath) || { path: filePath, status: 'M', staged: false });
615
+ jsonResponse(res, { patch, files });
616
+ } catch {
617
+ jsonResponse(res, { patch: '', files: [] });
618
+ }
619
+ return;
620
+ }
621
+ if (req.method === 'POST' && url.pathname === '/api/git-batch') {
622
+ const { dirs } = await readBody(req);
623
+ const result = {};
624
+ for (const dir of (Array.isArray(dirs) ? dirs : [])) {
625
+ try {
626
+ const resolved = path.resolve(dir);
627
+ const branch = execSync('git rev-parse --abbrev-ref HEAD', { cwd: resolved, encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'] }).trim();
628
+ result[dir] = { isGit: true, branch };
629
+ } catch {
630
+ result[dir] = { isGit: false, branch: null };
631
+ }
632
+ }
633
+ jsonResponse(res, result);
634
+ return;
635
+ }
636
+ if (req.method === 'POST' && url.pathname === '/api/project/open') {
637
+ const { path: projectPath } = await readBody(req);
638
+ if (!projectPath) { jsonResponse(res, { error: true, message: 'Missing path' }, 400); return; }
639
+ try {
640
+ const resolved = path.resolve(projectPath);
641
+ const stat = await fs.stat(resolved);
642
+ if (!stat.isDirectory()) throw new Error('Not a directory');
643
+ process.chdir(resolved);
644
+ currentProjectDir = process.cwd();
645
+ // Re-init runtime in new project
646
+ const { runtime: newRuntime, session } = await buildRuntimeForSession({
647
+ model: bridge.getState().model
648
+ });
649
+ await bridge.switchRuntime(newRuntime);
650
+ jsonResponse(res, { ok: true, cwd: currentProjectDir, sessionId: session.id });
651
+ } catch (err) {
652
+ jsonResponse(res, { error: true, message: err.message }, 400);
653
+ }
654
+ return;
655
+ }
656
+ if (req.method === 'POST' && url.pathname === '/api/project/browse') {
657
+ const { dir } = await readBody(req);
658
+ const base = dir ? path.resolve(dir) : path.resolve('/');
659
+ try {
660
+ const entries = await fs.readdir(base, { withFileTypes: true });
661
+ const dirs = entries
662
+ .filter(e => e.isDirectory() && !e.name.startsWith('.'))
663
+ .sort((a, b) => a.name.localeCompare(b.name))
664
+ .map(e => ({
665
+ name: e.name,
666
+ path: path.join(base, e.name),
667
+ isGit: false
668
+ }));
669
+ // Check for .git directories asynchronously
670
+ await Promise.all(dirs.map(async (d) => {
671
+ try { await fs.access(path.join(d.path, '.git')); d.isGit = true; } catch {}
672
+ }));
673
+ jsonResponse(res, { path: base, dirs });
674
+ } catch (err) {
675
+ jsonResponse(res, { path: base, dirs: [], error: err.message });
676
+ }
677
+ return;
678
+ }
679
+
680
+ // ── Config management ──
681
+ if (req.method === 'GET' && url.pathname === '/api/config') {
682
+ const config = await loadConfig();
683
+ jsonResponse(res, config);
684
+ return;
685
+ }
686
+ if (req.method === 'POST' && url.pathname === '/api/config/set') {
687
+ const { key, value } = await readBody(req);
688
+ if (!key) { jsonResponse(res, { error: true, message: 'Missing key' }, 400); return; }
689
+ try {
690
+ await setConfigValue(key, value);
691
+ const config = await loadConfig();
692
+ jsonResponse(res, { ok: true, config });
693
+ } catch (err) {
694
+ jsonResponse(res, { error: true, message: err.message }, 500);
695
+ }
696
+ return;
697
+ }
698
+ if (req.method === 'GET' && url.pathname.startsWith('/api/config/get/')) {
699
+ const key = url.pathname.slice('/api/config/get/'.length);
700
+ const value = await getConfigValue(key);
701
+ jsonResponse(res, { key, value });
702
+ return;
703
+ }
704
+
705
+ // ── Skills management ──
706
+ if (req.method === 'GET' && url.pathname === '/api/skills') {
707
+ try {
708
+ const skills = await listSkillEntries({ scope: 'all' });
709
+ jsonResponse(res, skills);
710
+ } catch (err) { jsonResponse(res, { error: true, message: err.message }, 500); }
711
+ return;
712
+ }
713
+ if (req.method === 'GET' && url.pathname.startsWith('/api/skills/') && url.pathname.endsWith('/content')) {
714
+ const name = decodeURIComponent(url.pathname.slice('/api/skills/'.length, -'/content'.length));
715
+ try {
716
+ const entries = await listSkillEntries({ scope: 'all' });
717
+ const skill = entries.find(s => s.name === name);
718
+ if (!skill) { jsonResponse(res, { error: true, message: 'Skill not found' }, 404); return; }
719
+ const content = await fs.readFile(skill.path, 'utf8');
720
+ jsonResponse(res, { name: skill.name, content, scope: skill.scope });
721
+ } catch (err) { jsonResponse(res, { error: true, message: err.message }, 500); }
722
+ return;
723
+ }
724
+ if (req.method === 'POST' && url.pathname === '/api/skills/create') {
725
+ const { name, description, content } = await readBody(req);
726
+ if (!name || !content) { jsonResponse(res, { error: true, message: 'Missing name or content' }, 400); return; }
727
+ try {
728
+ const skillDir = path.join(getSkillsDir(), name);
729
+ await fs.mkdir(skillDir, { recursive: true });
730
+ const skillFile = path.join(skillDir, 'SKILL.md');
731
+ await fs.writeFile(skillFile, content, 'utf8');
732
+ const { createHash } = await import('node:crypto');
733
+ const hash = createHash('sha256').update(content).digest('hex');
734
+ await upsertSkillRegistryEntry(undefined, {
735
+ name, version: '0.1.0', description: description || '', enabled: true,
736
+ source: 'web-ui', entryFile: 'SKILL.md', sha256: hash, installedAt: new Date().toISOString()
737
+ });
738
+ const config = await loadConfig();
739
+ config.skills = config.skills || {};
740
+ config.skills.enabled = config.skills.enabled || {};
741
+ config.skills.enabled[name] = true;
742
+ await saveConfig(config);
743
+ jsonResponse(res, { ok: true, name });
744
+ } catch (err) { jsonResponse(res, { error: true, message: err.message }, 500); }
745
+ return;
746
+ }
747
+ if (req.method === 'PUT' && url.pathname.startsWith('/api/skills/') && url.pathname.endsWith('/content')) {
748
+ const name = decodeURIComponent(url.pathname.slice('/api/skills/'.length, -'/content'.length));
749
+ const { content } = await readBody(req);
750
+ if (!content) { jsonResponse(res, { error: true, message: 'Missing content' }, 400); return; }
751
+ try {
752
+ const entries = await listSkillEntries({ scope: 'all' });
753
+ const skill = entries.find(s => s.name === name);
754
+ if (!skill) { jsonResponse(res, { error: true, message: 'Skill not found' }, 404); return; }
755
+ if (skill.scope === 'builtin') { jsonResponse(res, { error: true, message: 'Cannot edit builtin skill' }, 403); return; }
756
+ await fs.writeFile(skill.path, content, 'utf8');
757
+ jsonResponse(res, { ok: true });
758
+ } catch (err) { jsonResponse(res, { error: true, message: err.message }, 500); }
759
+ return;
760
+ }
761
+ if (req.method === 'DELETE' && url.pathname.startsWith('/api/skills/')) {
762
+ const name = decodeURIComponent(url.pathname.slice('/api/skills/'.length));
763
+ try {
764
+ const entries = await listSkillEntries({ scope: 'all' });
765
+ const skill = entries.find(s => s.name === name);
766
+ if (!skill) { jsonResponse(res, { error: true, message: 'Skill not found' }, 404); return; }
767
+ if (skill.scope === 'builtin') { jsonResponse(res, { error: true, message: 'Cannot delete builtin skill' }, 403); return; }
768
+ const dir = path.dirname(skill.path);
769
+ await fs.rm(dir, { recursive: true, force: true });
770
+ const registry = await readSkillRegistry();
771
+ registry.skills = (registry.skills || []).filter(s => s.name !== name);
772
+ await writeSkillRegistry(undefined, registry);
773
+ const config = await loadConfig();
774
+ if (config.skills?.enabled) delete config.skills.enabled[name];
775
+ await saveConfig(config);
776
+ jsonResponse(res, { ok: true });
777
+ } catch (err) { jsonResponse(res, { error: true, message: err.message }, 500); }
778
+ return;
779
+ }
780
+ if (req.method === 'POST' && url.pathname.startsWith('/api/skills/') && url.pathname.endsWith('/toggle')) {
781
+ const name = decodeURIComponent(url.pathname.slice('/api/skills/'.length, -'/toggle'.length));
782
+ const { enabled } = await readBody(req);
783
+ try {
784
+ const entries = await listSkillEntries({ scope: 'all' });
785
+ const skill = entries.find(s => s.name === name);
786
+ if (!skill) { jsonResponse(res, { error: true, message: 'Skill not found' }, 404); return; }
787
+ if (skill.scope === 'builtin') { jsonResponse(res, { error: true, message: 'Cannot toggle builtin skill' }, 403); return; }
788
+ const config = await loadConfig();
789
+ config.skills = config.skills || {};
790
+ config.skills.enabled = config.skills.enabled || {};
791
+ config.skills.enabled[name] = !!enabled;
792
+ await saveConfig(config);
793
+ const registry = await readSkillRegistry();
794
+ const idx = registry.skills.findIndex(s => s.name === name);
795
+ if (idx !== -1) { registry.skills[idx].enabled = !!enabled; await writeSkillRegistry(undefined, registry); }
796
+ jsonResponse(res, { ok: true });
797
+ } catch (err) { jsonResponse(res, { error: true, message: err.message }, 500); }
798
+ return;
799
+ }
800
+
801
+ // ── Souls management ──
802
+ const _BUNDLED_SOULS_DIR = path.resolve(__dirname, '..', 'souls');
803
+ const _CUSTOM_SOULS_DIR = path.join(getBaseConfigDir(), 'souls');
804
+
805
+ if (req.method === 'GET' && url.pathname === '/api/souls') {
806
+ try {
807
+ const config = await loadConfig();
808
+ const activePreset = config?.soul?.preset || 'default';
809
+ const souls = [];
810
+ const bundledEntries = await fs.readdir(_BUNDLED_SOULS_DIR);
811
+ for (const file of bundledEntries) {
812
+ if (!file.endsWith('.md')) continue;
813
+ const sname = file.slice(0, -3);
814
+ const scontent = await fs.readFile(path.join(_BUNDLED_SOULS_DIR, file), 'utf8');
815
+ souls.push({ name: sname, scope: 'builtin', preview: scontent.split('\n').slice(0, 3).join('\n').slice(0, 120), active: sname === activePreset });
816
+ }
817
+ try {
818
+ const customEntries = await fs.readdir(_CUSTOM_SOULS_DIR);
819
+ for (const file of customEntries) {
820
+ if (!file.endsWith('.md')) continue;
821
+ const sname = file.slice(0, -3);
822
+ const scontent = await fs.readFile(path.join(_CUSTOM_SOULS_DIR, file), 'utf8');
823
+ souls.push({ name: sname, scope: 'custom', preview: scontent.split('\n').slice(0, 3).join('\n').slice(0, 120), active: sname === activePreset });
824
+ }
825
+ } catch {}
826
+ jsonResponse(res, souls);
827
+ } catch (err) { jsonResponse(res, { error: true, message: err.message }, 500); }
828
+ return;
829
+ }
830
+ if (req.method === 'GET' && url.pathname.startsWith('/api/souls/') && url.pathname.endsWith('/content')) {
831
+ const sname = decodeURIComponent(url.pathname.slice('/api/souls/'.length, -'/content'.length));
832
+ try {
833
+ const customPath = path.join(_CUSTOM_SOULS_DIR, `${sname}.md`);
834
+ try { const scontent = await fs.readFile(customPath, 'utf8'); jsonResponse(res, { name: sname, content: scontent, scope: 'custom' }); return; } catch {}
835
+ const bundledPath = path.join(_BUNDLED_SOULS_DIR, `${sname}.md`);
836
+ const scontent = await fs.readFile(bundledPath, 'utf8');
837
+ jsonResponse(res, { name: sname, content: scontent, scope: 'builtin' });
838
+ } catch (err) { jsonResponse(res, { error: true, message: err.message }, 500); }
839
+ return;
840
+ }
841
+ if (req.method === 'POST' && url.pathname === '/api/souls/create') {
842
+ const { name: rawName, content: soulContent } = await readBody(req);
843
+ if (!rawName || !soulContent) { jsonResponse(res, { error: true, message: 'Missing name or content' }, 400); return; }
844
+ try {
845
+ const safeName = String(rawName).replace(/[^a-zA-Z0-9_-]/g, '');
846
+ if (!safeName) { jsonResponse(res, { error: true, message: 'Invalid name' }, 400); return; }
847
+ const bundledCheck = path.join(_BUNDLED_SOULS_DIR, `${safeName}.md`);
848
+ try { await fs.access(bundledCheck); jsonResponse(res, { error: true, message: 'Name conflicts with builtin soul' }, 409); return; } catch {}
849
+ await fs.mkdir(_CUSTOM_SOULS_DIR, { recursive: true });
850
+ await fs.writeFile(path.join(_CUSTOM_SOULS_DIR, `${safeName}.md`), soulContent, 'utf8');
851
+ jsonResponse(res, { ok: true, name: safeName });
852
+ } catch (err) { jsonResponse(res, { error: true, message: err.message }, 500); }
853
+ return;
854
+ }
855
+ if (req.method === 'PUT' && url.pathname.startsWith('/api/souls/') && url.pathname.endsWith('/content')) {
856
+ const sname = decodeURIComponent(url.pathname.slice('/api/souls/'.length, -'/content'.length));
857
+ const { content: soulContent } = await readBody(req);
858
+ if (!soulContent) { jsonResponse(res, { error: true, message: 'Missing content' }, 400); return; }
859
+ try {
860
+ const customPath = path.join(_CUSTOM_SOULS_DIR, `${sname}.md`);
861
+ try { await fs.access(customPath); } catch { jsonResponse(res, { error: true, message: 'Custom soul not found' }, 404); return; }
862
+ await fs.writeFile(customPath, soulContent, 'utf8');
863
+ jsonResponse(res, { ok: true });
864
+ } catch (err) { jsonResponse(res, { error: true, message: err.message }, 500); }
865
+ return;
866
+ }
867
+ if (req.method === 'DELETE' && url.pathname.startsWith('/api/souls/')) {
868
+ const sname = decodeURIComponent(url.pathname.slice('/api/souls/'.length));
869
+ try {
870
+ const bundledPath = path.join(_BUNDLED_SOULS_DIR, `${sname}.md`);
871
+ try { await fs.access(bundledPath); jsonResponse(res, { error: true, message: 'Cannot delete builtin soul' }, 403); return; } catch {}
872
+ const customPath = path.join(_CUSTOM_SOULS_DIR, `${sname}.md`);
873
+ await fs.unlink(customPath);
874
+ const config = await loadConfig();
875
+ if (config.soul?.preset === sname) { config.soul.preset = 'default'; await saveConfig(config); }
876
+ jsonResponse(res, { ok: true });
877
+ } catch (err) { jsonResponse(res, { error: true, message: err.message }, 500); }
878
+ return;
879
+ }
880
+ if (req.method === 'POST' && url.pathname === '/api/souls/activate') {
881
+ const { name: sname } = await readBody(req);
882
+ if (!sname) { jsonResponse(res, { error: true, message: 'Missing name' }, 400); return; }
883
+ try {
884
+ const config = await loadConfig();
885
+ config.soul = config.soul || {};
886
+ config.soul.preset = sname;
887
+ config.soul.custom_path = '';
888
+ await saveConfig(config);
889
+ jsonResponse(res, { ok: true });
890
+ } catch (err) { jsonResponse(res, { error: true, message: err.message }, 500); }
891
+ return;
892
+ }
893
+
894
+ res.writeHead(404, { 'Content-Type': 'text/plain' });
895
+ res.end('Not found');
896
+ });
897
+
898
+ server.listen(args.port, () => {
899
+ console.log(`\n Codemini Web UI\n http://localhost:${args.port}\n Project: ${currentProjectDir}\n`);
900
+ if (!args.open) return;
901
+ const openCmd = process.platform === 'darwin' ? 'open' : process.platform === 'win32' ? 'start' : 'xdg-open';
902
+ import('node:child_process').then(({ exec }) => {
903
+ exec(`${openCmd} http://localhost:${args.port}`, (err) => { if (err) console.log(' Could not auto-open browser.'); });
904
+ });
905
+ });
906
+
907
+ const cleanup = async () => {
908
+ await bridge.dispose();
909
+ server.close();
910
+ process.exit(0);
911
+ };
912
+ process.on('SIGINT', cleanup);
913
+ process.on('SIGTERM', cleanup);
914
+ }
915
+
916
+ main().catch((err) => {
917
+ console.error('Failed to start:', err);
918
+ process.exit(1);
919
+ });