@tequila99/githuman 0.2.2 → 0.3.0

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 (248) hide show
  1. package/README.md +27 -13
  2. package/dist/cli/commands/export.js +4 -3
  3. package/dist/cli/index.js +7 -4
  4. package/dist/cli/server-runtime.js +13 -1
  5. package/dist/cli/startup-message.js +3 -2
  6. package/dist/server/adapters/agents/event-mapper.js +140 -0
  7. package/dist/server/app.js +27 -14
  8. package/dist/server/config/agents.js +141 -0
  9. package/dist/server/errors/agents.js +12 -0
  10. package/dist/server/errors/comments.js +2 -0
  11. package/dist/server/errors/export.js +2 -0
  12. package/dist/server/errors/http.js +4 -0
  13. package/dist/server/errors/reviews.js +3 -0
  14. package/dist/server/hooks/agents/local-origin.js +33 -0
  15. package/dist/server/repositories/review.repo.js +1 -3
  16. package/dist/server/routes/agent.js +135 -0
  17. package/dist/server/routes/comments.js +25 -72
  18. package/dist/server/routes/diff.js +5 -6
  19. package/dist/server/routes/events.js +9 -3
  20. package/dist/server/routes/export.js +7 -16
  21. package/dist/server/routes/git.js +12 -21
  22. package/dist/server/routes/reviews.js +11 -43
  23. package/dist/server/services/agent/agent-session.js +497 -0
  24. package/dist/server/services/agent/context-builder.js +137 -0
  25. package/dist/server/services/agent/file-index.js +58 -0
  26. package/dist/server/services/agent/session-registry.js +100 -0
  27. package/dist/server/services/comment.service.js +1 -2
  28. package/dist/server/services/diff.service.js +2 -1
  29. package/dist/server/services/export.service.js +208 -28
  30. package/dist/server/services/git.service.js +9 -7
  31. package/dist/server/services/review.service.js +3 -3
  32. package/dist/server/utils/executable.js +26 -0
  33. package/dist/server/utils/file-name.js +6 -0
  34. package/dist/server/utils/sse.js +11 -0
  35. package/dist/server/utils/text.js +29 -0
  36. package/dist/shared/agents/chat-names.js +17 -0
  37. package/dist/shared/agents/constants.js +6 -0
  38. package/dist/shared/agents/context-format.js +17 -0
  39. package/dist/shared/agents/file-search.js +48 -0
  40. package/dist/shared/agents/schemas.js +51 -0
  41. package/dist/shared/agents/types.js +11 -0
  42. package/dist/shared/app/types.js +1 -0
  43. package/dist/shared/comments/types.js +1 -0
  44. package/dist/shared/events/types.js +14 -0
  45. package/dist/shared/git/types.js +1 -0
  46. package/dist/shared/http/types.js +1 -0
  47. package/dist/shared/network/loopback.js +4 -0
  48. package/dist/shared/reviews/types.js +1 -0
  49. package/dist/shared/utils/error-message.js +3 -0
  50. package/dist/shared/utils/guards.js +4 -0
  51. package/dist/web/assets/AppPage-CSb8txs5.js +1 -0
  52. package/dist/web/assets/CreateReviewFabButton-B18DLuH6.js +1 -0
  53. package/dist/web/assets/CreateReviewFabButton-Cufd8hVJ.css +1 -0
  54. package/dist/web/assets/{ErrorNotFound-DMYc7a3H.js → ErrorNotFound-B5uYICL4.js} +1 -1
  55. package/dist/web/assets/IndexPage-B-hkrnvt.css +1 -0
  56. package/dist/web/assets/IndexPage-Cm_ssBeC.js +1 -0
  57. package/dist/web/assets/MainLayout-ChxXEt_Q.css +1 -0
  58. package/dist/web/assets/MainLayout-ClBPF545.js +7 -0
  59. package/dist/web/assets/QBtn-E32fUOKx.js +1 -0
  60. package/dist/web/assets/QItemLabel-Co6oCEUr.js +1 -0
  61. package/dist/web/assets/QMenu-BZ1eK_rS.js +1 -0
  62. package/dist/web/assets/QSelect-D1V4tbDr.js +1 -0
  63. package/dist/web/assets/ReviewDetailPage-BWa3EEkv.css +1 -0
  64. package/dist/web/assets/ReviewDetailPage-Ct0hXwZL.js +1 -0
  65. package/dist/web/assets/ReviewsPage-CbLAU6OM.js +1 -0
  66. package/dist/web/assets/ReviewsPage-Lb74-4L2.css +1 -0
  67. package/dist/web/assets/StatusSwitcher-C0k4efwK.js +4 -0
  68. package/dist/web/assets/StatusSwitcher-CZYtI0ID.css +1 -0
  69. package/dist/web/assets/abnfDiagram-O67JEVCF-DkOrqZLn.js +1 -0
  70. package/dist/web/assets/agent-icon-BKoq2a6T.js +17 -0
  71. package/dist/web/assets/agent-icon-BUd-vCRS.css +1 -0
  72. package/dist/web/assets/agent-store-CEQ6MmTC.js +1 -0
  73. package/dist/web/assets/app-info-store-CIqmSeWK.js +1 -0
  74. package/dist/web/assets/arc-BALAYY3f.js +1 -0
  75. package/dist/web/assets/architecture-7GRP2DOG-CTkghkmX.js +1 -0
  76. package/dist/web/assets/architectureDiagram-NJMV4G6O-C-xtrMea.js +36 -0
  77. package/dist/web/assets/array-BifhSqXX.js +1 -0
  78. package/dist/web/assets/blockDiagram-BEXU5L5S-DYo9Os-m.js +140 -0
  79. package/dist/web/assets/c4Diagram-YGBWAQC7-BpSFjUAX.js +38 -0
  80. package/dist/web/assets/channel-f9cE6NV9.js +1 -0
  81. package/dist/web/assets/chunk-2Q5K7J3B-C1jixKkw.js +1 -0
  82. package/dist/web/assets/chunk-3FUC2YCW-DtUqbu5K.js +2 -0
  83. package/dist/web/assets/chunk-5DYCD2WN-DeYTIM8k.js +1 -0
  84. package/dist/web/assets/chunk-5VM5RSS4-ZNzvKenW.js +15 -0
  85. package/dist/web/assets/chunk-742MDFTN-8BfKAsu6.js +1 -0
  86. package/dist/web/assets/chunk-7INBJB4K-PXK092EZ.js +62 -0
  87. package/dist/web/assets/chunk-7M6MHVWA-Db9reT-V.js +213 -0
  88. package/dist/web/assets/chunk-7PRAP22T-CNFpjNaH.js +1 -0
  89. package/dist/web/assets/chunk-DUW6YSOI-Dgx8z5s3.js +1 -0
  90. package/dist/web/assets/chunk-FOHPRMQF-CoA4358r.js +161 -0
  91. package/dist/web/assets/chunk-GTNCS2PH-C5gqOp1W.js +88 -0
  92. package/dist/web/assets/chunk-GWA4HPMP-BquHLG8L.js +1 -0
  93. package/dist/web/assets/chunk-J5ZVWO5B-oDCd-gWO.js +1 -0
  94. package/dist/web/assets/chunk-JWPE2WC7-DVXcaiue.js +1 -0
  95. package/dist/web/assets/chunk-MBY4JIJT-DyOEFgCh.js +72 -0
  96. package/dist/web/assets/chunk-NETBCI7D-Bd3ij3-b.js +1 -0
  97. package/dist/web/assets/chunk-O7XYJQB3-DQvIvDe6.js +126 -0
  98. package/dist/web/assets/chunk-UA2S7LBM-BDIHwgCZ.js +1 -0
  99. package/dist/web/assets/chunk-WEXAMYUT-DwQKTsK-.js +1 -0
  100. package/dist/web/assets/chunk-XXDRQBXY-DTdiON6C.js +1 -0
  101. package/dist/web/assets/chunk-Y2CYZVJY-DsF7k-Jl.js +1 -0
  102. package/dist/web/assets/chunk-Z7XXMR3K-CIvnaGnU.js +10 -0
  103. package/dist/web/assets/chunk-ZIGJFQKS-BdT_T5CV.js +2 -0
  104. package/dist/web/assets/classDiagram-v2-NBCMYWYE-BUoclHlO.js +217 -0
  105. package/dist/web/assets/{core-Ruj7F7dx.js → core-B8kUiZoM.js} +1 -1
  106. package/dist/web/assets/cose-bilkent-JH36ORCC-C16eW6pc.js +1 -0
  107. package/dist/web/assets/create-B9ZqyTxV.js +1 -0
  108. package/dist/web/assets/cynefin-OW5HDTMX-1mS7ZeoM.js +1 -0
  109. package/dist/web/assets/cynefinDiagram-VND7K2PF-D1PC4YT4.js +62 -0
  110. package/dist/web/assets/cytoscape.esm-B-1iW2ft.js +321 -0
  111. package/dist/web/assets/dagre-6A5THRUB-amODOP74.js +4 -0
  112. package/dist/web/assets/debounce-CDIE7CV6.js +1 -0
  113. package/dist/web/assets/defaultLocale-BFoDCU3G.js +1 -0
  114. package/dist/web/assets/diagram-22UHCM2B-wUvcsN_B.js +200 -0
  115. package/dist/web/assets/diagram-3UASUU5V-CLiOVs2l.js +24 -0
  116. package/dist/web/assets/diagram-ATOU4E4O-Cs5cFGgz.js +3 -0
  117. package/dist/web/assets/diagram-CDSNMT55-C8Jb-YeE.js +30 -0
  118. package/dist/web/assets/diagram-MLGK6HIB-DA6C9fVB.js +24 -0
  119. package/dist/web/assets/diagram-MPIPVDR6-BOaFKuUi.js +41 -0
  120. package/dist/web/assets/dist-DuFVAX6Y.js +1 -0
  121. package/dist/web/assets/ebnfDiagram-ZINNZB2B-e5jvn-EE.js +1 -0
  122. package/dist/web/assets/elk-276RUBZZ-D7Gcu17D.js +27 -0
  123. package/dist/web/assets/engine-oniguruma-HbxpIAu4.js +1 -0
  124. package/dist/web/assets/erDiagram-OPXOYQCR-Cn1bBUF8.js +99 -0
  125. package/dist/web/assets/event-rR76ioXz.js +1 -0
  126. package/dist/web/assets/eventmodeling-NTZA5JFV-BjGpd42n.js +1 -0
  127. package/dist/web/assets/export-file-Di_2jvjP.js +1 -0
  128. package/dist/web/assets/fira-code-symbols2-400-normal-9X26I2LK.woff2 +0 -0
  129. package/dist/web/assets/fira-code-symbols2-500-normal-B5s_njw3.woff2 +0 -0
  130. package/dist/web/assets/fira-code-symbols2-600-normal-Bu3RnO39.woff2 +0 -0
  131. package/dist/web/assets/flowDiagram-KWPJA3E3-a3N8MVB8.js +1 -0
  132. package/dist/web/assets/ganttDiagram-FUAMR5RP-C3U8jeJB.js +292 -0
  133. package/dist/web/assets/gitGraph-4MIJSDKK-C165DC0H.js +1 -0
  134. package/dist/web/assets/gitGraphDiagram-X574FWY7-CVHdT1OC.js +106 -0
  135. package/dist/web/assets/golos-text-cyrillic-400-normal-BwL4n7Pb.woff +0 -0
  136. package/dist/web/assets/golos-text-cyrillic-400-normal-C7us6pn1.woff2 +0 -0
  137. package/dist/web/assets/golos-text-cyrillic-500-normal-BSLQUuP1.woff2 +0 -0
  138. package/dist/web/assets/golos-text-cyrillic-500-normal-hXinzVVQ.woff +0 -0
  139. package/dist/web/assets/golos-text-cyrillic-ext-400-normal-Bo67NYBq.woff2 +0 -0
  140. package/dist/web/assets/golos-text-cyrillic-ext-400-normal-D592xEk3.woff +0 -0
  141. package/dist/web/assets/golos-text-cyrillic-ext-500-normal-CLsu_eDI.woff2 +0 -0
  142. package/dist/web/assets/golos-text-cyrillic-ext-500-normal-InHNvB7v.woff +0 -0
  143. package/dist/web/assets/golos-text-latin-400-normal-Coi1FYaD.woff2 +0 -0
  144. package/dist/web/assets/golos-text-latin-400-normal-DOuJOmdK.woff +0 -0
  145. package/dist/web/assets/golos-text-latin-500-normal-BQo4s7Kn.woff +0 -0
  146. package/dist/web/assets/golos-text-latin-500-normal-BznAvurO.woff2 +0 -0
  147. package/dist/web/assets/golos-text-latin-ext-400-normal-BXgyX_iE.woff +0 -0
  148. package/dist/web/assets/golos-text-latin-ext-400-normal-BjKcJWKY.woff2 +0 -0
  149. package/dist/web/assets/golos-text-latin-ext-500-normal-9vZPKadr.woff2 +0 -0
  150. package/dist/web/assets/golos-text-latin-ext-500-normal-BmtMVntR.woff +0 -0
  151. package/dist/web/assets/graphlib-DS17s2tU.js +1 -0
  152. package/dist/web/assets/i18n-CyUPETur.js +1 -0
  153. package/dist/web/assets/{index-JAYe_SFI.css → index-C6ArNnKs.css} +1 -1
  154. package/dist/web/assets/index-C_B6Z2lc.js +2 -0
  155. package/dist/web/assets/info-A6RAGUB7-cbWtF3FJ.js +1 -0
  156. package/dist/web/assets/infoDiagram-VRGFBTTK-C_kWn3Ai.js +2 -0
  157. package/dist/web/assets/init-C-OQMol4.js +1 -0
  158. package/dist/web/assets/init-app-DnfYD-0k.js +1 -0
  159. package/dist/web/assets/is-ChHvR1Z-.js +1 -0
  160. package/dist/web/assets/ishikawaDiagram-OU5B5YK6-Xb9cDRr7.js +70 -0
  161. package/dist/web/assets/jetbrains-mono-cyrillic-ext-400-normal-Bh0R7Dhr.woff +0 -0
  162. package/dist/web/assets/jetbrains-mono-cyrillic-ext-400-normal-C_uLvvQ5.woff2 +0 -0
  163. package/dist/web/assets/jetbrains-mono-cyrillic-ext-500-normal-C9ShMxGR.woff2 +0 -0
  164. package/dist/web/assets/jetbrains-mono-cyrillic-ext-500-normal-CEVyPk4Y.woff +0 -0
  165. package/dist/web/assets/jetbrains-mono-cyrillic-ext-600-normal-DVvC-yKp.woff +0 -0
  166. package/dist/web/assets/jetbrains-mono-cyrillic-ext-600-normal-Dj6eZy5f.woff2 +0 -0
  167. package/dist/web/assets/jetbrains-mono-vietnamese-400-normal-ByoDsISC.woff2 +0 -0
  168. package/dist/web/assets/jetbrains-mono-vietnamese-500-normal-BvH7FW2L.woff2 +0 -0
  169. package/dist/web/assets/jetbrains-mono-vietnamese-600-normal-Dtizs43-.woff2 +0 -0
  170. package/dist/web/assets/journeyDiagram-ZHPQQLJL-6eZcefap.js +139 -0
  171. package/dist/web/assets/kanban-definition-PNTS6WVX-D_z_AsKR.js +89 -0
  172. package/dist/web/assets/katex-ZlcWpGUi.js +257 -0
  173. package/dist/web/assets/line-v7JtZ-Za.js +1 -0
  174. package/dist/web/assets/linear-hp1KWmiC.js +1 -0
  175. package/dist/web/assets/mermaid-parser.core-Cwwk5NqO.js +7 -0
  176. package/dist/web/assets/mermaid.core-BjPJhwW0.js +44 -0
  177. package/dist/web/assets/mindmap-definition-NLK3R4M7-lQohhDXq.js +96 -0
  178. package/dist/web/assets/ordinal-BDEzSJ7C.js +1 -0
  179. package/dist/web/assets/packet-AYTQ26CC-DxiGe6nt.js +1 -0
  180. package/dist/web/assets/path-fybaL0A-.js +1 -0
  181. package/dist/web/assets/pegDiagram-GJSIUBJH-XZRpQTLx.js +1 -0
  182. package/dist/web/assets/pie-WAS4IAKB-fxN0O90k.js +1 -0
  183. package/dist/web/assets/pieDiagram-5QR66LMP-M_bBKE-n.js +39 -0
  184. package/dist/web/assets/pinia-0B8sIOgD.js +1 -0
  185. package/dist/web/assets/pinia-DbmXdZbT.js +1 -0
  186. package/dist/web/assets/quadrantDiagram-O4NWA36T-BNxVPrq3.js +7 -0
  187. package/dist/web/assets/radar-RG4KPBEZ-Cd-5aokk.js +1 -0
  188. package/dist/web/assets/railroad-74A4TZTK-BvSKL8Ze.js +1 -0
  189. package/dist/web/assets/railroad-abnf-HS5TGJTU-CB32K25L.js +1 -0
  190. package/dist/web/assets/railroad-ebnf-LZEXJU2U-CqQR3zRl.js +1 -0
  191. package/dist/web/assets/railroad-peg-WCYAUIDC-DRSdB-oL.js +1 -0
  192. package/dist/web/assets/railroadDiagram-XR7U4H2S-D664lPSL.js +1 -0
  193. package/dist/web/assets/requirementDiagram-PLB6GJNP-B_1Dl0-3.js +84 -0
  194. package/dist/web/assets/rough.esm-Dy-Kn_BL.js +1 -0
  195. package/dist/web/assets/runtime-core.esm-bundler-Carm8LUR.js +1 -0
  196. package/dist/web/assets/safe-storage-CWaD6Dq3.js +1 -0
  197. package/dist/web/assets/sankeyDiagram-IPEJSGJF-BvFmMC_F.js +40 -0
  198. package/dist/web/assets/sequenceDiagram-PO4LG4MO-DMK1ey4g.js +169 -0
  199. package/dist/web/assets/single-flight-r8B7stt1.js +1 -0
  200. package/dist/web/assets/sizeCapture-INFHLROL-B0uUizjq.js +1 -0
  201. package/dist/web/assets/src-Cy0hT8a0.js +1 -0
  202. package/dist/web/assets/stateDiagram-v2-GCMORJYK-VVElU-_E.js +291 -0
  203. package/dist/web/assets/swimlanes-2SLR337P-DyfYJvHL.js +1 -0
  204. package/dist/web/assets/swimlanesDiagram-TC7HE7FX-BQ0mK2BI.js +8 -0
  205. package/dist/web/assets/theme-BFceB3b9.js +1 -0
  206. package/dist/web/assets/timeline-definition-EJHVYXUP-DBoRb7uC.js +120 -0
  207. package/dist/web/assets/treeView-Q6P3EWNA-D8AoZg1Y.js +1 -0
  208. package/dist/web/assets/treemap-WGGIJYW6-DhcVtZsu.js +1 -0
  209. package/dist/web/assets/use-app-theme-DxZW-eOR.js +1 -0
  210. package/dist/web/assets/use-panel-width-CY75Fomr.js +1 -0
  211. package/dist/web/assets/usecaseDiagram-POWQR4AR-BCfvWOH4.js +328 -0
  212. package/dist/web/assets/vennDiagram-UO4OBE2U-BIk6-FLo.js +34 -0
  213. package/dist/web/assets/{vue-i18n.runtime-DSeWNwrW.js → vue-i18n.runtime-BQwnBdmC.js} +2 -2
  214. package/dist/web/assets/wardley-WFR3VGLG-BMHH70A3.js +1 -0
  215. package/dist/web/assets/wardleyDiagram-VNRHLVJA-DB-cxlSq.js +78 -0
  216. package/dist/web/assets/xychartDiagram-PMCCYNJV-BejE-3gi.js +7 -0
  217. package/dist/web/index.html +8 -6
  218. package/package.json +18 -8
  219. package/dist/web/assets/AppPage-BeqKILJT.js +0 -1
  220. package/dist/web/assets/CreateReviewFabButton-BqWdgfEf.css +0 -1
  221. package/dist/web/assets/CreateReviewFabButton-wjxKeOL1.js +0 -1
  222. package/dist/web/assets/IndexPage-BQ0T9rLY.css +0 -1
  223. package/dist/web/assets/IndexPage-CSaToIzA.js +0 -1
  224. package/dist/web/assets/MainLayout-BRPoh8y0.css +0 -1
  225. package/dist/web/assets/MainLayout-xQ92tgGe.js +0 -1
  226. package/dist/web/assets/Platform-CEDL_PgC.js +0 -1
  227. package/dist/web/assets/QBtn-Cf6WtiKt.js +0 -1
  228. package/dist/web/assets/QTooltip-IUM54Nhj.js +0 -1
  229. package/dist/web/assets/ReviewDetailPage-DJHzinT1.css +0 -1
  230. package/dist/web/assets/ReviewDetailPage-DdufL7Rh.js +0 -1
  231. package/dist/web/assets/ReviewsPage-D8u1IeQ4.js +0 -1
  232. package/dist/web/assets/ReviewsPage-D9TFy2OP.css +0 -1
  233. package/dist/web/assets/StatusSwitcher-7Yqs8azq.css +0 -1
  234. package/dist/web/assets/StatusSwitcher-CUOMaD34.js +0 -3
  235. package/dist/web/assets/app-info-store-BpeVVjNB.js +0 -1
  236. package/dist/web/assets/client-De34x_P_.js +0 -1
  237. package/dist/web/assets/engine-oniguruma-Bon2Icja.js +0 -1
  238. package/dist/web/assets/event-BQt_DXaR.js +0 -1
  239. package/dist/web/assets/format-BWCY39oA.js +0 -1
  240. package/dist/web/assets/i18n-DHrLpaJV.js +0 -1
  241. package/dist/web/assets/index-DphTOTJs.js +0 -2
  242. package/dist/web/assets/init-app-5wT4mURp.js +0 -1
  243. package/dist/web/assets/pinia-CrBjT8X4.js +0 -1
  244. package/dist/web/assets/pinia-DK4OjQ7o.js +0 -1
  245. package/dist/web/assets/runtime-core.esm-bundler-Wxr8ezGp.js +0 -1
  246. package/dist/web/assets/theme-CrTbazqt.js +0 -1
  247. package/dist/web/assets/use-app-theme-CsAmLZfp.js +0 -1
  248. /package/dist/shared/{types.js → diff/types.js} +0 -0
