@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,64 @@
1
+ import { getProject } from '../projects/store.js';
2
+ import { isEmptyFilter, parsePrQuery, } from '../shared/api.js';
3
+ import { badRequest, notFound } from './http.js';
4
+ /**
5
+ * Hono path patterns behind API_ROUTES.prs / API_ROUTES.pr (shared/api.ts
6
+ * builds concrete URLs; these are the parameterised forms the router needs).
7
+ */
8
+ export const PRS_ROUTE = '/api/projects/:projectId/prs';
9
+ export const PR_ROUTE = `${PRS_ROUTE}/:number`;
10
+ /** The project named by `:projectId`, or a 404. */
11
+ export /** Trimmed param or undefined when empty. */ function clean(value) {
12
+ const trimmed = value?.trim();
13
+ return trimmed ? trimmed : undefined;
14
+ }
15
+ function resolveProject(ctx, c) {
16
+ const id = c.req.param('projectId');
17
+ const project = id ? getProject(ctx.db, id) : undefined;
18
+ if (!project)
19
+ throw notFound('project');
20
+ return { project, repo: { owner: project.owner, repo: project.repo } };
21
+ }
22
+ /** The project plus a validated `:number` (positive integer), or a 404 / 400. */
23
+ export function resolvePr(ctx, c) {
24
+ const scope = resolveProject(ctx, c);
25
+ const raw = c.req.param('number') ?? '';
26
+ const number = Number(raw);
27
+ if (!/^\d+$/.test(raw) || !Number.isSafeInteger(number) || number < 1) {
28
+ throw badRequest(`invalid pull request number "${raw}"`);
29
+ }
30
+ return { ...scope, number };
31
+ }
32
+ export function registerPullRoutes(app, ctx, deps) {
33
+ const { forge } = deps;
34
+ app.get(PRS_ROUTE, async (c) => {
35
+ const { repo } = resolveProject(ctx, c);
36
+ const parsed = Number.parseInt(c.req.query('page') ?? '1', 10);
37
+ const page = Number.isFinite(parsed) && parsed >= 1 ? parsed : 1;
38
+ // `q` is the GitHub-style query string (primary); the discrete params keep
39
+ // working for API callers that prefer structured input.
40
+ const q = clean(c.req.query('q'));
41
+ const draftParam = c.req.query('draft');
42
+ const filter = q
43
+ ? parsePrQuery(q)
44
+ : {
45
+ text: clean(c.req.query('text')),
46
+ author: clean(c.req.query('author')),
47
+ head: clean(c.req.query('head')),
48
+ base: clean(c.req.query('base')),
49
+ ...(draftParam === 'true'
50
+ ? { draft: true }
51
+ : draftParam === 'false'
52
+ ? { draft: false }
53
+ : {}),
54
+ };
55
+ // An empty filter means 'everything': pass undefined so the forge uses the plain list query.
56
+ const body = await forge.listPullRequestPage(repo, page, isEmptyFilter(filter) ? undefined : filter);
57
+ return c.json(body);
58
+ });
59
+ app.get(PR_ROUTE, async (c) => {
60
+ const { repo, number } = resolvePr(ctx, c);
61
+ const pr = await forge.getPullRequest(repo, number);
62
+ return c.json(pr);
63
+ });
64
+ }
@@ -0,0 +1,113 @@
1
+ import { z } from 'zod';
2
+ import { badRequest } from './http.js';
3
+ import { PR_ROUTE, resolvePr } from './pulls.js';
4
+ /**
5
+ * Review sync routes. Each handler is one explicit human action in the UI and
6
+ * goes straight to GitHub through the Forge — GitHub is the source of truth,
7
+ * nothing is stored locally. These are the only callers of the mutating Forge
8
+ * methods; the AI layer never gets a Forge ("the AI drafts, the human sends").
9
+ */
10
+ const side = z.enum(['LEFT', 'RIGHT']);
11
+ const nonBlank = (what) => z.string().refine((s) => s.trim().length > 0, { error: `${what} must not be blank` });
12
+ const addCommentSchema = z
13
+ .object({
14
+ path: z.string().min(1, { error: 'path is required' }),
15
+ body: nonBlank('body'),
16
+ line: z.number().int().min(1),
17
+ side,
18
+ startLine: z.number().int().min(1).optional(),
19
+ startSide: side.optional(),
20
+ })
21
+ .refine(
22
+ // Line numbers are only comparable within one side of the diff.
23
+ (v) => v.startLine === undefined || (v.startSide ?? v.side) !== v.side || v.startLine <= v.line, { error: 'startLine must not be greater than line', path: ['startLine'] });
24
+ const bodySchema = z.object({ body: nonBlank('body') });
25
+ const setViewedSchema = z.object({
26
+ path: z.string().min(1, { error: 'path is required' }),
27
+ viewed: z.boolean(),
28
+ });
29
+ const submitReviewSchema = z.object({
30
+ event: z.enum(['APPROVE', 'REQUEST_CHANGES', 'COMMENT']),
31
+ body: z.string().default(''),
32
+ });
33
+ async function parseBody(c, schema) {
34
+ let raw;
35
+ try {
36
+ raw = await c.req.json();
37
+ }
38
+ catch {
39
+ throw badRequest('request body must be JSON');
40
+ }
41
+ const result = schema.safeParse(raw);
42
+ if (!result.success) {
43
+ const issues = result.error.issues
44
+ .map((i) => (i.path.length ? `${i.path.map(String).join('.')}: ${i.message}` : i.message))
45
+ .join('; ');
46
+ throw badRequest(`invalid request: ${issues}`);
47
+ }
48
+ return result.data;
49
+ }
50
+ function requireParam(c, name) {
51
+ const value = c.req.param(name);
52
+ if (!value)
53
+ throw badRequest(`missing ${name}`);
54
+ return value;
55
+ }
56
+ export function registerReviewRoutes(app, ctx, deps) {
57
+ const { forge } = deps;
58
+ // Line/range comment → the viewer's pending review (created when missing).
59
+ app.post(`${PR_ROUTE}/comments`, async (c) => {
60
+ const { repo, number } = resolvePr(ctx, c);
61
+ const input = await parseBody(c, addCommentSchema);
62
+ const refs = await forge.getPullRequestRefs(repo, number);
63
+ const res = await forge.addPendingComment(repo, refs, input);
64
+ return c.json(res, 201);
65
+ });
66
+ // Comment and thread ids come from the client; each is tied to the route's PR before any
67
+ // mutation, so a request under one PR can never edit, delete or reply on another PR's thread.
68
+ app.patch(`${PR_ROUTE}/comments/:commentId`, async (c) => {
69
+ const { repo, number } = resolvePr(ctx, c);
70
+ const commentId = requireParam(c, 'commentId');
71
+ const { body } = await parseBody(c, bodySchema);
72
+ await forge.assertCommentInPullRequest(commentId, repo, number);
73
+ const comment = await forge.updateComment(commentId, body);
74
+ return c.json(comment);
75
+ });
76
+ app.delete(`${PR_ROUTE}/comments/:commentId`, async (c) => {
77
+ const { repo, number } = resolvePr(ctx, c);
78
+ const commentId = requireParam(c, 'commentId');
79
+ await forge.assertCommentInPullRequest(commentId, repo, number);
80
+ await forge.deleteComment(commentId);
81
+ return c.json({ ok: true });
82
+ });
83
+ // Replies publish immediately (decisions.md) unless the viewer has a pending review open, in
84
+ // which case GitHub attaches the reply to it; the response's comment.isPending tells the UI.
85
+ app.post(`${PR_ROUTE}/threads/:threadId/replies`, async (c) => {
86
+ const { repo, number } = resolvePr(ctx, c);
87
+ const threadId = requireParam(c, 'threadId');
88
+ const { body } = await parseBody(c, bodySchema);
89
+ await forge.assertThreadInPullRequest(threadId, repo, number);
90
+ const res = await forge.replyToThread(threadId, body);
91
+ return c.json(res, 201);
92
+ });
93
+ app.post(`${PR_ROUTE}/viewed`, async (c) => {
94
+ const { repo, number } = resolvePr(ctx, c);
95
+ const { path, viewed } = await parseBody(c, setViewedSchema);
96
+ const refs = await forge.getPullRequestRefs(repo, number);
97
+ const viewedState = await forge.setFileViewed(refs.id, path, viewed);
98
+ const res = { path, viewedState };
99
+ return c.json(res);
100
+ });
101
+ app.post(`${PR_ROUTE}/review`, async (c) => {
102
+ const { repo, number } = resolvePr(ctx, c);
103
+ const input = await parseBody(c, submitReviewSchema);
104
+ const refs = await forge.getPullRequestRefs(repo, number);
105
+ const res = await forge.submitReview(repo, refs, input);
106
+ return c.json(res);
107
+ });
108
+ app.delete(`${PR_ROUTE}/review`, async (c) => {
109
+ const { repo, number } = resolvePr(ctx, c);
110
+ await forge.discardPendingReview(repo, number);
111
+ return c.json({ ok: true });
112
+ });
113
+ }
@@ -0,0 +1,285 @@
1
+ import { z } from 'zod';
2
+ import { addCustomProviderSchema, deleteCustomProvider, getCustomProvider, listCustomProviders, saveCustomProvider, } from '../custom-providers.js';
3
+ import { settings } from '../db.js';
4
+ import { curatedModels } from '../secrets/providers.js';
5
+ import { providerKeyName } from '../secrets/store.js';
6
+ import { API_ROUTES, parseChatGptMarker, } from '../shared/api.js';
7
+ import { badRequest, HttpError } from './http.js';
8
+ /**
9
+ * Setup screen and AI-model routes (design.md, "Setup"). The GitHub step is
10
+ * read-only (`gh auth status`); the AI step stores provider keys in the
11
+ * SecretStore after validating them with the provider. Keys never leave the
12
+ * server: every response carries `configured: boolean` at most. The
13
+ * ChatGPT route is the one non-key path: the server runs the
14
+ * browser OAuth flow, tokens land in the SecretStore, and the browser sees
15
+ * connection facts only.
16
+ */
17
+ /** Settings key: '1' once the user saved a key or explicitly skipped the AI step. */
18
+ export const SETUP_COMPLETE_KEY = 'setup_complete';
19
+ export const MODEL_CACHE_TTL_MS = 10 * 60 * 1000;
20
+ export function registerSetupRoutes(app, ctx, deps) {
21
+ const { secrets } = deps;
22
+ const fetchImpl = deps.fetchImpl ?? fetch;
23
+ const ghStatus = async () => {
24
+ try {
25
+ return await deps.ghAuthStatus();
26
+ }
27
+ catch (err) {
28
+ const message = err instanceof Error ? err.message : String(err);
29
+ return { ok: false, error: `Could not check the GitHub CLI: ${message}` };
30
+ }
31
+ };
32
+ const chatgptStatus = async () => {
33
+ if (!deps.chatgpt)
34
+ return { connected: false };
35
+ const { signingIn: _signingIn, ...rest } = await deps.chatgpt.status();
36
+ return rest;
37
+ };
38
+ const status = async () => {
39
+ const [gh, chatgpt] = await Promise.all([ghStatus(), chatgptStatus()]);
40
+ return {
41
+ gh,
42
+ customProviders: await Promise.all(listCustomProviders(ctx.db).map(decorate)),
43
+ secrets: { backend: secrets.backend },
44
+ setupComplete: settings.get(ctx.db, SETUP_COMPLETE_KEY) === '1',
45
+ chatgpt,
46
+ };
47
+ };
48
+ /** Config + whether a key is stored — the API never returns key material. */
49
+ const decorate = async (p) => ({
50
+ ...p,
51
+ hasKey: Boolean(await secrets.get(providerKeyName(p.id))),
52
+ });
53
+ /**
54
+ * The subscription picker comes from the backend's model catalog (per-plan
55
+ * ground truth, incl. per-model reasoning efforts), gated on the connection.
56
+ * A catalog failure falls back to the short curated bootstrap list so the
57
+ * picker never empties.
58
+ */
59
+ const subscriptionModels = async () => {
60
+ if (!deps.chatgpt)
61
+ return [];
62
+ const state = await deps.chatgpt.status();
63
+ if (!state.connected)
64
+ return [];
65
+ try {
66
+ const models = await deps.chatgpt.listModels();
67
+ return models.map((m) => ({
68
+ id: `chatgpt:${m.slug}`,
69
+ provider: 'chatgpt',
70
+ modelId: m.slug,
71
+ label: m.displayName,
72
+ reasoningEfforts: m.reasoningEfforts.map((level) => ({
73
+ effort: level.effort,
74
+ ...(level.description ? { description: level.description } : {}),
75
+ })),
76
+ ...(m.defaultReasoningEffort ? { defaultReasoningEffort: m.defaultReasoningEffort } : {}),
77
+ }));
78
+ }
79
+ catch {
80
+ return curatedModels('chatgpt');
81
+ }
82
+ };
83
+ app.get(API_ROUTES.setupStatus, async (c) => c.json(await status()));
84
+ app.post(API_ROUTES.setupComplete, async (c) => {
85
+ settings.set(ctx.db, SETUP_COMPLETE_KEY, '1');
86
+ return c.json(await status());
87
+ });
88
+ // --- ChatGPT subscription ------------------------------------------------
89
+ /** Errors meant for the chat stream self-mark; Setup shows the bare text. */
90
+ const unmarked = (err) => {
91
+ const message = err instanceof Error ? err.message : String(err);
92
+ return parseChatGptMarker(message)?.text ?? message;
93
+ };
94
+ app.post(API_ROUTES.setupChatgptConnect, async (c) => {
95
+ if (!deps.chatgpt)
96
+ throw new HttpError(400, 'ChatGPT sign-in is not available', 'provider');
97
+ try {
98
+ const { authUrl } = await deps.chatgpt.connect();
99
+ return c.json({ ok: true, authUrl });
100
+ }
101
+ catch (err) {
102
+ if (err instanceof HttpError)
103
+ throw err;
104
+ throw new HttpError(400, unmarked(err), 'provider');
105
+ }
106
+ });
107
+ app.get(API_ROUTES.setupChatgptStatus, async (c) => {
108
+ if (!deps.chatgpt)
109
+ throw new HttpError(400, 'ChatGPT sign-in is not available', 'provider');
110
+ const { connected, signingIn, email, plan, authExpired, connectError } = await deps.chatgpt.status();
111
+ const body = {
112
+ status: connected ? 'connected' : signingIn ? 'connecting' : 'idle',
113
+ connected,
114
+ ...(email ? { email } : {}),
115
+ ...(plan ? { plan } : {}),
116
+ ...(authExpired ? { authExpired: true } : {}),
117
+ ...(connectError ? { connectError } : {}),
118
+ };
119
+ return c.json(body);
120
+ });
121
+ app.delete(API_ROUTES.setupChatgptDisconnect, async (c) => {
122
+ if (!deps.chatgpt)
123
+ throw new HttpError(400, 'ChatGPT sign-in is not available', 'provider');
124
+ await deps.chatgpt.disconnect();
125
+ return c.json({ ok: true });
126
+ });
127
+ app.get(API_ROUTES.aiModels, async (c) => {
128
+ const [subscription, custom] = await Promise.all([
129
+ subscriptionModels(),
130
+ Promise.resolve(listCustomProviders(ctx.db).flatMap((provider) => provider.models.map((model) => ({
131
+ id: `${provider.id}:${model.id}`,
132
+ provider: provider.id,
133
+ modelId: model.id,
134
+ // A display name from the endpoint's model list wins; otherwise
135
+ // the provider name plus the raw id.
136
+ label: model.label ?? `${provider.name} ${model.id}`,
137
+ providerLabel: provider.name,
138
+ })))),
139
+ ]);
140
+ return c.json([...subscription, ...custom]);
141
+ });
142
+ app.post(API_ROUTES.setupCustomProviders, async (c) => {
143
+ const request = parseOr400(addCustomProviderSchema, await readJson(c));
144
+ const { saved } = saveCustomProvider(ctx.db, request);
145
+ // Same id = intentional edit. A typed key stores/replaces; a blank one
146
+ // keeps whatever is stored (an edit that only renames must not drop the
147
+ // key — it is never shown again, so it could not be re-entered).
148
+ if (request.apiKey)
149
+ await secrets.set(providerKeyName(saved.id), request.apiKey);
150
+ const customProviders = await Promise.all(listCustomProviders(ctx.db).map(decorate));
151
+ return c.json({ customProviders });
152
+ });
153
+ // Raw segment (not the API_ROUTES helper): encodeURIComponent would escape the ':id' marker.
154
+ app.delete(`${API_ROUTES.setupCustomProviders}/:id`, async (c) => {
155
+ const id = c.req.param('id') ?? '';
156
+ if (!deleteCustomProvider(ctx.db, id))
157
+ throw new HttpError(404, `No custom provider "${id}"`);
158
+ await secrets.delete(providerKeyName(id));
159
+ return c.json({ ok: true });
160
+ });
161
+ const fetchModelsSchema = z
162
+ .object({
163
+ id: z
164
+ .string()
165
+ .regex(/^custom-[a-z0-9][a-z0-9-]*$/)
166
+ .optional(),
167
+ baseUrl: z.string().trim().url().optional(),
168
+ apiFormat: z.enum(['openai', 'anthropic']).optional(),
169
+ apiKey: z.string().max(4096).optional(),
170
+ })
171
+ .refine((v) => Boolean(v.id || (v.baseUrl && v.apiFormat)), {
172
+ message: 'need a provider id, or a base URL with an API format',
173
+ });
174
+ app.post(API_ROUTES.setupCustomModels, async (c) => {
175
+ const body = parseOr400(fetchModelsSchema, await readJson(c));
176
+ let baseUrl;
177
+ let apiFormat;
178
+ let apiKey = '';
179
+ if (body.id) {
180
+ const config = getCustomProvider(ctx.db, body.id);
181
+ if (!config)
182
+ throw new HttpError(404, `No custom provider "${body.id}"`);
183
+ baseUrl = config.baseUrl;
184
+ apiFormat = config.apiFormat;
185
+ // A typed key wins; otherwise the stored one — never shown, but usable.
186
+ apiKey = body.apiKey ?? (await secrets.get(providerKeyName(config.id))) ?? '';
187
+ }
188
+ else if (body.baseUrl && body.apiFormat) {
189
+ baseUrl = body.baseUrl;
190
+ apiFormat = body.apiFormat;
191
+ apiKey = body.apiKey ?? '';
192
+ }
193
+ else {
194
+ throw badRequest('need a provider id, or a base URL with an API format');
195
+ }
196
+ const url = `${baseUrl.replace(/\/+$/, '')}/models`;
197
+ const headers = apiFormat === 'openai'
198
+ ? apiKey
199
+ ? { authorization: `Bearer ${apiKey}` }
200
+ : {}
201
+ : { 'x-api-key': apiKey, 'anthropic-version': '2023-06-01' };
202
+ let res;
203
+ try {
204
+ res = await fetchImpl(url, {
205
+ headers,
206
+ signal: AbortSignal.timeout(10_000),
207
+ });
208
+ }
209
+ catch (err) {
210
+ const message = err instanceof Error ? err.message : String(err);
211
+ throw new HttpError(502, `Could not reach the model list at ${url} (${message}). Add models by hand.`, 'provider');
212
+ }
213
+ if (!res.ok) {
214
+ res.body?.cancel();
215
+ throw new HttpError(502, `The model list answered HTTP ${res.status}. The endpoint may not implement it — add models by hand.`, 'provider');
216
+ }
217
+ let parsed;
218
+ try {
219
+ parsed = await res.json();
220
+ }
221
+ catch {
222
+ throw new HttpError(502, 'The model list response was not JSON. Add models by hand.', 'provider');
223
+ }
224
+ // OpenAI shape: { data: [{ id }] }. Anthropic: { data: [{ id, display_name }] }.
225
+ // Some proxies use a bare array or { models: [...] } — accept all three.
226
+ const list = Array.isArray(parsed)
227
+ ? parsed
228
+ : typeof parsed === 'object' &&
229
+ parsed !== null &&
230
+ Array.isArray(parsed.data)
231
+ ? parsed.data
232
+ : typeof parsed === 'object' &&
233
+ parsed !== null &&
234
+ Array.isArray(parsed.models)
235
+ ? parsed.models
236
+ : null;
237
+ if (!list) {
238
+ throw new HttpError(502, 'The model list response was not recognised. Add models by hand.', 'provider');
239
+ }
240
+ const seen = new Set();
241
+ const models = [];
242
+ for (const entry of list) {
243
+ const id = typeof entry === 'string'
244
+ ? entry
245
+ : typeof entry === 'object' &&
246
+ entry !== null &&
247
+ typeof entry.id === 'string'
248
+ ? entry.id
249
+ : '';
250
+ const label = typeof entry === 'object' &&
251
+ entry !== null &&
252
+ typeof entry.display_name === 'string'
253
+ ? entry.display_name
254
+ : undefined;
255
+ if (!id || seen.has(id))
256
+ continue;
257
+ seen.add(id);
258
+ models.push(label ? { id, label } : { id });
259
+ if (models.length >= 64)
260
+ break;
261
+ }
262
+ if (models.length === 0) {
263
+ throw new HttpError(502, 'The model list was empty. Add models by hand.', 'provider');
264
+ }
265
+ return c.json({ models });
266
+ });
267
+ }
268
+ async function readJson(c) {
269
+ try {
270
+ return await c.req.json();
271
+ }
272
+ catch {
273
+ throw badRequest('expected a JSON body');
274
+ }
275
+ }
276
+ /** Validate with zod; a failure becomes a 400 whose message names the offending fields. */
277
+ function parseOr400(schema, input) {
278
+ const result = schema.safeParse(input);
279
+ if (result.success)
280
+ return result.data;
281
+ const details = result.error.issues
282
+ .map((issue) => `${issue.path.map(String).join('.') || 'body'}: ${issue.message}`)
283
+ .join('; ');
284
+ throw badRequest(`invalid request: ${details}`);
285
+ }
@@ -0,0 +1,27 @@
1
+ /**
2
+ * The ChatGPT subscription is the one non-key provider: users reach OpenAI
3
+ * and Anthropic (and anything else) through custom endpoints (Setup →
4
+ * "Add model provider"). This module keeps its display label and the short
5
+ * bootstrap model list used when the backend's catalog cannot be reached.
6
+ */
7
+ export const PROVIDER_LABELS = {
8
+ chatgpt: 'ChatGPT (subscription)',
9
+ };
10
+ /**
11
+ * Bootstrap-only fallback: the picker is normally driven by the backend's
12
+ * model catalog (`ChatGptConnection.listModels`), which is per-plan ground
13
+ * truth. This list — slugs verified live on a Plus account, 2026-09-05 —
14
+ * only covers a catalog outage before any mirror exists.
15
+ */
16
+ const CURATED_CHATGPT = [
17
+ { modelId: 'gpt-5.5', label: 'GPT-5.5' },
18
+ { modelId: 'gpt-5.4-mini', label: 'GPT-5.4 mini' },
19
+ ];
20
+ export function curatedModels(provider) {
21
+ return CURATED_CHATGPT.map(({ modelId, label }) => ({
22
+ id: `${provider}:${modelId}`,
23
+ provider,
24
+ modelId,
25
+ label,
26
+ }));
27
+ }
@@ -0,0 +1,190 @@
1
+ import { randomBytes } from 'node:crypto';
2
+ import { chmod, mkdir, readFile, rename, unlink, writeFile } from 'node:fs/promises';
3
+ import path from 'node:path';
4
+ import { z } from 'zod';
5
+ const PREFERENCES = ['auto', 'keychain', 'file'];
6
+ /** Service of every keychain item coja writes (`security find-generic-password -s coja -a <key>`). */
7
+ export const KEYCHAIN_SERVICE = 'coja';
8
+ /** Account read once on open to check the store works. Never written, so it never appears in the keychain. */
9
+ export const PROBE_ACCOUNT = '__coja_probe__';
10
+ /** File name of the fallback store inside the data dir. */
11
+ export const CREDENTIALS_FILE = 'credentials.json';
12
+ /** Keychain account / file key under which a provider's API key is stored. */
13
+ export const providerKeyName = (provider) => `${provider}-api-key`;
14
+ const loadNativeKeyring = () => import('@napi-rs/keyring');
15
+ export async function openSecretStore(opts) {
16
+ const env = opts.env ?? process.env;
17
+ const prefer = parsePreference(opts.prefer ?? env.COJA_CREDENTIAL_STORE ?? 'auto');
18
+ const platform = opts.platform ?? process.platform;
19
+ const warn = opts.warn ?? ((message) => console.warn(message));
20
+ const file = new FileSecretStore(opts.dataDir);
21
+ if (prefer === 'file')
22
+ return file;
23
+ if (prefer === 'auto' && platform === 'linux' && !env.DBUS_SESSION_BUS_ADDRESS) {
24
+ // No session bus means no Secret Service. keyring-rs would silently fall
25
+ // back to the kernel keyring, which is in-memory only, so a saved key would
26
+ // vanish on reboot. The file is the honest choice on a headless box.
27
+ return file;
28
+ }
29
+ try {
30
+ return await openKeychain(opts.loadKeyring ?? loadNativeKeyring);
31
+ }
32
+ catch (err) {
33
+ if (prefer === 'keychain')
34
+ throw err;
35
+ warn(`coja: OS keychain unavailable (${firstLine(err)}); storing API keys in ${file.path} (mode 0600)`);
36
+ return file;
37
+ }
38
+ }
39
+ function parsePreference(value) {
40
+ if (PREFERENCES.includes(value))
41
+ return value;
42
+ throw new Error(`COJA_CREDENTIAL_STORE must be one of ${PREFERENCES.join(', ')} (got ${JSON.stringify(value)})`);
43
+ }
44
+ // ---------------------------------------------------------------------------
45
+ // Keychain backend
46
+ // ---------------------------------------------------------------------------
47
+ async function openKeychain(load) {
48
+ // Throws "Cannot find native binding" when there is no prebuilt binary for this platform.
49
+ const { Entry } = await load();
50
+ const entry = (key) => new Entry(KEYCHAIN_SERVICE, key);
51
+ // A working store answers null for an account nobody wrote. A locked, denied
52
+ // or absent store throws — from the constructor on Linux, from the read on
53
+ // macOS — and that is exactly the signal to fall back on.
54
+ entry(PROBE_ACCOUNT).getPassword();
55
+ return {
56
+ backend: 'keychain',
57
+ async get(key) {
58
+ return entry(requireKey(key)).getPassword() ?? null;
59
+ },
60
+ async set(key, value) {
61
+ entry(requireKey(key)).setPassword(value);
62
+ },
63
+ async delete(key) {
64
+ return entry(requireKey(key)).deletePassword();
65
+ },
66
+ };
67
+ }
68
+ // ---------------------------------------------------------------------------
69
+ // File backend
70
+ // ---------------------------------------------------------------------------
71
+ const FileContents = z.object({
72
+ version: z.literal(1),
73
+ secrets: z.record(z.string(), z.string()),
74
+ });
75
+ /**
76
+ * `<dataDir>/credentials.json`, `{ version: 1, secrets: { <key>: <value> } }`.
77
+ * Every write goes to a temp file (mode 0600) that is renamed over the real
78
+ * one, so a crash never leaves a half-written or world-readable file. Calls
79
+ * are serialized within the process so concurrent sets cannot lose each other.
80
+ */
81
+ export class FileSecretStore {
82
+ dir;
83
+ backend = 'file';
84
+ path;
85
+ queue = Promise.resolve();
86
+ constructor(dir) {
87
+ this.dir = dir;
88
+ this.path = path.join(dir, CREDENTIALS_FILE);
89
+ }
90
+ async get(key) {
91
+ requireKey(key);
92
+ return this.serialized(async () => (await this.load()).get(key) ?? null);
93
+ }
94
+ async set(key, value) {
95
+ requireKey(key);
96
+ return this.serialized(async () => {
97
+ const secrets = await this.load();
98
+ secrets.set(key, value);
99
+ await this.save(secrets);
100
+ });
101
+ }
102
+ async delete(key) {
103
+ requireKey(key);
104
+ return this.serialized(async () => {
105
+ const secrets = await this.load();
106
+ const had = secrets.delete(key);
107
+ if (had)
108
+ await this.save(secrets);
109
+ return had;
110
+ });
111
+ }
112
+ serialized(fn) {
113
+ const run = this.queue.then(fn, fn);
114
+ this.queue = run.then(noop, noop);
115
+ return run;
116
+ }
117
+ async load() {
118
+ let raw;
119
+ try {
120
+ raw = await readFile(this.path, 'utf8');
121
+ }
122
+ catch (err) {
123
+ if (err.code === 'ENOENT')
124
+ return new Map();
125
+ throw err;
126
+ }
127
+ let json;
128
+ try {
129
+ json = JSON.parse(raw);
130
+ }
131
+ catch {
132
+ throw this.corrupt();
133
+ }
134
+ const parsed = FileContents.safeParse(json);
135
+ if (!parsed.success)
136
+ throw this.corrupt();
137
+ return new Map(Object.entries(parsed.data.secrets));
138
+ }
139
+ corrupt() {
140
+ return new Error(`${this.path} is not a valid coja credentials file. Fix it, or delete it and re-enter your API keys.`);
141
+ }
142
+ async save(secrets) {
143
+ await mkdir(this.dir, { recursive: true, mode: 0o700 });
144
+ // `mkdir`'s mode only applies when it creates the directory; tighten an existing one too.
145
+ await chmod(this.dir, 0o700).catch(noop);
146
+ const body = `${JSON.stringify({ version: 1, secrets: Object.fromEntries(secrets) }, null, 2)}\n`;
147
+ const tmp = `${this.path}.${process.pid}.${randomBytes(4).toString('hex')}.tmp`;
148
+ try {
149
+ await writeFile(tmp, body, { mode: 0o600, flag: 'wx' });
150
+ // writeFile's mode is subject to the umask; enforce 0600 before the file becomes visible.
151
+ await chmod(tmp, 0o600);
152
+ await rename(tmp, this.path);
153
+ }
154
+ catch (err) {
155
+ await unlink(tmp).catch(noop);
156
+ throw err;
157
+ }
158
+ }
159
+ }
160
+ // ---------------------------------------------------------------------------
161
+ // In-memory backend (tests)
162
+ // ---------------------------------------------------------------------------
163
+ export class MemorySecretStore {
164
+ backend;
165
+ values = new Map();
166
+ constructor(backend = 'keychain') {
167
+ this.backend = backend;
168
+ }
169
+ async get(key) {
170
+ return this.values.get(requireKey(key)) ?? null;
171
+ }
172
+ async set(key, value) {
173
+ this.values.set(requireKey(key), value);
174
+ }
175
+ async delete(key) {
176
+ return this.values.delete(requireKey(key));
177
+ }
178
+ }
179
+ // ---------------------------------------------------------------------------
180
+ function requireKey(key) {
181
+ if (typeof key !== 'string' || key.length === 0) {
182
+ throw new Error('secret key must be a non-empty string');
183
+ }
184
+ return key;
185
+ }
186
+ function firstLine(err) {
187
+ const message = err instanceof Error ? err.message : String(err);
188
+ return message.split('\n')[0] ?? message;
189
+ }
190
+ function noop() { }