@ayushporwal/coja 0.0.1

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 (455) hide show
  1. package/bin/coja.js +2 -0
  2. package/dist/ai/chat-handler.js +115 -0
  3. package/dist/ai/chats.js +92 -0
  4. package/dist/ai/history.js +70 -0
  5. package/dist/ai/prompt.js +73 -0
  6. package/dist/ai/providers.js +81 -0
  7. package/dist/ai/title.js +77 -0
  8. package/dist/ai/tools.js +495 -0
  9. package/dist/ai/types.js +22 -0
  10. package/dist/app.js +142 -0
  11. package/dist/cli.js +99 -0
  12. package/dist/codex/connection.js +293 -0
  13. package/dist/codex/errors.js +52 -0
  14. package/dist/codex/models.js +97 -0
  15. package/dist/codex/oauth.js +217 -0
  16. package/dist/codex/protocol.js +49 -0
  17. package/dist/codex/provider.js +209 -0
  18. package/dist/context.js +8 -0
  19. package/dist/custom-providers.js +134 -0
  20. package/dist/data-dir.js +25 -0
  21. package/dist/db.js +65 -0
  22. package/dist/forge/forge.js +11 -0
  23. package/dist/forge/gh-cli.js +99 -0
  24. package/dist/forge/github.js +627 -0
  25. package/dist/forge/graphql.js +106 -0
  26. package/dist/forge/queries.js +258 -0
  27. package/dist/git/plumbing.js +519 -0
  28. package/dist/git/remote.js +47 -0
  29. package/dist/git/run.js +256 -0
  30. package/dist/open-browser.js +27 -0
  31. package/dist/paths.js +18 -0
  32. package/dist/pr/fetcher.js +173 -0
  33. package/dist/pr/objects.js +83 -0
  34. package/dist/pr/refs.js +17 -0
  35. package/dist/projects/add.js +111 -0
  36. package/dist/projects/store.js +37 -0
  37. package/dist/routes/chat.js +187 -0
  38. package/dist/routes/fs.js +65 -0
  39. package/dist/routes/git.js +75 -0
  40. package/dist/routes/guard.js +96 -0
  41. package/dist/routes/http.js +35 -0
  42. package/dist/routes/projects.js +51 -0
  43. package/dist/routes/pulls.js +64 -0
  44. package/dist/routes/review.js +113 -0
  45. package/dist/routes/setup.js +285 -0
  46. package/dist/secrets/providers.js +27 -0
  47. package/dist/secrets/store.js +190 -0
  48. package/dist/server.js +51 -0
  49. package/dist/services.js +15 -0
  50. package/dist/shared/api.js +186 -0
  51. package/package.json +35 -0
  52. package/public/assets/abap-CLvhMVsD.js +1 -0
  53. package/public/assets/actionscript-3-Cs6JJXT5.js +1 -0
  54. package/public/assets/ada-C5qYipkI.js +1 -0
  55. package/public/assets/ahk-CA9gdOdF.js +1 -0
  56. package/public/assets/ahk2-_6rzW9BS.js +1 -0
  57. package/public/assets/andromeeda-vGVdxbeo.js +1 -0
  58. package/public/assets/angular-html-BvGMhoM4.js +1 -0
  59. package/public/assets/angular-ts-D4cJlfHk.js +1 -0
  60. package/public/assets/apache-U0d_L8uA.js +1 -0
  61. package/public/assets/apex-KFGFyKwG.js +1 -0
  62. package/public/assets/apl-BAzDJNhF.js +1 -0
  63. package/public/assets/applescript-CCn79oCD.js +1 -0
  64. package/public/assets/ara-4CJ0cIlV.js +1 -0
  65. package/public/assets/asciidoc-SCjQUq34.js +1 -0
  66. package/public/assets/asm-Cmm7eHzH.js +1 -0
  67. package/public/assets/astro-tbl65Kw6.js +1 -0
  68. package/public/assets/aurora-x-CDeNXAV0.js +1 -0
  69. package/public/assets/awk-BWXHIvNe.js +1 -0
  70. package/public/assets/ayu-dark-DluEY0Gj.js +1 -0
  71. package/public/assets/ayu-light-C3h-C4tm.js +1 -0
  72. package/public/assets/ayu-mirage-Bqwy1Gya.js +1 -0
  73. package/public/assets/ballerina-B7ZEbQpA.js +1 -0
  74. package/public/assets/bat-qdxAdBWQ.js +1 -0
  75. package/public/assets/beancount-D-usSTwE.js +1 -0
  76. package/public/assets/berry-DKpUyyne.js +1 -0
  77. package/public/assets/bibtex-Ci_nEsc7.js +1 -0
  78. package/public/assets/bicep-CUHmPFLl.js +1 -0
  79. package/public/assets/bird2-CYIUgYqB.js +1 -0
  80. package/public/assets/blade-Cy5u_P69.js +1 -0
  81. package/public/assets/bsl-_VjlB9mv.js +1 -0
  82. package/public/assets/c-xsYtGME5.js +1 -0
  83. package/public/assets/c3-D0apG41V.js +1 -0
  84. package/public/assets/cadence-CQ2zXKGN.js +1 -0
  85. package/public/assets/cairo-DLTphjLi.js +1 -0
  86. package/public/assets/catppuccin-frappe-C9Q5zVZY.js +1 -0
  87. package/public/assets/catppuccin-latte-CtsyrtPq.js +1 -0
  88. package/public/assets/catppuccin-macchiato-R8nAf1vR.js +1 -0
  89. package/public/assets/catppuccin-mocha-DjuaAJKi.js +1 -0
  90. package/public/assets/chapel-DlXZg0HF.js +1 -0
  91. package/public/assets/clarity-CtfBBspg.js +1 -0
  92. package/public/assets/clojure-DqKBuwfJ.js +1 -0
  93. package/public/assets/cmake-Bj61d0ZC.js +1 -0
  94. package/public/assets/cobol-NNONwiOX.js +1 -0
  95. package/public/assets/codeowners-C8r90Shi.js +1 -0
  96. package/public/assets/codeql-oeQT6MSM.js +1 -0
  97. package/public/assets/coffee-O853MBTp.js +1 -0
  98. package/public/assets/common-lisp-Cv5bFMCO.js +1 -0
  99. package/public/assets/coq-DuBRPxYg.js +1 -0
  100. package/public/assets/cpp-DtWY3seq.js +1 -0
  101. package/public/assets/crystal-CcZ3zkyx.js +1 -0
  102. package/public/assets/csharp-oqKa8noW.js +1 -0
  103. package/public/assets/css-mSNQLTOb.js +1 -0
  104. package/public/assets/csv-Dx-8-gkx.js +1 -0
  105. package/public/assets/cue-CE9AQfxI.js +1 -0
  106. package/public/assets/cypher-ClKdZ_lG.js +1 -0
  107. package/public/assets/d-qD-0Kul2.js +1 -0
  108. package/public/assets/dark-plus-Cs2F2srj.js +1 -0
  109. package/public/assets/dart-CnvKMtbv.js +1 -0
  110. package/public/assets/dax-BkyTk9wS.js +1 -0
  111. package/public/assets/desktop-Dlh5hvp9.js +1 -0
  112. package/public/assets/diff-woXpYk--.js +1 -0
  113. package/public/assets/docker-IyjqRm3v.js +1 -0
  114. package/public/assets/dotenv-_5a1GRtc.js +1 -0
  115. package/public/assets/dracula-BHWKrbxM.js +1 -0
  116. package/public/assets/dracula-soft-5eyTD99u.js +1 -0
  117. package/public/assets/dream-maker-DW3nJb8Q.js +1 -0
  118. package/public/assets/edge-BA0HOmx0.js +1 -0
  119. package/public/assets/elixir-Buj0VxhK.js +1 -0
  120. package/public/assets/elm-lVH2iZsu.js +1 -0
  121. package/public/assets/emacs-lisp-c_oH4hRZ.js +1 -0
  122. package/public/assets/erb-CbQPls-2.js +1 -0
  123. package/public/assets/erlang-Cphh6RMH.js +1 -0
  124. package/public/assets/everforest-dark-sB-x3p7T.js +1 -0
  125. package/public/assets/everforest-light-Df2xbC6M.js +1 -0
  126. package/public/assets/fennel-DQxkIbk2.js +1 -0
  127. package/public/assets/fish-BJitypiv.js +1 -0
  128. package/public/assets/fluent-C03EYrpw.js +1 -0
  129. package/public/assets/fortran-fixed-form-DEKoE2YW.js +1 -0
  130. package/public/assets/fortran-free-form-CYNrtFtB.js +1 -0
  131. package/public/assets/fsharp-D13ZGOAj.js +1 -0
  132. package/public/assets/gdresource-xMC7jr7x.js +1 -0
  133. package/public/assets/gdscript-DYvRD3hg.js +1 -0
  134. package/public/assets/gdshader-CBce3t8t.js +1 -0
  135. package/public/assets/genie-CV2tkWYe.js +1 -0
  136. package/public/assets/gherkin-DExj1W_8.js +1 -0
  137. package/public/assets/git-commit-BSykSTBG.js +1 -0
  138. package/public/assets/git-rebase-DidgtTB0.js +1 -0
  139. package/public/assets/github-dark-C-LZuMrd.js +1 -0
  140. package/public/assets/github-dark-default-DXG-b-1a.js +1 -0
  141. package/public/assets/github-dark-dimmed-Bx1FflLF.js +1 -0
  142. package/public/assets/github-dark-high-contrast-B_tTalzw.js +1 -0
  143. package/public/assets/github-light-EUqPIrTm.js +1 -0
  144. package/public/assets/github-light-default-BXViO-2h.js +1 -0
  145. package/public/assets/github-light-high-contrast-B68TUdTA.js +1 -0
  146. package/public/assets/gleam-CSRkHgEL.js +1 -0
  147. package/public/assets/glimmer-js-Cck5MdS-.js +1 -0
  148. package/public/assets/glimmer-ts-DMfQxyYr.js +1 -0
  149. package/public/assets/glsl-DPBFrb1h.js +1 -0
  150. package/public/assets/gn-ilITqXS6.js +1 -0
  151. package/public/assets/gnuplot-7GGW24-e.js +1 -0
  152. package/public/assets/go-rLFTqkRN.js +1 -0
  153. package/public/assets/graphql-BvPubeCr.js +1 -0
  154. package/public/assets/groovy-CacY0gHj.js +1 -0
  155. package/public/assets/gruvbox-dark-hard-C820rvS2.js +1 -0
  156. package/public/assets/gruvbox-dark-medium-BPjhmG05.js +1 -0
  157. package/public/assets/gruvbox-dark-soft-MrdJrrXF.js +1 -0
  158. package/public/assets/gruvbox-light-hard-BC_s9l72.js +1 -0
  159. package/public/assets/gruvbox-light-medium-BAWPOn9u.js +1 -0
  160. package/public/assets/gruvbox-light-soft-BSMLrYjP.js +1 -0
  161. package/public/assets/hack-Drv5y81z.js +1 -0
  162. package/public/assets/haml-DcpINEk-.js +1 -0
  163. package/public/assets/handlebars-sj4UAwHW.js +1 -0
  164. package/public/assets/haskell-D8IpX4py.js +1 -0
  165. package/public/assets/haxe-C8oCnwJJ.js +1 -0
  166. package/public/assets/hcl-Dh228itO.js +1 -0
  167. package/public/assets/hjson-CxZEssPk.js +1 -0
  168. package/public/assets/hlsl-Cvrh5tZx.js +1 -0
  169. package/public/assets/horizon-CE9ld1lL.js +1 -0
  170. package/public/assets/horizon-bright-DSNQnXHK.js +1 -0
  171. package/public/assets/houston-CsvMBhTu.js +1 -0
  172. package/public/assets/html-DQjDPZbg.js +1 -0
  173. package/public/assets/html-derivative-DscSYlel.js +1 -0
  174. package/public/assets/http-CCrfmrUy.js +1 -0
  175. package/public/assets/hurl-DLm6Xdy2.js +1 -0
  176. package/public/assets/hxml-Dcm88srW.js +1 -0
  177. package/public/assets/hy-CZbG8q4J.js +1 -0
  178. package/public/assets/imba-DsUTQ-LC.js +1 -0
  179. package/public/assets/index-BDRMh7Hy.js +2918 -0
  180. package/public/assets/index-Cw-82hD0.css +1 -0
  181. package/public/assets/ini-B5eOa1yu.js +1 -0
  182. package/public/assets/java-CzpiG9dn.js +1 -0
  183. package/public/assets/javascript-2eedadNh.js +1 -0
  184. package/public/assets/jinja-Cch1Cycs.js +1 -0
  185. package/public/assets/jison-C3DQW9A-.js +1 -0
  186. package/public/assets/json-BhBCwuxI.js +1 -0
  187. package/public/assets/json5-BR5RXkoi.js +1 -0
  188. package/public/assets/jsonc-CYpm1nAK.js +1 -0
  189. package/public/assets/jsonl-CmCQp5Yx.js +1 -0
  190. package/public/assets/jsonnet-CJTPZ8u_.js +1 -0
  191. package/public/assets/jssm-DXw9l8Rf.js +1 -0
  192. package/public/assets/jsx-De1vU88o.js +1 -0
  193. package/public/assets/julia-BlJzw_Rq.js +1 -0
  194. package/public/assets/just-A2qsO4cM.js +1 -0
  195. package/public/assets/kanagawa-dragon-Bozd7klA.js +1 -0
  196. package/public/assets/kanagawa-lotus-BmuOG9ZW.js +1 -0
  197. package/public/assets/kanagawa-wave-BML5mF2J.js +1 -0
  198. package/public/assets/kdl-CsD5j6eV.js +1 -0
  199. package/public/assets/kotlin-DhhofPvG.js +1 -0
  200. package/public/assets/kusto-C7mF5XQf.js +1 -0
  201. package/public/assets/laserwave-C_8bwKvT.js +1 -0
  202. package/public/assets/latex-CDaOJk0y.js +1 -0
  203. package/public/assets/lean-CewbzKMR.js +1 -0
  204. package/public/assets/less-DVTAwKKz.js +1 -0
  205. package/public/assets/light-plus-DVQuIRkW.js +1 -0
  206. package/public/assets/liquid-BOnT7KYX.js +1 -0
  207. package/public/assets/llvm-DqHOMS-s.js +1 -0
  208. package/public/assets/log-BNLmms1o.js +1 -0
  209. package/public/assets/logo-Cluzi2Zq.js +1 -0
  210. package/public/assets/lottie-BT9QSO-G.js +2 -0
  211. package/public/assets/lua-CoLL9qBE.js +1 -0
  212. package/public/assets/luau-C2pBCSgt.js +1 -0
  213. package/public/assets/make-Dixweg8N.js +1 -0
  214. package/public/assets/markdown-BYOwaDjH.js +1 -0
  215. package/public/assets/marko-D8Xuri3Y.js +1 -0
  216. package/public/assets/material-theme-Bm3Qr25_.js +1 -0
  217. package/public/assets/material-theme-darker-2IIEA8gg.js +1 -0
  218. package/public/assets/material-theme-lighter-uhdI0v04.js +1 -0
  219. package/public/assets/material-theme-ocean-CHQ94UKr.js +1 -0
  220. package/public/assets/material-theme-palenight-B5W6OYN7.js +1 -0
  221. package/public/assets/matlab-D7qyCx1q.js +1 -0
  222. package/public/assets/mdc-BhVsbv7E.js +1 -0
  223. package/public/assets/mdx-DQZ5AkYe.js +1 -0
  224. package/public/assets/mermaid-BxpfTIzt.js +1 -0
  225. package/public/assets/min-dark-BSWPekZh.js +1 -0
  226. package/public/assets/min-light-DDpmG2fV.js +1 -0
  227. package/public/assets/mipsasm-BMqwQI7S.js +1 -0
  228. package/public/assets/mojo-fg40_ntQ.js +1 -0
  229. package/public/assets/monokai-B_bV9PQ9.js +1 -0
  230. package/public/assets/moonbit-DgjMoLWr.js +1 -0
  231. package/public/assets/move-BYnGhZoM.js +1 -0
  232. package/public/assets/narrat-_X_XdTYD.js +1 -0
  233. package/public/assets/nextflow-BJtWHP5T.js +1 -0
  234. package/public/assets/nextflow-groovy-DJMQeKeT.js +1 -0
  235. package/public/assets/nginx-CN1bxnHs.js +1 -0
  236. package/public/assets/night-owl-DhmEMT88.js +1 -0
  237. package/public/assets/night-owl-light-eJ-hLW7d.js +1 -0
  238. package/public/assets/nim-BKDc-Eam.js +1 -0
  239. package/public/assets/nix-IvuFDN5E.js +1 -0
  240. package/public/assets/nord-Cb4Vim4T.js +1 -0
  241. package/public/assets/nsis-CjNLYCLg.js +1 -0
  242. package/public/assets/nushell-DHYfO66s.js +1 -0
  243. package/public/assets/objective-c-D1A_Heim.js +1 -0
  244. package/public/assets/objective-cpp-BsSzOQcm.js +1 -0
  245. package/public/assets/ocaml-O90oeIOV.js +1 -0
  246. package/public/assets/odin-B1RWQWA5.js +1 -0
  247. package/public/assets/one-dark-pro-CLwyXe_n.js +1 -0
  248. package/public/assets/one-light-D7Lr4KcI.js +1 -0
  249. package/public/assets/openscad-BUDT5pXO.js +1 -0
  250. package/public/assets/org-CBldiceC.js +1 -0
  251. package/public/assets/pascal-4ZHwLPI5.js +1 -0
  252. package/public/assets/perl-KiFGXvwB.js +1 -0
  253. package/public/assets/php-CjFYgW_Q.js +1 -0
  254. package/public/assets/pierre-dark-CpLgRqie.js +1 -0
  255. package/public/assets/pierre-dark-protanopia-deuteranopia-B35FxJx-.js +1 -0
  256. package/public/assets/pierre-dark-soft-kZQmAZld.js +1 -0
  257. package/public/assets/pierre-dark-tritanopia-CpjhbsIL.js +1 -0
  258. package/public/assets/pierre-dark-vibrant-CpQYzh95.js +1 -0
  259. package/public/assets/pierre-light-CoaEpmwp.js +1 -0
  260. package/public/assets/pierre-light-protanopia-deuteranopia-0fSaH845.js +1 -0
  261. package/public/assets/pierre-light-soft-lWLdNTOI.js +1 -0
  262. package/public/assets/pierre-light-tritanopia-CEbqgOJL.js +1 -0
  263. package/public/assets/pierre-light-vibrant-D80Fkn33.js +1 -0
  264. package/public/assets/pkl-ot-7Btpt.js +1 -0
  265. package/public/assets/plastic-DQwYfKfQ.js +1 -0
  266. package/public/assets/plsql-DGHpHOYJ.js +1 -0
  267. package/public/assets/po-BiJDBrnU.js +1 -0
  268. package/public/assets/poimandres-DRFjx7u4.js +1 -0
  269. package/public/assets/polar-C7UOKdEL.js +1 -0
  270. package/public/assets/postcss-BXeXVLqQ.js +1 -0
  271. package/public/assets/powerquery-DNMTfnFr.js +1 -0
  272. package/public/assets/powershell-CCSX75yM.js +1 -0
  273. package/public/assets/prisma-DMgNhnYS.js +1 -0
  274. package/public/assets/prolog-iXnhIJG7.js +1 -0
  275. package/public/assets/proto-DB4EqR-F.js +1 -0
  276. package/public/assets/pug-CiQNzZtF.js +1 -0
  277. package/public/assets/puppet-CDv2pdJW.js +1 -0
  278. package/public/assets/purescript-9MfHhQsQ.js +1 -0
  279. package/public/assets/python-gzcpVVnB.js +1 -0
  280. package/public/assets/qml-BynhyZxj.js +1 -0
  281. package/public/assets/qmldir-DCQb3MpD.js +1 -0
  282. package/public/assets/qss-Fe1Jh2GI.js +1 -0
  283. package/public/assets/r-BydfSESc.js +1 -0
  284. package/public/assets/racket-DcIDlBhZ.js +1 -0
  285. package/public/assets/raku-B3gFvitq.js +1 -0
  286. package/public/assets/razor-Bfi-c6ag.js +1 -0
  287. package/public/assets/rbs-C0MpOrj4.js +1 -0
  288. package/public/assets/red-DX1BQOs8.js +1 -0
  289. package/public/assets/reg-CRGYupPL.js +1 -0
  290. package/public/assets/regexp-BFpups_T.js +1 -0
  291. package/public/assets/rel-BtDbiS_P.js +1 -0
  292. package/public/assets/riscv-Ckw8ddFX.js +1 -0
  293. package/public/assets/ron-VUp2lXgN.js +1 -0
  294. package/public/assets/rose-pine-aMsaMD1D.js +1 -0
  295. package/public/assets/rose-pine-dawn-8PwUyIlP.js +1 -0
  296. package/public/assets/rose-pine-moon-DO-qXg-I.js +1 -0
  297. package/public/assets/rosmsg-CAekHB0j.js +1 -0
  298. package/public/assets/rst-Z5_5o5cf.js +1 -0
  299. package/public/assets/ruby-DyP6QZ5x.js +1 -0
  300. package/public/assets/rust-Cfkwpbl8.js +1 -0
  301. package/public/assets/sas-DzcEtbFW.js +1 -0
  302. package/public/assets/sass-DXrisJhu.js +1 -0
  303. package/public/assets/scala-BiFt_7mu.js +1 -0
  304. package/public/assets/scheme-DQCgrYNe.js +1 -0
  305. package/public/assets/scss-BVi50TCh.js +1 -0
  306. package/public/assets/sdbl-bTVj8UrX.js +1 -0
  307. package/public/assets/shaderlab-TOUzSsQk.js +1 -0
  308. package/public/assets/shellscript-DwBEbT4l.js +1 -0
  309. package/public/assets/shellsession-DtCkQKX0.js +1 -0
  310. package/public/assets/slack-dark-DnToyrRv.js +1 -0
  311. package/public/assets/slack-ochin-B2OO5cIa.js +1 -0
  312. package/public/assets/smalltalk-BHpQb5nM.js +1 -0
  313. package/public/assets/smithy-Dgaz7aur.js +1 -0
  314. package/public/assets/snazzy-light-4G7pJPwS.js +1 -0
  315. package/public/assets/solarized-dark-UdsmMDCL.js +1 -0
  316. package/public/assets/solarized-light-C1IUL_tW.js +1 -0
  317. package/public/assets/solidity-Bj7E1mRn.js +1 -0
  318. package/public/assets/soy-Col_eH-0.js +1 -0
  319. package/public/assets/sparql-D_iOobhT.js +1 -0
  320. package/public/assets/splunk-BC2Px7Mm.js +1 -0
  321. package/public/assets/sql-CF1geVQJ.js +1 -0
  322. package/public/assets/ssh-config-BgfXC-Er.js +1 -0
  323. package/public/assets/stata-JT7HPvq1.js +1 -0
  324. package/public/assets/stylus-B6D30XZt.js +1 -0
  325. package/public/assets/surrealql-kYIUYZug.js +1 -0
  326. package/public/assets/svelte-CsWZn_FS.js +1 -0
  327. package/public/assets/swift-CyEgAFGc.js +1 -0
  328. package/public/assets/synthwave-84-nFMaYfgc.js +1 -0
  329. package/public/assets/system-verilog-Dh5ihWbf.js +1 -0
  330. package/public/assets/systemd-BxMlprV5.js +1 -0
  331. package/public/assets/talonscript-CohzipZa.js +1 -0
  332. package/public/assets/tasl-DMoTqEGO.js +1 -0
  333. package/public/assets/tcl-CZd0xW_V.js +1 -0
  334. package/public/assets/templ-DCsT8WEJ.js +1 -0
  335. package/public/assets/terraform-DswuEJGm.js +1 -0
  336. package/public/assets/tex-ZyN9TW16.js +1 -0
  337. package/public/assets/tokyo-night-oM2G3aXe.js +1 -0
  338. package/public/assets/toml-CcmNWLt0.js +1 -0
  339. package/public/assets/ts-tags-ArLnwaTi.js +1 -0
  340. package/public/assets/tsv-sltzmVWM.js +1 -0
  341. package/public/assets/tsx-CXwa5__r.js +1 -0
  342. package/public/assets/turtle-ByJddavk.js +1 -0
  343. package/public/assets/twig-BsBKj8BU.js +1 -0
  344. package/public/assets/typescript-Brm8t4pO.js +1 -0
  345. package/public/assets/typespec-BRdr0IET.js +1 -0
  346. package/public/assets/typst-vNjrO6QE.js +1 -0
  347. package/public/assets/v-DBj4K9d_.js +1 -0
  348. package/public/assets/vala-zf12oZj6.js +1 -0
  349. package/public/assets/vb-DOwzaHZ0.js +1 -0
  350. package/public/assets/verilog-B2gwz-sJ.js +1 -0
  351. package/public/assets/vesper-DdrHHSXu.js +1 -0
  352. package/public/assets/vhdl-BroJfC0k.js +1 -0
  353. package/public/assets/viml-DvXPmvsu.js +1 -0
  354. package/public/assets/vitesse-black-fwtXNY1n.js +1 -0
  355. package/public/assets/vitesse-dark-BZCL-v6S.js +1 -0
  356. package/public/assets/vitesse-light-VbXTXTou.js +1 -0
  357. package/public/assets/vue-CcPq89qx.js +1 -0
  358. package/public/assets/vue-html-B5qW2N7E.js +1 -0
  359. package/public/assets/vue-vine-LYElP1mf.js +1 -0
  360. package/public/assets/vyper-CgoNMtux.js +1 -0
  361. package/public/assets/wasm-BnjxR4X6.js +1 -0
  362. package/public/assets/wasm-ByWQv1Qj.js +1 -0
  363. package/public/assets/wenyan-C8pVoKbM.js +1 -0
  364. package/public/assets/wgsl-BsKzXJz4.js +1 -0
  365. package/public/assets/wikitext-ClFFjSW2.js +1 -0
  366. package/public/assets/wit-DdvCle-K.js +1 -0
  367. package/public/assets/wolfram-DLL8P-h_.js +1 -0
  368. package/public/assets/xml-D5hvN78Q.js +1 -0
  369. package/public/assets/xsl-CBSZ35CX.js +1 -0
  370. package/public/assets/yaml-D4FTZq40.js +1 -0
  371. package/public/assets/zenscript-BnlCZFoB.js +1 -0
  372. package/public/assets/zig-CMLA9XwU.js +1 -0
  373. package/public/brand/coja/cj-dark.svg +1 -0
  374. package/public/brand/coja/cj-light.svg +1 -0
  375. package/public/brand/coja/cj-themeable.svg +1 -0
  376. package/public/brand/coja/lottie/cj-huge-dark.json +311 -0
  377. package/public/brand/coja/lottie/cj-huge-light.json +311 -0
  378. package/public/brand/coja/lottie/cj-large-dark.json +311 -0
  379. package/public/brand/coja/lottie/cj-large-light.json +311 -0
  380. package/public/brand/coja/lottie/cj-medium-dark.json +311 -0
  381. package/public/brand/coja/lottie/cj-medium-light.json +311 -0
  382. package/public/brand/coja/lottie/cj-small-dark.json +311 -0
  383. package/public/brand/coja/lottie/cj-small-light.json +311 -0
  384. package/public/brand/coja/manifest.json +172 -0
  385. package/public/brand/coja/themes/ember/dark/cj-huge.json +311 -0
  386. package/public/brand/coja/themes/ember/dark/cj-large.json +311 -0
  387. package/public/brand/coja/themes/ember/dark/cj-medium.json +311 -0
  388. package/public/brand/coja/themes/ember/dark/cj-small.json +311 -0
  389. package/public/brand/coja/themes/ember/dark/cj.svg +1 -0
  390. package/public/brand/coja/themes/ember/light/cj-huge.json +311 -0
  391. package/public/brand/coja/themes/ember/light/cj-large.json +311 -0
  392. package/public/brand/coja/themes/ember/light/cj-medium.json +311 -0
  393. package/public/brand/coja/themes/ember/light/cj-small.json +311 -0
  394. package/public/brand/coja/themes/ember/light/cj.svg +1 -0
  395. package/public/brand/coja/themes/grove/dark/cj-huge.json +311 -0
  396. package/public/brand/coja/themes/grove/dark/cj-large.json +311 -0
  397. package/public/brand/coja/themes/grove/dark/cj-medium.json +311 -0
  398. package/public/brand/coja/themes/grove/dark/cj-small.json +311 -0
  399. package/public/brand/coja/themes/grove/dark/cj.svg +1 -0
  400. package/public/brand/coja/themes/grove/light/cj-huge.json +311 -0
  401. package/public/brand/coja/themes/grove/light/cj-large.json +311 -0
  402. package/public/brand/coja/themes/grove/light/cj-medium.json +311 -0
  403. package/public/brand/coja/themes/grove/light/cj-small.json +311 -0
  404. package/public/brand/coja/themes/grove/light/cj.svg +1 -0
  405. package/public/brand/coja/themes/iris/dark/cj-huge.json +311 -0
  406. package/public/brand/coja/themes/iris/dark/cj-large.json +311 -0
  407. package/public/brand/coja/themes/iris/dark/cj-medium.json +311 -0
  408. package/public/brand/coja/themes/iris/dark/cj-small.json +311 -0
  409. package/public/brand/coja/themes/iris/dark/cj.svg +1 -0
  410. package/public/brand/coja/themes/iris/light/cj-huge.json +311 -0
  411. package/public/brand/coja/themes/iris/light/cj-large.json +311 -0
  412. package/public/brand/coja/themes/iris/light/cj-medium.json +311 -0
  413. package/public/brand/coja/themes/iris/light/cj-small.json +311 -0
  414. package/public/brand/coja/themes/iris/light/cj.svg +1 -0
  415. package/public/brand/coja/themes/mulberry/dark/cj-huge.json +311 -0
  416. package/public/brand/coja/themes/mulberry/dark/cj-large.json +311 -0
  417. package/public/brand/coja/themes/mulberry/dark/cj-medium.json +311 -0
  418. package/public/brand/coja/themes/mulberry/dark/cj-small.json +311 -0
  419. package/public/brand/coja/themes/mulberry/dark/cj.svg +1 -0
  420. package/public/brand/coja/themes/mulberry/light/cj-huge.json +311 -0
  421. package/public/brand/coja/themes/mulberry/light/cj-large.json +311 -0
  422. package/public/brand/coja/themes/mulberry/light/cj-medium.json +311 -0
  423. package/public/brand/coja/themes/mulberry/light/cj-small.json +311 -0
  424. package/public/brand/coja/themes/mulberry/light/cj.svg +1 -0
  425. package/public/brand/coja/themes/ocean/dark/cj-huge.json +311 -0
  426. package/public/brand/coja/themes/ocean/dark/cj-large.json +311 -0
  427. package/public/brand/coja/themes/ocean/dark/cj-medium.json +311 -0
  428. package/public/brand/coja/themes/ocean/dark/cj-small.json +311 -0
  429. package/public/brand/coja/themes/ocean/dark/cj.svg +1 -0
  430. package/public/brand/coja/themes/ocean/light/cj-huge.json +311 -0
  431. package/public/brand/coja/themes/ocean/light/cj-large.json +311 -0
  432. package/public/brand/coja/themes/ocean/light/cj-medium.json +311 -0
  433. package/public/brand/coja/themes/ocean/light/cj-small.json +311 -0
  434. package/public/brand/coja/themes/ocean/light/cj.svg +1 -0
  435. package/public/fonts/ibm-plex-sans/IBMPlexSans-Bold.woff2 +0 -0
  436. package/public/fonts/ibm-plex-sans/IBMPlexSans-BoldItalic.woff2 +0 -0
  437. package/public/fonts/ibm-plex-sans/IBMPlexSans-Italic.woff2 +0 -0
  438. package/public/fonts/ibm-plex-sans/IBMPlexSans-Medium.woff2 +0 -0
  439. package/public/fonts/ibm-plex-sans/IBMPlexSans-MediumItalic.woff2 +0 -0
  440. package/public/fonts/ibm-plex-sans/IBMPlexSans-Regular.woff2 +0 -0
  441. package/public/fonts/ibm-plex-sans/IBMPlexSans-SemiBold.woff2 +0 -0
  442. package/public/fonts/ibm-plex-sans/IBMPlexSans-SemiBoldItalic.woff2 +0 -0
  443. package/public/fonts/ibm-plex-sans/LICENSE.txt +93 -0
  444. package/public/fonts/ibm-plex-sans/PROVENANCE.md +10 -0
  445. package/public/fonts/maple-mono-nf/LICENSE.txt +93 -0
  446. package/public/fonts/maple-mono-nf/MapleMono-NF-Bold.woff2 +0 -0
  447. package/public/fonts/maple-mono-nf/MapleMono-NF-BoldItalic.woff2 +0 -0
  448. package/public/fonts/maple-mono-nf/MapleMono-NF-Italic.woff2 +0 -0
  449. package/public/fonts/maple-mono-nf/MapleMono-NF-Medium.woff2 +0 -0
  450. package/public/fonts/maple-mono-nf/MapleMono-NF-MediumItalic.woff2 +0 -0
  451. package/public/fonts/maple-mono-nf/MapleMono-NF-Regular.woff2 +0 -0
  452. package/public/fonts/maple-mono-nf/MapleMono-NF-SemiBold.woff2 +0 -0
  453. package/public/fonts/maple-mono-nf/MapleMono-NF-SemiBoldItalic.woff2 +0 -0
  454. package/public/fonts/maple-mono-nf/PROVENANCE.md +12 -0
  455. package/public/index.html +14 -0