@@ -0,0 +1,135 @@
1
+ import { NotFoundError } from "../errors/http.js";
2
+ import { SessionParams, PermissionParams, CreateSessionBody, AutoApproveBody, FileSearchQuery, PromptBody, PermissionBody, ConfigBody } from "../../shared/agents/schemas.js";
3
+ import { buildPromptBlocks } from "../services/agent/context-builder.js";
4
+ import { MAX_ATTACHMENT_BYTES, DEFAULT_FILE_SEARCH_LIMIT } from "../../shared/agents/constants.js";
5
+ import { createFileIndex } from "../services/agent/file-index.js";
6
+ import { safeSend } from "../utils/sse.js";
7
+ import { requireLocalOrigin } from "../hooks/agents/local-origin.js";
8
+ import { findReviewById } from "../repositories/review.repo.js";
9
+ /** Room for a handful of max-size base64 attachments; the schema caps each one. */
10
+ const PROMPT_BODY_LIMIT = 5 * Math.ceil((MAX_ATTACHMENT_BYTES * 4) / 3) + 1024 * 1024;
11
+ export async function agentRoutes(app, opts) {
12
+ const { registry, db, repositoryPath, eventBus } = opts;
13
+ const typedApp = app.withTypeProvider();
14
+ const fileIndex = createFileIndex(repositoryPath, eventBus);
15
+ app.addHook('onRequest', requireLocalOrigin);
16
+ app.addHook('onClose', async () => {
17
+ registry.closeAll();
18
+ fileIndex.close();
19
+ });
20
+ typedApp.get('/api/agent/presets', async () => registry.listPresets());
21
+ typedApp.get('/api/agent/sessions', async () => registry.list());
22
+ typedApp.post('/api/agent/sessions', { schema: { body: CreateSessionBody } }, async (request, reply) => {
23
+ const { presetId, reviewId, name } = request.body;
24
+ if (reviewId !== undefined && !findReviewById(db, reviewId)) {
25
+ throw new NotFoundError(`Review ${reviewId} not found`);
26
+ }
27
+ // The agent starts in the background; failure shows up as a closed session.
28
+ const session = registry.create(presetId, reviewId ?? null, name);
29
+ reply.code(201);
30
+ return session.info();
31
+ });
32
+ typedApp.get('/api/agent/sessions/:id', { schema: { params: SessionParams } }, async (request) => {
33
+ const session = registry.get(request.params.id);
34
+ if (!session) {
35
+ throw new NotFoundError('Session not found');
36
+ }
37
+ return session.info();
38
+ });
39
+ typedApp.delete('/api/agent/sessions/:id', { schema: { params: SessionParams } }, async (request, reply) => {
40
+ if (!registry.close(request.params.id)) {
41
+ throw new NotFoundError('Session not found');
42
+ }
43
+ return reply.code(204).send();
44
+ });
45
+ typedApp.post('/api/agent/sessions/:id/prompt', {
46
+ schema: { params: SessionParams, body: PromptBody },
47
+ bodyLimit: PROMPT_BODY_LIMIT
48
+ }, async (request, reply) => {
49
+ const session = registry.get(request.params.id);
50
+ if (!session) {
51
+ throw new NotFoundError('Session not found');
52
+ }
53
+ session.assertCanPrompt();
54
+ const blocks = await buildPromptBlocks(request.body, {
55
+ db,
56
+ repositoryPath,
57
+ embeddedContext: session.supportsEmbeddedContext,
58
+ image: session.supportsImages,
59
+ attachmentDir: session.attachmentDir
60
+ });
61
+ session.prompt(blocks, request.body);
62
+ return reply.code(202).send();
63
+ });
64
+ typedApp.post('/api/agent/sessions/:id/cancel', { schema: { params: SessionParams } }, async (request, reply) => {
65
+ const session = registry.get(request.params.id);
66
+ if (!session) {
67
+ throw new NotFoundError('Session not found');
68
+ }
69
+ await session.cancel();
70
+ return reply.code(204).send();
71
+ });
72
+ typedApp.post('/api/agent/sessions/:id/permissions/:requestId', { schema: { params: PermissionParams, body: PermissionBody } }, async (request, reply) => {
73
+ const session = registry.get(request.params.id);
74
+ if (!session?.answerPermission(request.params.requestId, request.body.optionId)) {
75
+ throw new NotFoundError('No such pending permission request');
76
+ }
77
+ return reply.code(204).send();
78
+ });
79
+ typedApp.post('/api/agent/sessions/:id/config', { schema: { params: SessionParams, body: ConfigBody } }, async (request, reply) => {
80
+ const session = registry.get(request.params.id);
81
+ if (!session) {
82
+ throw new NotFoundError('Session not found');
83
+ }
84
+ await session.setConfig(request.body.configId, request.body.value);
85
+ return reply.code(204).send();
86
+ });
87
+ typedApp.post('/api/agent/sessions/:id/auto-approve', { schema: { params: SessionParams, body: AutoApproveBody } }, async (request, reply) => {
88
+ const session = registry.get(request.params.id);
89
+ if (!session) {
90
+ throw new NotFoundError('Session not found');
91
+ }
92
+ session.setAutoApprove(request.body.enabled);
93
+ return reply.code(204).send();
94
+ });
95
+ typedApp.get('/api/agent/files', { schema: { querystring: FileSearchQuery } }, async (request) => ({
96
+ files: await fileIndex.search(request.query.q ?? '', request.query.limit ?? DEFAULT_FILE_SEARCH_LIMIT)
97
+ }));
98
+ // One stream for every chat (ADR 0024): browsers allow ~6 connections per
99
+ // origin over HTTP/1.1, so a stream per session would starve the page's own
100
+ // requests. Event ids are shared across sessions, so `Last-Event-ID` works.
101
+ typedApp.get('/api/agent/events', { sse: true }, async (request, reply) => {
102
+ const lastEventId = Number(request.headers['last-event-id']);
103
+ reply.sse.keepAlive();
104
+ // Flushes the headers: with no session yet nothing else would be sent, and
105
+ // the browser would not consider the stream open.
106
+ await reply.sse.send({ event: 'connected', data: {} });
107
+ // The client can leave during the await. `onClose` callbacks run once, so
108
+ // one added after the close would never run and the listener would leak.
109
+ if (!reply.sse.isConnected) {
110
+ return;
111
+ }
112
+ const unsubscribe = registry.subscribe(message => {
113
+ if (message.type === 'event') {
114
+ safeSend(reply.sse, {
115
+ id: String(message.envelope.id),
116
+ event: 'agent',
117
+ data: message.envelope
118
+ });
119
+ }
120
+ else if (message.type === 'gap') {
121
+ safeSend(reply.sse, {
122
+ event: 'gap',
123
+ data: { sessionId: message.sessionId }
124
+ });
125
+ }
126
+ else if (message.type === 'state') {
127
+ safeSend(reply.sse, { event: 'state', data: message.state });
128
+ }
129
+ else {
130
+ safeSend(reply.sse, { event: 'sessions', data: {} });
131
+ }
132
+ }, Number.isInteger(lastEventId) ? lastEventId : 0);
133
+ reply.sse.onClose(unsubscribe);
134
+ });
135
+ }
@@ -1,18 +1,22 @@
1
+ import { NotFoundError } from "../errors/http.js";
1
2
  import { Type } from '@sinclair/typebox';