@@ -0,0 +1,111 @@
1
+ import { mkdir, rm, stat } from 'node:fs/promises';
2
+ import os from 'node:os';
3
+ import path from 'node:path';
4
+ import { cloneBare, isBareRepo, isGitRepo, remoteUrl, topLevel } from '../git/plumbing.js';
5
+ import { parseGitHubRemote, parseRepoSlug } from '../git/remote.js';
6
+ import { GitError } from '../git/run.js';
7
+ import { badRequest, HttpError, notFound } from '../routes/http.js';
8
+ import { deleteProject, findProjectByPath, getProject, insertProject } from './store.js';
9
+ /**
10
+ * Adding and removing projects (design.md, "Projects → PR list"): a `local`
11
+ * project is the user's own clone, identified through its `origin` remote; a
12
+ * `clone` project is a bare clone coja manages under `<dataDir>/repos/`.
13
+ */
14
+ /** Register the git repository at `inputPath` (absolute, or `~`-relative) as a local project. */
15
+ export async function addLocalProject(ctx, inputPath) {
16
+ const resolved = resolveUserPath(inputPath);
17
+ let info;
18
+ try {
19
+ info = await stat(resolved);
20
+ }
21
+ catch {
22
+ throw badRequest(`${resolved} does not exist`);
23
+ }
24
+ if (!info.isDirectory())
25
+ throw badRequest(`${resolved} is not a directory`);
26
+ if (!(await isGitRepo(resolved)))
27
+ throw badRequest(`${resolved} is not a git repository`);
28
+ // Normalise to the working-tree root so the same clone is never registered twice.
29
+ const repoPath = (await isBareRepo(resolved))
30
+ ? resolved
31
+ : ((await topLevel(resolved)) ?? resolved);
32
+ const existing = findProjectByPath(ctx.db, repoPath);
33
+ if (existing)
34
+ return existing;
35
+ const url = await remoteUrl(repoPath, 'origin');
36
+ if (!url) {
37
+ throw badRequest(`${repoPath} has no "origin" remote; coja needs a GitHub remote to find its pull requests`);
38
+ }
39
+ const gh = parseGitHubRemote(url);
40
+ if (!gh)
41
+ throw badRequest(`the "origin" remote of ${repoPath} is not a GitHub repository (${url})`);
42
+ return insertProject(ctx.db, { kind: 'local', owner: gh.owner, repo: gh.repo, path: repoPath });
43
+ }
44
+ /** Clone `owner/repo` (or a github.com URL) bare into the data dir and register it. */
45
+ export async function addCloneProject(ctx, slug, opts = {}) {
46
+ const parsed = parseRepoSlug(slug);
47
+ if (!parsed) {
48
+ throw badRequest(`"${slug}" is not a GitHub repository (expected owner/repo or a github.com URL)`);
49
+ }
50
+ const { owner, repo } = parsed;
51
+ const dir = cloneDir(ctx.dataDir, owner, repo);
52
+ const existing = findProjectByPath(ctx.db, dir);
53
+ if (existing)
54
+ return existing;
55
+ if (await isGitRepo(dir)) {
56
+ // Only a bare clone of ours is adopted; anything else at that path is not ours to use.
57
+ if (!(await isBareRepo(dir))) {
58
+ throw badRequest(`${dir} already exists but is not a bare repository; move it away and try again`);
59
+ }
60
+ }
61
+ else {
62
+ await mkdir(path.dirname(dir), { recursive: true });
63
+ // A leftover from an interrupted clone would make `git clone` refuse the directory.
64
+ await rm(dir, { recursive: true, force: true });
65
+ try {
66
+ await (opts.clone ?? cloneBare)(`https://github.com/${owner}/${repo}.git`, dir, {
67
+ signal: opts.signal,
68
+ });
69
+ }
70
+ catch (err) {
71
+ // Whether git failed or the request was aborted, never leave a half-written clone behind.
72
+ await rm(dir, { recursive: true, force: true });
73
+ if (err instanceof GitError) {
74
+ const detail = err.stderr.trim() || err.message;
75
+ throw new HttpError(400, `could not clone ${owner}/${repo}: ${detail}`, 'git');
76
+ }
77
+ throw err;
78
+ }
79
+ }
80
+ return insertProject(ctx.db, { kind: 'clone', owner, repo, path: dir });
81
+ }
82
+ /**
83
+ * Forget a project. App-managed bare clones are deleted from disk (they are
84
+ * ours); a local project's files are never touched.
85
+ */
86
+ export async function removeProject(ctx, id) {
87
+ const project = getProject(ctx.db, id);
88
+ if (!project)
89
+ throw notFound('project');
90
+ deleteProject(ctx.db, id);
91
+ if (project.kind === 'clone' && isInside(reposDir(ctx.dataDir), project.path)) {
92
+ await rm(project.path, { recursive: true, force: true });
93
+ }
94
+ }
95
+ export const reposDir = (dataDir) => path.join(dataDir, 'repos');
96
+ /** `<dataDir>/repos/<owner>__<repo>.git`, lower-cased because GitHub slugs are case-insensitive. */
97
+ export function cloneDir(dataDir, owner, repo) {
98
+ return path.join(reposDir(dataDir), `${owner}__${repo}.git`.toLowerCase());
99
+ }
100
+ function resolveUserPath(input) {
101
+ const trimmed = input.trim();
102
+ if (trimmed === '~')
103
+ return os.homedir();
104
+ if (trimmed.startsWith('~/'))
105
+ return path.resolve(os.homedir(), trimmed.slice(2));
106
+ return path.resolve(trimmed);
107
+ }
108
+ function isInside(parent, child) {
109
+ const rel = path.relative(parent, child);
110
+ return rel !== '' && !rel.startsWith('..') && !path.isAbsolute(rel);
111
+ }
@@ -0,0 +1,37 @@
1
+ import { randomBytes } from 'node:crypto';
2
+ import { nowIso } from '../db.js';
3
+ const toProject = (r) => ({
4
+ id: r.id,
5
+ kind: r.kind,
6
+ owner: r.owner,
7
+ repo: r.repo,
8
+ path: r.path,
9
+ createdAt: r.created_at,
10
+ });
11
+ export function listProjects(db) {
12
+ const rows = db
13
+ .prepare('SELECT * FROM projects ORDER BY created_at ASC')
14
+ .all();
15
+ return rows.map(toProject);
16
+ }
17
+ export function getProject(db, id) {
18
+ const row = db.prepare('SELECT * FROM projects WHERE id = ?').get(id);
19
+ return row ? toProject(row) : undefined;
20
+ }
21
+ export function findProjectByPath(db, path) {
22
+ const row = db.prepare('SELECT * FROM projects WHERE path = ?').get(path);
23
+ return row ? toProject(row) : undefined;
24
+ }
25
+ export function insertProject(db, input) {
26
+ const project = {
27
+ id: randomBytes(6).toString('hex'),
28
+ ...input,
29
+ createdAt: nowIso(),
30
+ };
31
+ db.prepare('INSERT INTO projects (id, kind, owner, repo, path, created_at) VALUES (?, ?, ?, ?, ?, ?)').run(project.id, project.kind, project.owner, project.repo, project.path, project.createdAt);
32
+ return project;
33
+ }
34
+ export function deleteProject(db, id) {
35
+ const res = db.prepare('DELETE FROM projects WHERE id = ?').run(id);
36
+ return Number(res.changes) > 0;
37
+ }
@@ -0,0 +1,187 @@
1
+ import { z } from 'zod';
2
+ import { handleChatTurn } from '../ai/chat-handler.js';
3
+ import { createChat, deleteChat, getChat, listChats } from '../ai/chats.js';
4
+ import { buildSystemPrompt } from '../ai/prompt.js';
5
+ import { defaultModelId, parseModelId, } from '../ai/providers.js';
6
+ import { describeTools } from '../ai/tools.js';
7
+ import { diffNameStatus } from '../git/plumbing.js';
8
+ import { badRequest, HttpError, notFound } from './http.js';
9
+ import { PR_ROUTE, resolvePr } from './pulls.js';
10
+ /**
11
+ * AI chat routes (API_ROUTES.prChats / prChat / prChatMessages / prAiContext).
12
+ *
13
+ * Deliberate shape: this module gets a read-only `readPullRequest` function,
14
+ * never a Forge, so nothing reachable from the AI layer can post to GitHub —
15
+ * "the AI drafts, the human sends". Routes that read PR code (a chat turn,
16
+ * the ai-context preview) answer 409 until the PR's objects are fetched, like
17
+ * the git routes; chat bookkeeping (list/create/get/delete) needs no objects.
18
+ */
19
+ export const CHATS_ROUTE = `${PR_ROUTE}/chats`;
20
+ export const CHAT_ROUTE = `${CHATS_ROUTE}/:chatId`;
21
+ export const CHAT_MESSAGES_ROUTE = `${CHAT_ROUTE}/messages`;
22
+ export const AI_CONTEXT_ROUTE = `${PR_ROUTE}/ai-context`;
23
+ const newChatSchema = z.object({ model: z.string().min(1).optional() });
24
+ /**
25
+ * The browser re-sends the whole conversation every turn, so one request is
26
+ * bounded here (bytes, messages); what of it reaches the model is bounded in
27
+ * ai/history.ts.
28
+ */
29
+ export const MAX_BODY_BYTES = 5 * 1024 * 1024;
30
+ export const MAX_MESSAGES = 400;
31
+ const chatRequestSchema = z.object({
32
+ messages: z
33
+ .array(z.unknown())
34
+ .max(MAX_MESSAGES, `a conversation may hold at most ${MAX_MESSAGES} messages; start a new chat`),
35
+ model: z.string().min(1),
36
+ reasoningEffort: z.string().min(1).max(32).optional(),
37
+ });
38
+ export function registerChatRoutes(app, ctx, deps) {
39
+ const { secrets, fetcher, readPullRequest } = deps;
40
+ /** Head and merge base once the PR's objects are in the repository; 409 (`code: 'git'`) before. */
41
+ const requireReady = (project, number) => {
42
+ const status = fetcher.status(project.id, number);
43
+ if (status.state === 'ready' && status.headOid && status.mergeBaseOid) {
44
+ return { headOid: status.headOid, mergeBaseOid: status.mergeBaseOid };
45
+ }
46
+ throw new HttpError(409, 'PR objects are still being fetched', 'git');
47
+ };
48
+ /** The PR detail and its changed files from git — the AI's standing context. */
49
+ const loadPr = async (project, number) => {
50
+ const { headOid, mergeBaseOid } = requireReady(project, number);
51
+ const [detail, files] = await Promise.all([
52
+ readPullRequest(project, number),
53
+ diffNameStatus(project.path, mergeBaseOid, headOid),
54
+ ]);
55
+ return { headOid, mergeBaseOid, detail, files };
56
+ };
57
+ /** The chat named by `:chatId`, which must belong to this project and PR. */
58
+ const loadChat = (c, project, number) => {
59
+ const id = c.req.param('chatId') ?? '';
60
+ const found = id ? getChat(ctx.db, id) : undefined;
61
+ if (!found || found.chat.projectId !== project.id || found.chat.prNumber !== number) {
62
+ throw notFound('chat');
63
+ }
64
+ return found;
65
+ };
66
+ app.get(CHATS_ROUTE, (c) => {
67
+ const { project, number } = resolvePr(ctx, c);
68
+ return c.json(listChats(ctx.db, project.id, number));
69
+ });
70
+ app.post(CHATS_ROUTE, async (c) => {
71
+ const { project, number } = resolvePr(ctx, c);
72
+ const body = parse(newChatSchema, await readJsonBody(c));
73
+ const model = body.model ?? (await defaultModelId(deps.subscription));
74
+ if (!model)
75
+ throw new HttpError(400, 'No AI provider configured', 'provider');
76
+ parseModelId(model, deps.customProviders);
77
+ return c.json(createChat(ctx.db, { projectId: project.id, prNumber: number, model }), 201);
78
+ });
79
+ app.get(CHAT_ROUTE, (c) => {
80
+ const { project, number } = resolvePr(ctx, c);
81
+ return c.json(loadChat(c, project, number));
82
+ });
83
+ app.delete(CHAT_ROUTE, (c) => {
84
+ const { project, number } = resolvePr(ctx, c);
85
+ const { chat } = loadChat(c, project, number);
86
+ deleteChat(ctx.db, chat.id);
87
+ return c.json({ ok: true });
88
+ });
89
+ app.post(CHAT_MESSAGES_ROUTE, async (c) => {
90
+ const { project, number } = resolvePr(ctx, c);
91
+ const { chat } = loadChat(c, project, number);
92
+ const body = parse(chatRequestSchema, await readJsonBody(c));
93
+ // An unknown model is a 400 here, before the fetch-state check and whichever model resolver runs.
94
+ const { provider, modelId } = parseModelId(body.model);
95
+ // A requested reasoning effort must be one the backend catalog offers for
96
+ // this model (subscription path); other providers do not take one from coja.
97
+ if (body.reasoningEffort && provider === 'chatgpt') {
98
+ const ok = deps.subscription &&
99
+ (await deps.subscription
100
+ .supportsReasoningEffort(modelId, body.reasoningEffort)
101
+ .catch(() => false));
102
+ if (!ok) {
103
+ throw new HttpError(400, `reasoning effort "${body.reasoningEffort}" is not offered for ${modelId}; pick one from the model's list`, 'provider');
104
+ }
105
+ }
106
+ const { headOid, mergeBaseOid, detail, files } = await loadPr(project, number);
107
+ return handleChatTurn({
108
+ db: ctx.db,
109
+ secrets,
110
+ chat,
111
+ messages: body.messages,
112
+ model: body.model,
113
+ toolContext: {
114
+ repo: project.path,
115
+ headOid,
116
+ baseOid: mergeBaseOid,
117
+ files,
118
+ },
119
+ detail,
120
+ files,
121
+ signal: c.req.raw.signal,
122
+ customProviders: deps.customProviders,
123
+ ...(body.reasoningEffort ? { reasoningEffort: body.reasoningEffort } : {}),
124
+ ...(deps.subscription ? { subscription: deps.subscription } : {}),
125
+ ...(deps.resolveModel ? { languageModel: await deps.resolveModel(body.model, secrets) } : {}),
126
+ });
127
+ });
128
+ app.get(AI_CONTEXT_ROUTE, async (c) => {
129
+ const { project, number } = resolvePr(ctx, c);
130
+ const { detail, files } = await loadPr(project, number);
131
+ const response = {
132
+ system: buildSystemPrompt({ detail, files }),
133
+ tools: describeTools(),
134
+ };
135
+ return c.json(response);
136
+ });
137
+ }
138
+ /** The JSON body, or `{}` when there is none (all fields of NewChatRequest are optional). 413 past MAX_BODY_BYTES. */
139
+ async function readJsonBody(c) {
140
+ const text = await readBodyText(c, MAX_BODY_BYTES);
141
+ if (text.trim() === '')
142
+ return {};
143
+ try {
144
+ return JSON.parse(text);
145
+ }
146
+ catch {
147
+ throw badRequest('request body must be JSON');
148
+ }
149
+ }
150
+ /**
151
+ * The request body as text, refusing more than `maxBytes` with a 413: up front
152
+ * when Content-Length says so, otherwise as soon as the stream exceeds the cap,
153
+ * so an oversized body is never buffered whole (let alone parsed).
154
+ */
155
+ async function readBodyText(c, maxBytes) {
156
+ const tooLarge = () => new HttpError(413, `request body too large: at most ${maxBytes / (1024 * 1024)} MB per chat request`, 'bad_request');
157
+ const declared = Number(c.req.header('content-length'));
158
+ if (Number.isFinite(declared) && declared > maxBytes)
159
+ throw tooLarge();
160
+ const body = c.req.raw.body;
161
+ if (!body)
162
+ return '';
163
+ const reader = body.getReader();
164
+ const chunks = [];
165
+ let total = 0;
166
+ for (;;) {
167
+ const { done, value } = await reader.read();
168
+ if (done)
169
+ break;
170
+ total += value.byteLength;
171
+ if (total > maxBytes) {
172
+ await reader.cancel().catch(() => undefined);
173
+ throw tooLarge();
174
+ }
175
+ chunks.push(value);
176
+ }
177
+ return Buffer.concat(chunks).toString('utf8');
178
+ }
179
+ function parse(schema, value) {
180
+ const result = schema.safeParse(value);
181
+ if (result.success)
182
+ return result.data;
183
+ const issues = result.error.issues
184
+ .map((i) => (i.path.length ? `${i.path.map(String).join('.')}: ${i.message}` : i.message))
185
+ .join('; ');
186
+ throw badRequest(`invalid request: ${issues}`);
187
+ }
@@ -0,0 +1,65 @@
1
+ import { readdir, stat } from 'node:fs/promises';
2
+ import os from 'node:os';
3
+ import path from 'node:path';
4
+ import { API_ROUTES } from '../shared/api.js';
5
+ import { badRequest, notFound } from './http.js';
6
+ /**
7
+ * Read-only filesystem browsing for the Add-project path picker (design.md §2).
8
+ * Names of subdirectories only — never file listings, contents, or a disk-wide
9
+ * search — capped and prefix-filtered so a huge directory costs the same as a
10
+ * small one. The request guard already confines the app to loopback; this adds
11
+ * no capability a local user lacks, and the picker only ever *suggests* paths
12
+ * (adding one still validates that it is a git clone with an origin remote).
13
+ */
14
+ /** Hard ceiling for one listing; the picker asks for no more than this. */
15
+ export const FS_LIST_CAP = 500;
16
+ export function registerFsRoutes(app) {
17
+ app.get(API_ROUTES.fsHome, (c) => c.json({ home: os.homedir() }));
18
+ app.get(API_ROUTES.fsDirs, async (c) => {
19
+ const raw = c.req.query('path') ?? '';
20
+ if (!raw.startsWith('/'))
21
+ throw badRequest('path must be an absolute path');
22
+ const prefix = (c.req.query('prefix') ?? '').toLowerCase();
23
+ const parsedLimit = Number.parseInt(c.req.query('limit') ?? '', 10);
24
+ const limit = Number.isFinite(parsedLimit)
25
+ ? Math.min(Math.max(parsedLimit, 1), FS_LIST_CAP)
26
+ : FS_LIST_CAP;
27
+ let entries;
28
+ try {
29
+ entries = await readdir(raw, { withFileTypes: true, encoding: 'utf8' });
30
+ }
31
+ catch (err) {
32
+ const code = err.code;
33
+ if (code === 'ENOENT')
34
+ throw notFound(`directory ${raw}`);
35
+ if (code === 'ENOTDIR')
36
+ throw badRequest(`${raw} is not a directory`);
37
+ if (code === 'EACCES')
38
+ throw badRequest(`${raw} is not readable`);
39
+ throw err;
40
+ }
41
+ const names = entries
42
+ .filter((entry) => entry.isDirectory())
43
+ .map((entry) => entry.name)
44
+ .sort((a, b) => a.localeCompare(b, undefined, { sensitivity: 'base', numeric: true }))
45
+ .filter((name) => (prefix ? name.toLowerCase().startsWith(prefix) : true));
46
+ const slice = names.slice(0, limit);
47
+ // One stat per listed entry, capped: a `.git` entry (directory, file for
48
+ // worktrees/submodules) marks a likely clone worth a badge in the picker.
49
+ const dirs = await Promise.all(slice.map(async (name) => {
50
+ const hasGit = await stat(path.join(raw, name, '.git'))
51
+ .then(() => true)
52
+ .catch(() => false);
53
+ return { name, hasGit };
54
+ }));
55
+ const parent = path.dirname(raw);
56
+ const body = {
57
+ path: raw,
58
+ parent: parent === raw ? null : parent,
59
+ dirs,
60
+ total: names.length,
61
+ truncated: names.length > slice.length,
62
+ };
63
+ return c.json(body);
64
+ });
65
+ }
@@ -0,0 +1,75 @@
1
+ import { z } from 'zod';
2
+ import { getBlob, getChangedFiles, getFileDiff } from '../pr/objects.js';
3
+ import { getProject } from '../projects/store.js';
4
+ import { badRequest, HttpError, notFound } from './http.js';
5
+ import { validate } from './projects.js';
6
+ const PR = '/api/projects/:projectId/prs/:number';
7
+ const DiffQuery = z.object({
8
+ path: z.string().min(1),
9
+ previousPath: z.string().min(1).optional(),
10
+ });
11
+ const BlobQuery = z.object({
12
+ ref: z.enum(['base', 'head']).default('head'),
13
+ path: z.string().min(1),
14
+ start: z.coerce.number().int().min(1).optional(),
15
+ end: z.coerce.number().int().min(1).optional(),
16
+ });
17
+ /**
18
+ * `API_ROUTES.prFetch`, `prGitFiles`, `prDiff`, `prBlob`: the git side of a
19
+ * pull request. Everything but `fetch` answers 409 (`code: 'git'`) until the
20
+ * PR's objects are fetched; the UI polls `GET …/fetch` meanwhile.
21
+ */
22
+ export function registerGitRoutes(app, ctx, deps) {
23
+ const { forge, fetcher } = deps;
24
+ const load = (c) => {
25
+ const project = getProject(ctx.db, c.req.param('projectId') ?? '');
26
+ if (!project)
27
+ throw notFound('project');
28
+ return { project, number: parsePrNumber(c.req.param('number')) };
29
+ };
30
+ const requireReady = (project, number) => {
31
+ const status = fetcher.status(project.id, number);
32
+ if (status.state === 'ready' && status.headOid && status.baseOid && status.mergeBaseOid) {
33
+ return { headOid: status.headOid, baseOid: status.baseOid, mergeBaseOid: status.mergeBaseOid };
34
+ }
35
+ const reason = status.state === 'error'
36
+ ? `fetching PR #${number} failed: ${status.error ?? 'unknown error'}`
37
+ : `PR #${number} is not fetched yet (${status.state}); POST …/fetch first`;
38
+ throw new HttpError(409, reason, 'git');
39
+ };
40
+ app.post(`${PR}/fetch`, async (c) => {
41
+ const { project, number } = load(c);
42
+ const refs = await forge.getPullRequestRefs({ owner: project.owner, repo: project.repo }, number);
43
+ return c.json(await fetcher.ensure(project, refs));
44
+ });
45
+ app.get(`${PR}/fetch`, (c) => {
46
+ const { project, number } = load(c);
47
+ return c.json(fetcher.status(project.id, number));
48
+ });
49
+ app.get(`${PR}/git-files`, async (c) => {
50
+ const { project, number } = load(c);
51
+ const { mergeBaseOid, headOid } = requireReady(project, number);
52
+ return c.json(await getChangedFiles(project.path, mergeBaseOid, headOid));
53
+ });
54
+ app.get(`${PR}/diff`, async (c) => {
55
+ const { project, number } = load(c);
56
+ const query = validate(DiffQuery, c.req.query());
57
+ const { mergeBaseOid, headOid } = requireReady(project, number);
58
+ return c.json(await getFileDiff(project.path, mergeBaseOid, headOid, query.path, query.previousPath));
59
+ });
60
+ app.get(`${PR}/blob`, async (c) => {
61
+ const { project, number } = load(c);
62
+ const query = validate(BlobQuery, c.req.query());
63
+ const ready = requireReady(project, number);
64
+ // `base` is the merge base: the commit the diff's left side shows.
65
+ const oid = query.ref === 'head' ? ready.headOid : ready.mergeBaseOid;
66
+ return c.json(await getBlob(project.path, query.ref, oid, query.path, query.start, query.end));
67
+ });
68
+ }
69
+ function parsePrNumber(raw) {
70
+ const n = Number(raw);
71
+ if (raw === undefined || !/^\d+$/.test(raw) || !Number.isSafeInteger(n) || n <= 0) {
72
+ throw badRequest(`invalid pull request number: ${raw ?? ''}`);
73
+ }
74
+ return n;
75
+ }
@@ -0,0 +1,96 @@
1
+ const LOOPBACK_HOSTNAMES = ['127.0.0.1', 'localhost', '::1'];
2
+ /** Methods that never mutate here; the UI only ever reads through them. */
3
+ const SAFE_METHODS = new Set(['GET', 'HEAD']);
4
+ /** `app.use(requestGuard())` — see {@link registerRequestGuard}. */
5
+ export function requestGuard(opts = {}) {
6
+ const allowed = new Set([
7
+ ...LOOPBACK_HOSTNAMES,
8
+ ...(opts.allowedHosts ?? []).map((h) => h.trim().toLowerCase()),
9
+ ]);
10
+ return async (c, next) => {
11
+ // The Node adapter builds the request URL from the Host header, so the URL's host is the
12
+ // same thing; it is the fallback when a request has no Host header (tests, HTTP/1.0).
13
+ const host = c.req.header('host') ?? new URL(c.req.url).host;
14
+ const hostname = hostnameOf(host);
15
+ if (hostname === null || !allowed.has(hostname)) {
16
+ // Name the offender: someone who reached this over `--host` sees at once which Host was refused.
17
+ return forbidden(c, `bad host "${host.slice(0, 200)}": only loopback or the --host address is served`);
18
+ }
19
+ const method = c.req.method.toUpperCase();
20
+ // No CORS is offered, so a preflight can only be a cross-origin page probing us.
21
+ if (method === 'OPTIONS')
22
+ return forbidden(c, 'cross-site request refused');
23
+ if (SAFE_METHODS.has(method))
24
+ return next();
25
+ const site = c.req.header('sec-fetch-site')?.trim().toLowerCase();
26
+ if (site !== undefined) {
27
+ // `same-site` is not enough: another local server on a different port is same-site.
28
+ if (site !== 'same-origin' && site !== 'none') {
29
+ return forbidden(c, 'cross-site request refused');
30
+ }
31
+ }
32
+ else {
33
+ const origin = c.req.header('origin');
34
+ if (origin !== undefined && !originMatchesHost(origin, host)) {
35
+ return forbidden(c, 'cross-site request refused');
36
+ }
37
+ }
38
+ if (hasBody(c) && !isJson(c.req.header('content-type'))) {
39
+ return c.json({ error: 'content-type must be application/json' }, 415);
40
+ }
41
+ return next();
42
+ };
43
+ }
44
+ /** Install the guard on `app` before any route, so it runs for every request. */
45
+ export function registerRequestGuard(app, opts = {}) {
46
+ app.use('*', requestGuard(opts));
47
+ }
48
+ /**
49
+ * Hostname of a `Host` header value, lower-cased and without port or IPv6
50
+ * brackets: `127.0.0.1:4321` → `127.0.0.1`, `[::1]:4321` → `::1`,
51
+ * `LocalHost` → `localhost`. Null when the value is not a valid host.
52
+ */
53
+ export function hostnameOf(host) {
54
+ const value = host.trim().toLowerCase();
55
+ if (value === '')
56
+ return null;
57
+ if (value.startsWith('[')) {
58
+ const end = value.indexOf(']');
59
+ if (end === -1)
60
+ return null;
61
+ const port = value.slice(end + 1);
62
+ if (port !== '' && !/^:\d{1,5}$/.test(port))
63
+ return null;
64
+ return value.slice(1, end) || null;
65
+ }
66
+ const parts = value.split(':');
67
+ if (parts.length > 2)
68
+ return null; // an IPv6 literal must be bracketed
69
+ const [name, port] = parts;
70
+ if (!name || (port !== undefined && !/^\d{1,5}$/.test(port)))
71
+ return null;
72
+ return name;
73
+ }
74
+ /** True when `origin` (an `Origin` header) names the same host and port as `host` (a `Host` header). */
75
+ function originMatchesHost(origin, host) {
76
+ try {
77
+ // Parsing both normalises case and default ports (`localhost:80` ≡ `localhost` over http).
78
+ return new URL(origin).host === new URL(`http://${host}`).host;
79
+ }
80
+ catch {
81
+ return false; // `Origin: null`, or malformed
82
+ }
83
+ }
84
+ /** A request carries a body iff it declares one (RFC 9112 §6): Content-Length or Transfer-Encoding. */
85
+ function hasBody(c) {
86
+ const length = c.req.header('content-length');
87
+ if (length !== undefined)
88
+ return length.trim() !== '0';
89
+ return c.req.header('transfer-encoding') !== undefined;
90
+ }
91
+ function isJson(contentType) {
92
+ return contentType?.split(';')[0]?.trim().toLowerCase() === 'application/json';
93
+ }
94
+ function forbidden(c, error) {
95
+ return c.json({ error }, 403);
96
+ }
@@ -0,0 +1,35 @@
1
+ import { ForgeError } from '../forge/forge.js';
2
+ /** Error with an HTTP status, raised by route handlers and domain code. */
3
+ export class HttpError extends Error {
4
+ status;
5
+ code;
6
+ constructor(status, message, code) {
7
+ super(message);
8
+ this.status = status;
9
+ this.code = code;
10
+ this.name = 'HttpError';
11
+ }
12
+ }
13
+ export const notFound = (what) => new HttpError(404, `${what} not found`, 'not_found');
14
+ export const badRequest = (message) => new HttpError(400, message, 'bad_request');
15
+ /** Serialize any thrown value as an ApiError response. Register with `app.onError(onApiError)`. */
16
+ export function onApiError(err, c) {
17
+ if (err instanceof HttpError) {
18
+ return c.json({ error: err.message, ...(err.code ? { code: err.code } : {}) }, err.status);
19
+ }
20
+ if (err instanceof ForgeError) {
21
+ const status = (err.status >= 400 && err.status <= 599 ? err.status : 502);
22
+ return c.json({ error: err.message, code: 'github' }, status);
23
+ }
24
+ const anyErr = err;
25
+ if (anyErr?.name === 'ZodError') {
26
+ return c.json({ error: `invalid request: ${anyErr.message}`, code: 'bad_request' }, 400);
27
+ }
28
+ if (anyErr?.name === 'GitError') {
29
+ return c.json({ error: anyErr.message ?? 'git failed', code: 'git' }, 500);
30
+ }
31
+ // Anything else is a bug or an environment failure: log the full error for the operator,
32
+ // but never echo its message (paths, stack fragments) to the client.
33
+ console.error(err);
34
+ return c.json({ error: 'internal error' }, 500);
35
+ }
@@ -0,0 +1,51 @@
1
+ import { z } from 'zod';
2
+ import { addCloneProject, addLocalProject, removeProject } from '../projects/add.js';
3
+ import { getProject, listProjects } from '../projects/store.js';
4
+ import { badRequest, notFound } from './http.js';
5
+ /** Mirrors `AddProjectRequest` in shared/api.ts. */
6
+ const AddProjectSchema = z.discriminatedUnion('kind', [
7
+ z.object({ kind: z.literal('local'), path: z.string().trim().min(1) }),
8
+ z.object({ kind: z.literal('clone'), slug: z.string().trim().min(1) }),
9
+ ]);
10
+ /**
11
+ * `API_ROUTES.projects` and `API_ROUTES.project`. Register before any `/api/*`
12
+ * catch-all; errors are thrown and mapped by `onApiError`.
13
+ */
14
+ export function registerProjectRoutes(app, ctx) {
15
+ app.get('/api/projects', (c) => c.json(listProjects(ctx.db)));
16
+ app.post('/api/projects', async (c) => {
17
+ const body = validate(AddProjectSchema, await readJson(c));
18
+ const project = body.kind === 'local'
19
+ ? await addLocalProject(ctx, body.path)
20
+ : await addCloneProject(ctx, body.slug, { signal: c.req.raw.signal });
21
+ return c.json(project);
22
+ });
23
+ app.get('/api/projects/:projectId', (c) => {
24
+ const project = getProject(ctx.db, c.req.param('projectId'));
25
+ if (!project)
26
+ throw notFound('project');
27
+ return c.json(project);
28
+ });
29
+ app.delete('/api/projects/:projectId', async (c) => {
30
+ await removeProject(ctx, c.req.param('projectId'));
31
+ return c.json({ ok: true });
32
+ });
33
+ }
34
+ async function readJson(c) {
35
+ try {
36
+ return await c.req.json();
37
+ }
38
+ catch {
39
+ throw badRequest('request body must be JSON');
40
+ }
41
+ }
42
+ /** Validate a body or query with zod, turning failures into a 400 with a readable summary. */
43
+ export function validate(schema, value) {
44
+ const result = schema.safeParse(value);
45
+ if (result.success)
46
+ return result.data;
47
+ const issues = result.error.issues
48
+ .map((i) => (i.path.length ? `${i.path.join('.')}: ${i.message}` : i.message))
49
+ .join('; ');
50
+ throw badRequest(`invalid request: ${issues}`);
51
+ }