2
3
  import { getReview } from "../services/review.service.js";
3
- import { createComment, getComments, editComment, removeComment, resolveComment, unresolveComment, ValidationError } from "../services/comment.service.js";
4
+ import { createComment, getComments, editComment, removeComment, resolveComment, unresolveComment } from "../services/comment.service.js";
5
+ // Null goes first in each union: Fastify's Ajv coerces types, and with
6
+ // Integer (or String) first it turns a JSON null into 0 (or "") before the
7
+ // Null branch is ever tried — a file-level comment would be stored as line 0.
4
8
  const CreateCommentBody = Type.Object({
5
9
  filePath: Type.String({ minLength: 1 }),
6
- lineNumber: Type.Optional(Type.Union([Type.Integer(), Type.Null()])),
7
- lineNumberEnd: Type.Optional(Type.Union([Type.Integer(), Type.Null()])),
10
+ lineNumber: Type.Optional(Type.Union([Type.Null(), Type.Integer()])),
11
+ lineNumberEnd: Type.Optional(Type.Union([Type.Null(), Type.Integer()])),
8
12
  lineType: Type.Optional(Type.Union([
13
+ Type.Null(),
9
14
  Type.Literal('added'),
10
15
  Type.Literal('removed'),
11
- Type.Literal('context'),
12
- Type.Null()
16
+ Type.Literal('context')
13
17
  ])),
14
18
  content: Type.String(),
15
- suggestion: Type.Optional(Type.Union([Type.String(), Type.Null()]))
19
+ suggestion: Type.Optional(Type.Union([Type.Null(), Type.String()]))
16
20
  });
17
21
  const UpdateCommentBody = Type.Object({
18
22
  content: Type.String()
@@ -24,87 +28,36 @@ export async function commentRoutes(app, opts) {
24
28
  const typedApp = app.withTypeProvider();
25
29
  typedApp.post('/api/reviews/:id/comments', { schema: { params: ReviewIdParams, body: CreateCommentBody } }, async (request, reply) => {
26
30
  if (!getReview(db, request.params.id)) {
27
- reply.code(404);
28
- return {
29
- error: 'Not Found',
30
- message: `Review ${request.params.id} not found`,
31
- statusCode: 404
32
- };
33
- }
34
- try {
35
- const comment = createComment(db, request.params.id, request.body, eventBus);
36
- reply.code(201);
37
- return comment;
38
- }
39
- catch (error) {
40
- if (error instanceof ValidationError) {
41
- reply.code(400);
42
- return {
43
- error: 'Bad Request',
44
- message: error.message,
45
- statusCode: 400
46
- };
47
- }
48
- throw error;
31
+ throw new NotFoundError(`Review ${request.params.id} not found`);
49
32
  }
33
+ const comment = createComment(db, request.params.id, request.body, eventBus);
34
+ reply.code(201);
35
+ return comment;
50
36
  });
51
- typedApp.get('/api/reviews/:id/comments', { schema: { params: ReviewIdParams } }, async (request, reply) => {
37
+ typedApp.get('/api/reviews/:id/comments', { schema: { params: ReviewIdParams } }, async (request) => {
52
38
  if (!getReview(db, request.params.id)) {
53
- reply.code(404);
54
- return {
55
- error: 'Not Found',
56
- message: `Review ${request.params.id} not found`,
57
- statusCode: 404
58
- };
39
+ throw new NotFoundError(`Review ${request.params.id} not found`);
59
40
  }
60
41
  return getComments(db, request.params.id);
61
42
  });
62
- typedApp.patch('/api/comments/:id', { schema: { params: CommentIdParams, body: UpdateCommentBody } }, async (request, reply) => {
63
- try {
64
- const updated = editComment(db, request.params.id, request.body.content, eventBus);
65
- if (!updated) {
66
- reply.code(404);
67
- return {
68
- error: 'Not Found',
69
- message: `Comment ${request.params.id} not found`,
70
- statusCode: 404
71
- };
72
- }
73
- return updated;
74
- }
75
- catch (error) {
76
- if (error instanceof ValidationError) {
77
- reply.code(400);
78
- return {
79
- error: 'Bad Request',
80
- message: error.message,
81
- statusCode: 400
82
- };
83
- }
84
- throw error;
43
+ typedApp.patch('/api/comments/:id', { schema: { params: CommentIdParams, body: UpdateCommentBody } }, async (request) => {
44
+ const updated = editComment(db, request.params.id, request.body.content, eventBus);
45
+ if (!updated) {
46
+ throw new NotFoundError(`Comment ${request.params.id} not found`);
85
47
  }
48
+ return updated;
86
49
  });
87
- typedApp.patch('/api/comments/:id/resolve', { schema: { params: CommentIdParams } }, async (request, reply) => {
50
+ typedApp.patch('/api/comments/:id/resolve', { schema: { params: CommentIdParams } }, async (request) => {
88
51
  const updated = resolveComment(db, request.params.id, eventBus);
89
52
  if (!updated) {
90
- reply.code(404);
91
- return {
92
- error: 'Not Found',
93
- message: `Comment ${request.params.id} not found`,
94
- statusCode: 404
95
- };
53
+ throw new NotFoundError(`Comment ${request.params.id} not found`);
96
54
  }
97
55
  return updated;
98
56
  });
99
- typedApp.patch('/api/comments/:id/unresolve', { schema: { params: CommentIdParams } }, async (request, reply) => {
57
+ typedApp.patch('/api/comments/:id/unresolve', { schema: { params: CommentIdParams } }, async (request) => {
100
58
  const updated = unresolveComment(db, request.params.id, eventBus);
101
59
  if (!updated) {
102
- reply.code(404);
103
- return {
104
- error: 'Not Found',
105
- message: `Comment ${request.params.id} not found`,
106
- statusCode: 404
107
- };
60
+ throw new NotFoundError(`Comment ${request.params.id} not found`);
108
61
  }
109
62
  return updated;
110
63
  });
@@ -1,3 +1,4 @@
1
+ import { BadRequestError } from "../errors/http.js";
1
2
  import { Type } from '@sinclair/typebox';
2
3
  import { getStagedDiff, getUnstagedDiff, getBranchDiff, getCommitsDiff } from "../services/diff.service.js";
3
4
  const BranchQuery = Type.Object({
@@ -16,24 +17,22 @@ export async function diffRoutes(app, opts) {
16
17
  typedApp.get('/api/diff/unstaged', async () => {
17
18
  return getUnstagedDiff(repositoryPath);
18
19
  });
19
- typedApp.get('/api/diff/branch', { schema: { querystring: BranchQuery } }, async (request, reply) => {
20
+ typedApp.get('/api/diff/branch', { schema: { querystring: BranchQuery } }, async (request) => {
20
21
  try {
21
22
  return await getBranchDiff(repositoryPath, request.query.base);
22
23
  }
23
24
  catch (err) {
24
- reply.code(400);
25
25
  const message = err instanceof Error ? err.message : 'Bad ref';
26
- return { error: 'Bad Request', message, statusCode: 400 };
26
+ throw new BadRequestError(message, { cause: err });
27
27
  }
28
28
  });
29
- typedApp.get('/api/diff/commits', { schema: { querystring: CommitsQuery } }, async (request, reply) => {
29
+ typedApp.get('/api/diff/commits', { schema: { querystring: CommitsQuery } }, async (request) => {
30
30
  try {
31
31
  return await getCommitsDiff(repositoryPath, request.query.from, request.query.to);
32
32
  }
33
33
  catch (err) {
34
- reply.code(400);
35
34
  const message = err instanceof Error ? err.message : 'Bad ref';
36
- return { error: 'Bad Request', message, statusCode: 400 };
35
+ throw new BadRequestError(message, { cause: err });
37
36
  }
38
37
  });
39
38
  }
@@ -1,10 +1,16 @@
1
+ import { safeSend } from "../utils/sse.js";
1
2
  export async function eventRoutes(app, opts) {
2
- const { eventBus } = opts;
3
+ const { eventBus, instanceId } = opts;
4
+ const hello = { instanceId };
3
5
  app.get('/api/events', { sse: true }, async (request, reply) => {
4
6
  reply.sse.keepAlive();
5
- await reply.sse.send({ event: 'connected', data: {} });
7
+ await reply.sse.send({ event: 'connected', data: hello });
8
+ // The client can leave during the await; a later `onClose` would never run.
9
+ if (!reply.sse.isConnected) {
10
+ return;
11
+ }
6
12
  const unsubscribe = eventBus.subscribe(event => {
7
- void reply.sse.send({
13
+ safeSend(reply.sse, {
8
14
  event: event.type,
9
15
  data: { reviewId: event.reviewId }
10
16
  });
@@ -1,27 +1,18 @@
1
1
  import { Type } from '@sinclair/typebox';
2
- import { exportAsJson, exportAsMarkdown, ExportNotFoundError } from "../services/export.service.js";
2
+ import { exportAsJson, exportAsMarkdown } from "../services/export.service.js";
3
3
  const ExportParams = Type.Object({ id: Type.String() });
4
4
  const ExportQuery = Type.Object({
5
5
  format: Type.Union([Type.Literal('json'), Type.Literal('markdown')])
6
6
  });
7
7
  export async function exportRoutes(app, opts) {
8
- const { db } = opts;
8
+ const { db, repositoryPath } = opts;
9
9
  const typedApp = app.withTypeProvider();
10
10
  typedApp.get('/api/reviews/:id/export', { schema: { params: ExportParams, querystring: ExportQuery } }, async (request, reply) => {
11
- try {
12
- if (request.query.format === 'json') {
13
- return exportAsJson(db, request.params.id);
14
- }
15
- const markdown = exportAsMarkdown(db, request.params.id);
16
- reply.type('text/markdown');
17
- return markdown;
18
- }
19
- catch (error) {
20
- if (error instanceof ExportNotFoundError) {
21
- reply.code(404);
22
- return { error: 'Not Found', message: error.message, statusCode: 404 };
23
- }
24
- throw error;
11
+ if (request.query.format === 'json') {
12
+ return exportAsJson(db, request.params.id);
25
13
  }
14
+ const markdown = await exportAsMarkdown(db, request.params.id, repositoryPath);
15
+ reply.type('text/markdown');
16
+ return markdown;
26
17
  });
27
18
  }
@@ -1,3 +1,4 @@
1
+ import { BadRequestError } from "../errors/http.js";
1
2
  import { Type } from '@sinclair/typebox';
2
3
  import { getRepositoryInfo, stagePaths, unstagePaths, discardPaths, getFilesAtRef, getFileAtRef } from "../services/git.service.js";
3
4
  const TreeParams = Type.Object({
@@ -20,61 +21,52 @@ export async function gitRoutes(app, opts) {
20
21
  app.get('/api/git/info', async () => {
21
22
  return getRepositoryInfo(repositoryPath);
22
23
  });
23
- typedApp.post('/api/git/stage', { schema: { body: OptionalPathsBody } }, async (request, reply) => {
24
+ typedApp.post('/api/git/stage', { schema: { body: OptionalPathsBody } }, async (request) => {
24
25
  try {
25
26
  await stagePaths(repositoryPath, request.body.paths ?? []);
26
27
  return { ok: true };
27
28
  }
28
29
  catch (err) {
29
- reply.code(400);
30
30
  const message = err instanceof Error ? err.message : 'Failed to stage';
31
- return { error: 'Bad Request', message, statusCode: 400 };
31
+ throw new BadRequestError(message, { cause: err });
32
32
  }
33
33
  });
34
- typedApp.post('/api/git/unstage', { schema: { body: OptionalPathsBody } }, async (request, reply) => {
34
+ typedApp.post('/api/git/unstage', { schema: { body: OptionalPathsBody } }, async (request) => {
35
35
  try {
36
36
  await unstagePaths(repositoryPath, request.body.paths ?? []);
37
37
  return { ok: true };
38
38
  }
39
39
  catch (err) {
40
- reply.code(400);
41
40
  const message = err instanceof Error ? err.message : 'Failed to unstage';
42
- return { error: 'Bad Request', message, statusCode: 400 };
41
+ throw new BadRequestError(message, { cause: err });
43
42
  }
44
43
  });
45
- typedApp.post('/api/git/discard', { schema: { body: DiscardBody } }, async (request, reply) => {
44
+ typedApp.post('/api/git/discard', { schema: { body: DiscardBody } }, async (request) => {
46
45
  try {
47
46
  await discardPaths(repositoryPath, request.body.paths);
48
47
  return { ok: true };
49
48
  }
50
49
  catch (err) {
51
- reply.code(400);
52
50
  const message = err instanceof Error ? err.message : 'Failed to discard changes';
53
- return { error: 'Bad Request', message, statusCode: 400 };
51
+ throw new BadRequestError(message, { cause: err });
54
52
  }
55
53
  });
56
- typedApp.get('/api/git/tree/:ref', { schema: { params: TreeParams } }, async (request, reply) => {
54
+ typedApp.get('/api/git/tree/:ref', { schema: { params: TreeParams } }, async (request) => {
57
55
  const { ref } = request.params;
58
56
  try {
59
57
  const files = await getFilesAtRef(repositoryPath, ref);
60
58
  return { ref, files };
61
59
  }
62
60
  catch (err) {
63
- reply.code(400);
64
61
  const message = err instanceof Error ? err.message : 'Failed to list files';
65
- return { error: 'Bad Request', message, statusCode: 400 };
62
+ throw new BadRequestError(message, { cause: err });
66
63
  }
67
64
  });
68
- typedApp.get('/api/git/file/*', { schema: { querystring: FileContentQuery } }, async (request, reply) => {
65
+ typedApp.get('/api/git/file/*', { schema: { querystring: FileContentQuery } }, async (request) => {
69
66
  const filePath = request.params['*'];
70
67
  const { ref } = request.query;
71
68
  if (!filePath) {
72
- reply.code(400);
73
- return {
74
- error: 'Bad Request',
75
- message: 'File path is required',
76
- statusCode: 400
77
- };
69
+ throw new BadRequestError('File path is required');
78
70
  }
79
71
  let content;
80
72
  let isBinary;
@@ -83,9 +75,8 @@ export async function gitRoutes(app, opts) {
83
75
  ({ content, isBinary } = await getFileAtRef(repositoryPath, ref, filePath));
84
76
  }
85
77
  catch (err) {
86
- reply.code(400);
87
78
  const message = err instanceof Error ? err.message : 'Failed to read file';
88
- return { error: 'Bad Request', message, statusCode: 400 };
79
+ throw new BadRequestError(message, { cause: err });
89
80
  }
90
81
  const lines = isBinary ? [] : content.split('\n');
91
82
  return {
@@ -1,5 +1,6 @@
1
+ import { NotFoundError, BadRequestError } from "../errors/http.js";
1
2
  import { Type } from '@sinclair/typebox';
2
- import { createReview, getReview, getReviews, setReviewStatus, removeReview, ValidationError } from "../services/review.service.js";
3
+ import { createReview, getReview, getReviews, setReviewStatus, removeReview } from "../services/review.service.js";
3
4
  import { getRepositoryInfo } from "../services/git.service.js";
4
5
  const CreateReviewBody = Type.Object({
5
6
  sourceType: Type.Optional(Type.Union([
@@ -33,22 +34,9 @@ export async function reviewRoutes(app, opts) {
33
34
  const { repositoryPath, db, eventBus } = opts;
34
35
  const typedApp = app.withTypeProvider();
35
36
  typedApp.post('/api/reviews', { schema: { body: CreateReviewBody } }, async (request, reply) => {
36
- try {
37
- const review = await createReview(db, repositoryPath, request.body, eventBus);
38
- reply.code(201);
39
- return review;
40
- }
41
- catch (error) {
42
- if (error instanceof ValidationError) {
43
- reply.code(400);
44
- return {
45
- error: 'Bad Request',
46
- message: error.message,
47
- statusCode: 400
48
- };
49
- }
50
- throw error;
51
- }
37
+ const review = await createReview(db, repositoryPath, request.body, eventBus);
38
+ reply.code(201);
39
+ return review;
52
40
  });
53
41
  typedApp.get('/api/reviews', { schema: { querystring: ReviewsQuery } }, async (request) => {
54
42
  const { branch, search, createdFrom, createdTo, files } = request.query;
@@ -63,46 +51,26 @@ export async function reviewRoutes(app, opts) {
63
51
  : undefined
64
52
  });
65
53
  });
66
- typedApp.get('/api/reviews/:id', { schema: { params: ReviewIdParams } }, async (request, reply) => {
54
+ typedApp.get('/api/reviews/:id', { schema: { params: ReviewIdParams } }, async (request) => {
67
55
  const review = getReview(db, request.params.id);
68
56
  if (!review) {
69
- reply.code(404);
70
- return {
71
- error: 'Not Found',
72
- message: `Review ${request.params.id} not found`,
73
- statusCode: 404
74
- };
57
+ throw new NotFoundError(`Review ${request.params.id} not found`);
75
58
  }
76
59
  return review;
77
60
  });
78
- typedApp.patch('/api/reviews/:id', { schema: { params: ReviewIdParams, body: UpdateReviewBody } }, async (request, reply) => {
61
+ typedApp.patch('/api/reviews/:id', { schema: { params: ReviewIdParams, body: UpdateReviewBody } }, async (request) => {
79
62
  if (!request.body.status) {
80
- reply.code(400);
81
- return {
82
- error: 'Bad Request',
83
- message: '"status" is required',
84
- statusCode: 400
85
- };
63
+ throw new BadRequestError('"status" is required');
86
64
  }
87
65
  const updated = setReviewStatus(db, request.params.id, request.body.status, eventBus);
88
66
  if (!updated) {
89
- reply.code(404);
90
- return {
91
- error: 'Not Found',
92
- message: `Review ${request.params.id} not found`,
93
- statusCode: 404
94
- };
67
+ throw new NotFoundError(`Review ${request.params.id} not found`);
95
68
  }
96
69
  return updated;
97
70
  });
98
71
  typedApp.delete('/api/reviews/:id', { schema: { params: ReviewIdParams } }, async (request, reply) => {
99
72
  if (!getReview(db, request.params.id)) {
100
- reply.code(404);
101
- return {
102
- error: 'Not Found',
103
- message: `Review ${request.params.id} not found`,
104
- statusCode: 404
105
- };
73
+ throw new NotFoundError(`Review ${request.params.id} not found`);
106
74
  }
107
75
  removeReview(db, request.params.id, eventBus);
108
76
  reply.code(204);