@tequila99/githuman 0.4.0 → 0.4.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (168) hide show
  1. package/README.md +8 -1
  2. package/dist/cli/config.js +4 -2
  3. package/dist/cli/server-runtime.js +1 -0
  4. package/dist/server/app.js +10 -0
  5. package/dist/server/config/openapi.js +29 -0
  6. package/dist/server/repositories/review.repo.js +31 -5
  7. package/dist/server/routes/agent.js +145 -18
  8. package/dist/server/routes/app-info.js +8 -1
  9. package/dist/server/routes/comments.js +73 -27
  10. package/dist/server/routes/diff.js +58 -31
  11. package/dist/server/routes/events.js +14 -1
  12. package/dist/server/routes/export.js +22 -5
  13. package/dist/server/routes/git.js +52 -21
  14. package/dist/server/routes/health.js +8 -1
  15. package/dist/server/routes/openapi.js +4 -0
  16. package/dist/server/routes/reviews.js +59 -33
  17. package/dist/server/services/agent/agent-session.js +5 -3
  18. package/dist/server/services/diff.service.js +23 -1
  19. package/dist/shared/agents/schemas.js +120 -31
  20. package/dist/shared/app/schemas.js +7 -0
  21. package/dist/shared/comments/schemas.js +48 -0
  22. package/dist/shared/diff/preamble.js +77 -0
  23. package/dist/shared/diff/schemas.js +80 -0
  24. package/dist/shared/diff/types.js +0 -3
  25. package/dist/shared/events/schemas.js +4 -0
  26. package/dist/shared/git/schemas.js +59 -0
  27. package/dist/shared/http/schemas.js +23 -0
  28. package/dist/shared/reviews/schemas.js +96 -0
  29. package/dist/shared/reviews/summary.js +5 -0
  30. package/dist/shared/utils/schemas.js +9 -0
  31. package/dist/web/assets/AppPage-BYrCfRnC.js +93 -0
  32. package/dist/web/assets/CreateReviewFabButton-UdzUa_ra.js +1 -0
  33. package/dist/web/assets/IndexPage-BtoeYBg2.js +1 -0
  34. package/dist/web/assets/{IndexPage-CztuM7ha.css → IndexPage-D894ONbY.css} +1 -1
  35. package/dist/web/assets/MainLayout-DjoItVv0.js +7 -0
  36. package/dist/web/assets/{QMenu-QYPSkHBE.js → QMenu-Bh-SjGqz.js} +1 -1
  37. package/dist/web/assets/{QSelect-D4U0Of4k.js → QSelect-CZqAOLiG.js} +1 -1
  38. package/dist/web/assets/ReviewDetailPage-DvDoJ37c.js +1 -0
  39. package/dist/web/assets/{ReviewsPage-CmHdSBsU.js → ReviewsPage-CHUXLKKw.js} +1 -1
  40. package/dist/web/assets/StatusSwitcher-Br_8-_CT.js +2 -0
  41. package/dist/web/assets/{StatusSwitcher-Ca5k9Tjx.css → StatusSwitcher-DLU1h0rK.css} +1 -1
  42. package/dist/web/assets/{abnfDiagram-O67JEVCF-C4PFTrGZ.js → abnfDiagram-O67JEVCF-dyvffE6R.js} +1 -1
  43. package/dist/web/assets/agent-icon-Bc3evt8o.js +17 -0
  44. package/dist/web/assets/{agent-store-D1WRRYIE.js → agent-store-FOng_RxK.js} +1 -1
  45. package/dist/web/assets/{arc-CiNErV2i.js → arc-DgcGdgmL.js} +1 -1
  46. package/dist/web/assets/architecture-7GRP2DOG-JRGyT2Fi.js +1 -0
  47. package/dist/web/assets/architectureDiagram-NJMV4G6O-Dk-QthCI.js +36 -0
  48. package/dist/web/assets/{blockDiagram-BEXU5L5S-CCN9Ja5L.js → blockDiagram-BEXU5L5S-sF48dzNS.js} +1 -1
  49. package/dist/web/assets/{c4Diagram-YGBWAQC7-CPzIw79A.js → c4Diagram-YGBWAQC7-BWrux-0F.js} +1 -1
  50. package/dist/web/assets/channel-C9BIu6HY.js +1 -0
  51. package/dist/web/assets/{chunk-3FUC2YCW-BbeiQKv_.js → chunk-3FUC2YCW-DM9oDQ0E.js} +1 -1
  52. package/dist/web/assets/{chunk-5DYCD2WN-CuzXaWoe.js → chunk-5DYCD2WN-CpgXf7LJ.js} +1 -1
  53. package/dist/web/assets/{chunk-742MDFTN-DJUCmQfr.js → chunk-742MDFTN-BGzG-dZE.js} +1 -1
  54. package/dist/web/assets/{chunk-7INBJB4K-BBd4ghzW.js → chunk-7INBJB4K-DJGc-gqS.js} +1 -1
  55. package/dist/web/assets/{chunk-7M6MHVWA-B0L68u_O.js → chunk-7M6MHVWA-CYIOaZel.js} +17 -17
  56. package/dist/web/assets/{chunk-7PRAP22T-BvEmVPr6.js → chunk-7PRAP22T-CJZNYL51.js} +1 -1
  57. package/dist/web/assets/{chunk-GTNCS2PH-B_vHEKRO.js → chunk-GTNCS2PH-DOYGHmMk.js} +1 -1
  58. package/dist/web/assets/{chunk-GWA4HPMP-DWaW5-VL.js → chunk-GWA4HPMP-RIikrSw1.js} +1 -1
  59. package/dist/web/assets/{chunk-MBY4JIJT-Dfo1FM_M.js → chunk-MBY4JIJT-ZSssyEJi.js} +1 -1
  60. package/dist/web/assets/{chunk-NETBCI7D-DzjBtXVV.js → chunk-NETBCI7D-3tnHEq90.js} +1 -1
  61. package/dist/web/assets/{chunk-O7XYJQB3-DY5ZFCJH.js → chunk-O7XYJQB3-CDFTy-aQ.js} +6 -6
  62. package/dist/web/assets/{chunk-UA2S7LBM-DX_QjVLS.js → chunk-UA2S7LBM-B8rY8Yj-.js} +1 -1
  63. package/dist/web/assets/{chunk-WEXAMYUT-Cckc2fqg.js → chunk-WEXAMYUT-BdPY0e7q.js} +1 -1
  64. package/dist/web/assets/{chunk-XXDRQBXY-DjF0kpJr.js → chunk-XXDRQBXY-BvKO0v-W.js} +1 -1
  65. package/dist/web/assets/{chunk-Z7XXMR3K-Do7oG6KK.js → chunk-Z7XXMR3K-Br_E149D.js} +1 -1
  66. package/dist/web/assets/{chunk-ZIGJFQKS-DTcqsqHq.js → chunk-ZIGJFQKS-DDjPFexL.js} +1 -1
  67. package/dist/web/assets/{classDiagram-v2-NBCMYWYE-Z6ZhPv4N.js → classDiagram-v2-NBCMYWYE-D9kneDxy.js} +7 -7
  68. package/dist/web/assets/{core-tP7fpdit.js → core-C37KF-j0.js} +1 -1
  69. package/dist/web/assets/core-II1Hg8Ir.js +12 -0
  70. package/dist/web/assets/cose-bilkent-JH36ORCC-CMiVhuwL.js +1 -0
  71. package/dist/web/assets/{cynefin-OW5HDTMX-BYrEwheK.js → cynefin-OW5HDTMX-Dot1PBqs.js} +1 -1
  72. package/dist/web/assets/{cynefinDiagram-VND7K2PF-BXbLnUDP.js → cynefinDiagram-VND7K2PF-6uYi0u-j.js} +1 -1
  73. package/dist/web/assets/{dagre-6A5THRUB-DJHHAqZU.js → dagre-6A5THRUB-BDM5YkfJ.js} +1 -1
  74. package/dist/web/assets/{diagram-22UHCM2B-DA9deaTX.js → diagram-22UHCM2B-DxE0D8WB.js} +16 -16
  75. package/dist/web/assets/{diagram-3UASUU5V-C6bo1gN6.js → diagram-3UASUU5V-CuD1fcFQ.js} +1 -1
  76. package/dist/web/assets/{diagram-ATOU4E4O-TTQR35E_.js → diagram-ATOU4E4O-DX6enGgq.js} +1 -1
  77. package/dist/web/assets/{diagram-CDSNMT55-ChFXwow0.js → diagram-CDSNMT55-eLqOiVoC.js} +1 -1
  78. package/dist/web/assets/{diagram-MLGK6HIB-D3F8BzhV.js → diagram-MLGK6HIB-DtIMj5y1.js} +1 -1
  79. package/dist/web/assets/{diagram-MPIPVDR6-NJ6F45a1.js → diagram-MPIPVDR6-lYcwsjHR.js} +1 -1
  80. package/dist/web/assets/dist-B_J_HbC0.js +1 -0
  81. package/dist/web/assets/{ebnfDiagram-ZINNZB2B-u2e1UzYe.js → ebnfDiagram-ZINNZB2B-CGmBWaKx.js} +1 -1
  82. package/dist/web/assets/{elk-276RUBZZ-tCLm9cCU.js → elk-276RUBZZ-B1ntUD9z.js} +1 -1
  83. package/dist/web/assets/engine-oniguruma-B4iNeJbd.js +1 -0
  84. package/dist/web/assets/engine-oniguruma-DdQrZxAE.js +1 -0
  85. package/dist/web/assets/{erDiagram-OPXOYQCR-ChNVjKDe.js → erDiagram-OPXOYQCR-Dqzoldin.js} +1 -1
  86. package/dist/web/assets/eventmodeling-NTZA5JFV-DblBuyVi.js +1 -0
  87. package/dist/web/assets/flowDiagram-KWPJA3E3-D5iLm5ip.js +1 -0
  88. package/dist/web/assets/ganttDiagram-FUAMR5RP-CF6kJAoY.js +292 -0
  89. package/dist/web/assets/{gitGraph-4MIJSDKK-DtM1RZrd.js → gitGraph-4MIJSDKK-CUzcGxMa.js} +1 -1
  90. package/dist/web/assets/{gitGraphDiagram-X574FWY7-C7KXkLLU.js → gitGraphDiagram-X574FWY7-D-XNa6f2.js} +1 -1
  91. package/dist/web/assets/highlight.worker-BHCdEX06.js +2 -0
  92. package/dist/web/assets/html-CEES10Qm.js +1 -0
  93. package/dist/web/assets/html-FFOZEiuQ.js +1 -0
  94. package/dist/web/assets/{i18n-BmgFD4Gx.js → i18n-DvBemXXT.js} +1 -1
  95. package/dist/web/assets/index-Bn7ByP7x.js +2 -0
  96. package/dist/web/assets/{info-A6RAGUB7-C5tVl8cQ.js → info-A6RAGUB7-CH4SNmU8.js} +1 -1
  97. package/dist/web/assets/{infoDiagram-VRGFBTTK-DO4D3mI7.js → infoDiagram-VRGFBTTK-35AKnD7M.js} +1 -1
  98. package/dist/web/assets/{init-app-C0kkg1Ie.js → init-app-C4PeiqQX.js} +1 -1
  99. package/dist/web/assets/{ishikawaDiagram-OU5B5YK6-DA2H3Eih.js → ishikawaDiagram-OU5B5YK6-D41YBWSb.js} +1 -1
  100. package/dist/web/assets/{journeyDiagram-ZHPQQLJL-CdPvR3Sr.js → journeyDiagram-ZHPQQLJL-CZF6Wwk8.js} +1 -1
  101. package/dist/web/assets/{kanban-definition-PNTS6WVX-BRIiazVf.js → kanban-definition-PNTS6WVX-CVGaUle0.js} +1 -1
  102. package/dist/web/assets/{line-CmXhYhmh.js → line-CeH4KBRO.js} +1 -1
  103. package/dist/web/assets/{linear-Cyx-ptw-.js → linear-BUnzfXgk.js} +1 -1
  104. package/dist/web/assets/{mermaid-parser.core-CZ05RARR.js → mermaid-parser.core-YrU9LTTJ.js} +3 -3
  105. package/dist/web/assets/mermaid.core-BkLEn5Vh.js +44 -0
  106. package/dist/web/assets/{mindmap-definition-NLK3R4M7-a7nG9rPn.js → mindmap-definition-NLK3R4M7-D22lxuXn.js} +1 -1
  107. package/dist/web/assets/{packet-AYTQ26CC-yIJsAali.js → packet-AYTQ26CC-DMK0BdeB.js} +1 -1
  108. package/dist/web/assets/{pegDiagram-GJSIUBJH-CBdB4i41.js → pegDiagram-GJSIUBJH-B-e5e5cW.js} +1 -1
  109. package/dist/web/assets/{pie-WAS4IAKB-ybMEHYPX.js → pie-WAS4IAKB-BYP5ND4b.js} +1 -1
  110. package/dist/web/assets/{pieDiagram-5QR66LMP-BfiG4Lho.js → pieDiagram-5QR66LMP-DrEY0oA_.js} +1 -1
  111. package/dist/web/assets/pinia-DB8WAJFa.js +1 -0
  112. package/dist/web/assets/{quadrantDiagram-O4NWA36T-7A6x_hoF.js → quadrantDiagram-O4NWA36T-D12UM_7Y.js} +1 -1
  113. package/dist/web/assets/{radar-RG4KPBEZ-BbeFIczu.js → radar-RG4KPBEZ-50JpTuMu.js} +1 -1
  114. package/dist/web/assets/{railroad-74A4TZTK-CAyDA7Zz.js → railroad-74A4TZTK-C-7jDcYE.js} +1 -1
  115. package/dist/web/assets/railroad-abnf-HS5TGJTU-Bf3pkLrz.js +1 -0
  116. package/dist/web/assets/railroad-ebnf-LZEXJU2U-f10AkgiO.js +1 -0
  117. package/dist/web/assets/railroad-peg-WCYAUIDC-C4FjCCVf.js +1 -0
  118. package/dist/web/assets/{railroadDiagram-XR7U4H2S-BXiOJ_AA.js → railroadDiagram-XR7U4H2S-q0-o99ts.js} +1 -1
  119. package/dist/web/assets/{requirementDiagram-PLB6GJNP-bQNGcpuV.js → requirementDiagram-PLB6GJNP-DsO-Ejin.js} +1 -1
  120. package/dist/web/assets/rolldown-runtime-Dd_uD5pT.js +1 -0
  121. package/dist/web/assets/{safe-storage-CqQMHcn7.js → safe-storage-DRouskD4.js} +1 -1
  122. package/dist/web/assets/{sankeyDiagram-IPEJSGJF-CgfGV4TY.js → sankeyDiagram-IPEJSGJF-Ct9goD-6.js} +1 -1
  123. package/dist/web/assets/scss-CLKKmMvk.js +1 -0
  124. package/dist/web/assets/{sequenceDiagram-PO4LG4MO-BdCuKkDZ.js → sequenceDiagram-PO4LG4MO-BMO_A2FU.js} +1 -1
  125. package/dist/web/assets/src-oBChb5qS.js +1 -0
  126. package/dist/web/assets/{stateDiagram-v2-GCMORJYK-aj1Op7xi.js → stateDiagram-v2-GCMORJYK-BSzg0zQp.js} +7 -7
  127. package/dist/web/assets/{swimlanes-2SLR337P-DQ_YYhZu.js → swimlanes-2SLR337P-CcmfFMci.js} +1 -1
  128. package/dist/web/assets/swimlanesDiagram-TC7HE7FX-DLq4WqTk.js +8 -0
  129. package/dist/web/assets/theme-DQpF6lSZ.js +1 -0
  130. package/dist/web/assets/{timeline-definition-EJHVYXUP-Bk3Mvy3w.js → timeline-definition-EJHVYXUP-0dcHdY2G.js} +1 -1
  131. package/dist/web/assets/{treeView-Q6P3EWNA-BzM7oZcT.js → treeView-Q6P3EWNA-KynQIJ_Z.js} +1 -1
  132. package/dist/web/assets/{treemap-WGGIJYW6-DzgzhZei.js → treemap-WGGIJYW6-DPpYRzcH.js} +1 -1
  133. package/dist/web/assets/{use-app-theme-DEiGbVuj.js → use-app-theme-B1Vh5WKT.js} +1 -1
  134. package/dist/web/assets/{use-panel-width-ujFPWf-l.js → use-panel-width-Cs_K2M_v.js} +1 -1
  135. package/dist/web/assets/{usecaseDiagram-POWQR4AR-kriFnh7L.js → usecaseDiagram-POWQR4AR-DJBtB_eN.js} +1 -1
  136. package/dist/web/assets/{vennDiagram-UO4OBE2U-CDdCMQs7.js → vennDiagram-UO4OBE2U-BgllcrO_.js} +1 -1
  137. package/dist/web/assets/vue-15YBeysQ.js +1 -0
  138. package/dist/web/assets/{wardley-WFR3VGLG-BUWTYQyf.js → wardley-WFR3VGLG-D2Je-cZt.js} +1 -1
  139. package/dist/web/assets/{wardleyDiagram-VNRHLVJA-CW7lMcTj.js → wardleyDiagram-VNRHLVJA-B7ktegci.js} +1 -1
  140. package/dist/web/assets/wasm-S0UXjx1m.js +1 -0
  141. package/dist/web/assets/{xychartDiagram-PMCCYNJV-DoIu8J_3.js → xychartDiagram-PMCCYNJV-DL7WkDZI.js} +1 -1
  142. package/dist/web/index.html +1 -1
  143. package/package.json +2 -2
  144. package/dist/web/assets/AppPage-BNPBe_fi.js +0 -1
  145. package/dist/web/assets/CreateReviewFabButton-3V9ZYSHd.js +0 -1
  146. package/dist/web/assets/IndexPage-xqMEFqAN.js +0 -1
  147. package/dist/web/assets/MainLayout-CMbAu8_d.js +0 -7
  148. package/dist/web/assets/ReviewDetailPage-DSCeLI9D.js +0 -1
  149. package/dist/web/assets/StatusSwitcher-CenEW-Lu.js +0 -4
  150. package/dist/web/assets/agent-icon-C1oimBWa.js +0 -17
  151. package/dist/web/assets/architecture-7GRP2DOG-DORh0iBM.js +0 -1
  152. package/dist/web/assets/architectureDiagram-NJMV4G6O-DnAqHYSW.js +0 -36
  153. package/dist/web/assets/channel-WV0H_O-w.js +0 -1
  154. package/dist/web/assets/cose-bilkent-JH36ORCC-B5DV641c.js +0 -1
  155. package/dist/web/assets/dist-Besl4xjp.js +0 -1
  156. package/dist/web/assets/engine-oniguruma-DaaPGLR3.js +0 -1
  157. package/dist/web/assets/eventmodeling-NTZA5JFV-KBTHxbEB.js +0 -1
  158. package/dist/web/assets/flowDiagram-KWPJA3E3-DM-yXypD.js +0 -1
  159. package/dist/web/assets/ganttDiagram-FUAMR5RP-8mcAm2ao.js +0 -292
  160. package/dist/web/assets/index-CuqF4_ks.js +0 -2
  161. package/dist/web/assets/mermaid.core-ZosqiKX-.js +0 -44
  162. package/dist/web/assets/pinia-r4XAEwbB.js +0 -1
  163. package/dist/web/assets/railroad-abnf-HS5TGJTU-BzS6thC5.js +0 -1
  164. package/dist/web/assets/railroad-ebnf-LZEXJU2U-MS09sAPW.js +0 -1
  165. package/dist/web/assets/railroad-peg-WCYAUIDC-DEQwqbcE.js +0 -1
  166. package/dist/web/assets/src-AvcVSqt3.js +0 -1
  167. package/dist/web/assets/swimlanesDiagram-TC7HE7FX-9frEtrG_.js +0 -8
  168. package/dist/web/assets/theme-BSjPUW3f.js +0 -1
package/README.md CHANGED
@@ -114,6 +114,13 @@ node dist/cli/index.js serve --db-prefix myteam- # .githuman/myteam-reviews.db
114
114
  node dist/cli/index.js list --db-prefix "" # .githuman/reviews.db (оригинальный githuman)
115
115
  ```
116
116
 
117
+ ### HTTP API
118
+
119
+ Сервер отдаёт OpenAPI-спецификацию своего API по адресу `/api/openapi.json`
120
+ (OpenAPI 3.1, всегда). Swagger UI — на `/api/docs`, только с флагом
121
+ `--api-docs` (`pnpm run dev:server` включает его сам). Агентские маршруты
122
+ попадают в спецификацию, только когда сервер их отдаёт (loopback-хост).
123
+
117
124
  ## Разработка
118
125
 
119
126
  Два процесса нужны одновременно — backend (Fastify, автоперезапуск при
@@ -134,7 +141,7 @@ githuman — запустите `dev:server` из cwd того репозито
134
141
  ```bash
135
142
  cd /path/to/other/repo
136
143
  node --watch-path=/path/to/githuman/src \
137
- /path/to/githuman/src/cli/index.ts serve --no-open
144
+ /path/to/githuman/src/cli/index.ts serve --no-open --api-docs
138
145
  ```
139
146
 
140
147
  Прочие команды:
@@ -53,7 +53,8 @@ export function parseServeArgs(argv) {
53
53
  host: { type: 'string' },
54
54
  open: { type: 'boolean', default: true },
55
55
  'no-open': { type: 'boolean', default: false },
56
- 'db-prefix': { type: 'string' }
56
+ 'db-prefix': { type: 'string' },
57
+ 'api-docs': { type: 'boolean', default: false }
57
58
  },
58
59
  strict: false
59
60
  });
@@ -61,5 +62,6 @@ export function parseServeArgs(argv) {
61
62
  const host = resolveHost(typeof values.host === 'string' ? values.host : undefined);
62
63
  const open = values['no-open'] === true ? false : values.open !== false;
63
64
  const dbPrefix = resolveDbPrefix(typeof values['db-prefix'] === 'string' ? values['db-prefix'] : undefined);
64
- return { port, host, open, dbPrefix };
65
+ const apiDocs = values['api-docs'] === true;
66
+ return { port, host, open, dbPrefix, apiDocs };
65
67
  }
@@ -63,6 +63,7 @@ export async function startServer(options) {
63
63
  repositoryPath,
64
64
  db,
65
65
  watchFiles: true,
66
+ apiDocs: options.apiDocs === true,
66
67
  // Agents can run commands, so they are only offered on a loopback bind.
67
68
  ...(isLoopbackHost(options.host)
68
69
  ? {
@@ -4,6 +4,8 @@ import { readFileSync } from 'node:fs';
4
4
  import { join } from 'node:path';
5
5
  import Fastify from 'fastify';
6
6
  import fastifyStatic from '@fastify/static';
7
+ import fastifySwagger from '@fastify/swagger';
8
+ import fastifySwaggerUi from '@fastify/swagger-ui';
7
9
  // @fastify/sse's published types declare an ESM default export that doesn't match
8
10
  // its actual CJS runtime shape, so the default import resolves to the whole module
9
11
  // namespace under NodeNext — cast it back to the plugin function type.
@@ -17,6 +19,8 @@ import { commentRoutes } from "./routes/comments.js";
17
19
  import { exportRoutes } from "./routes/export.js";
18
20
  import { eventRoutes } from "./routes/events.js";
19
21
  import { agentRoutes } from "./routes/agent.js";
22
+ import { openApiRoutes } from "./routes/openapi.js";
23
+ import { openApiOptions } from "./config/openapi.js";
20
24
  import { createSessionRegistry } from "./services/agent/session-registry.js";
21
25
  import { createTestDatabase } from "./db/index.js";
22
26
  import { createEventBus } from "./event-bus.js";
@@ -29,6 +33,12 @@ export function buildApp(options = {}) {
29
33
  const db = options.db ?? createTestDatabase();
30
34
  const eventBus = options.eventBus ?? createEventBus();
31
35
  app.register(fastifySse);
36
+ // The spec collects routes as they are added, so it comes before them.
37
+ app.register(fastifySwagger, openApiOptions());
38
+ if (options.apiDocs) {
39
+ app.register(fastifySwaggerUi, { routePrefix: '/api/docs' });
40
+ }
41
+ app.register(openApiRoutes);
32
42
  app.register(healthRoutes);
33
43
  app.register(appInfoRoutes);
34
44
  app.register(diffRoutes, { repositoryPath });
@@ -0,0 +1,29 @@
1
+ import { getAppVersion } from "../app-version.js";
2
+ /**
3
+ * Options of `@fastify/swagger`. OpenAPI 3.1 because TypeBox writes null as
4
+ * `{ type: 'null' }`, and OpenAPI 3.0 does not allow it.
5
+ */
6
+ export function openApiOptions() {
7
+ return {
8
+ openapi: {
9
+ openapi: '3.1.0',
10
+ info: {
11
+ title: 'githuman',
12
+ version: getAppVersion(),
13
+ description: 'HTTP API of the local githuman server. The API has no authentication: the tool is local.'
14
+ },
15
+ tags: [
16
+ { name: 'diff', description: 'Changes of the working tree and refs.' },
17
+ { name: 'git', description: 'Repository facts, files and index.' },
18
+ { name: 'reviews', description: 'Reviews and their export.' },
19
+ { name: 'comments', description: 'Comments of reviews.' },
20
+ { name: 'events', description: 'Server-sent change events.' },
21
+ { name: 'app', description: 'Server version and health.' },
22
+ {
23
+ name: 'agent',
24
+ description: 'Agent chats. The server has these routes only on a loopback host, and each request needs a local Host and Origin.'
25
+ }
26
+ ]
27
+ }
28
+ };
29
+ }
@@ -1,12 +1,28 @@
1
1
  import { UniqueNameError } from "../errors/reviews.js";
2
- function rowToReview(row) {
2
+ // Every column but snapshot_data: the list doesn't need the diff, and it is
3
+ // requested on each review/comment event (#56). Update this list when
4
+ // ReviewSummaryRow changes: a missing column reads as undefined, silently.
5
+ const SUMMARY_COLUMNS = [
6
+ 'id',
7
+ 'repository_path',
8
+ 'base_ref',
9
+ 'source_type',
10
+ 'source_ref',
11
+ 'status',
12
+ 'name',
13
+ 'branch',
14
+ 'created_at',
15
+ 'updated_at'
16
+ ]
17
+ .map(column => `reviews.${column}`)
18
+ .join(', ');
19
+ function rowToSummary(row) {
3
20
  return {
4
21
  id: row.id,
5
22
  repositoryPath: row.repository_path,
6
23
  baseRef: row.base_ref,
7
24
  sourceType: row.source_type,
8
25
  sourceRef: row.source_ref,
9
- snapshotData: row.snapshot_data,
10
26
  status: row.status,
11
27
  name: row.name,
12
28
  branch: row.branch,
@@ -14,6 +30,9 @@ function rowToReview(row) {
14
30
  updatedAt: row.updated_at
15
31
  };
16
32
  }
33
+ function rowToReview(row) {
34
+ return { ...rowToSummary(row), snapshotData: row.snapshot_data };
35
+ }
17
36
  export function createReview(db, review) {
18
37
  try {
19
38
  db.prepare(`INSERT INTO reviews (id, repository_path, base_ref, source_type, source_ref, snapshot_data, status, name, branch, created_at, updated_at)
@@ -32,6 +51,13 @@ export function findReviewById(db, id) {
32
51
  const row = db.prepare('SELECT * FROM reviews WHERE id = ?').get(id);
33
52
  return row ? rowToReview(row) : null;
34
53
  }
54
+ /** Reads a review without its snapshot, for callers that only need its fields (#57). */
55
+ export function findReviewSummaryById(db, id) {
56
+ const row = db
57
+ .prepare(`SELECT ${SUMMARY_COLUMNS} FROM reviews WHERE id = ?`)
58
+ .get(id);
59
+ return row ? rowToSummary(row) : null;
60
+ }
35
61
  export function listReviews(db, filters = {}) {
36
62
  const { branch, search, createdFrom, createdTo, filePaths } = filters;
37
63
  const conditions = [];
@@ -56,12 +82,12 @@ export function listReviews(db, filters = {}) {
56
82
  }
57
83
  const whereClause = conditions.length > 0 ? `WHERE ${conditions.join(' AND ')}` : '';
58
84
  const rows = db
59
- .prepare(`SELECT DISTINCT reviews.* FROM reviews
85
+ .prepare(`SELECT DISTINCT ${SUMMARY_COLUMNS} FROM reviews
60
86
  ${joins.join(' ')}
61
87
  ${whereClause}
62
88
  ORDER BY reviews.created_at DESC`)
63
89
  .all(...params);
64
- const reviews = rows.map(rowToReview);
90
+ const reviews = rows.map(rowToSummary);
65
91
  // Done in JS, not SQL `LOWER()`/`NOCASE` (ASCII-only in SQLite without the
66
92
  // ICU extension) — `String.prototype.toLowerCase()` handles Cyrillic and
67
93
  // other non-ASCII scripts correctly (see ADR 0017).
@@ -82,5 +108,5 @@ export function updateReviewStatus(db, id, status) {
82
108
  if (result.changes === 0) {
83
109
  return null;
84
110
  }
85
- return findReviewById(db, id);
111
+ return findReviewSummaryById(db, id);
86
112
  }
@@ -1,5 +1,7 @@
1
1
  import { NotFoundError } from "../errors/http.js";
2
- import { SessionParams, PermissionParams, CreateSessionBody, AutoApproveBody, FileSearchQuery, PromptBody, PermissionBody, ConfigBody } from "../../shared/agents/schemas.js";
2
+ import { Type } from '@sinclair/typebox';
3
+ import { SessionParams, PermissionParams, CreateSessionBody, AutoApproveBody, FileSearchQuery, PromptBody, PermissionBody, ConfigBody, AgentPresetInfoSchema, AgentSessionInfoSchema, AgentFileSearchResponseSchema, AgentEventsHeaders, AgentStreamSchema } from "../../shared/agents/schemas.js";
4
+ import { ApiErrorSchema, ERROR_RESPONSES, NoContentSchema } from "../../shared/http/schemas.js";
3
5
  import { buildPromptBlocks } from "../services/agent/context-builder.js";
4
6
  import { MAX_ATTACHMENT_BYTES, DEFAULT_FILE_SEARCH_LIMIT } from "../../shared/agents/constants.js";
5
7
  import { createFileIndex } from "../services/agent/file-index.js";
@@ -8,6 +10,16 @@ import { requireLocalOrigin } from "../hooks/agents/local-origin.js";
8
10
  import { findReviewById } from "../repositories/review.repo.js";
9
11
  /** Room for a handful of max-size base64 attachments; the schema caps each one. */
10
12
  const PROMPT_BODY_LIMIT = 5 * Math.ceil((MAX_ATTACHMENT_BYTES * 4) / 3) + 1024 * 1024;
13
+ // OpenAPI group of these routes.
14
+ const TAGS = ['agent'];
15
+ // The spec shows the local-origin guard on each operation, not only on the tag.
16
+ const AGENT_ERROR_RESPONSES = {
17
+ ...ERROR_RESPONSES,
18
+ 403: {
19
+ ...ApiErrorSchema,
20
+ description: 'Host or Origin of the request is not local.'
21
+ }
22
+ };
11
23
  export async function agentRoutes(app, opts) {
12
24
  const { registry, db, repositoryPath, eventBus } = opts;
13
25
  const typedApp = app.withTypeProvider();
@@ -17,9 +29,44 @@ export async function agentRoutes(app, opts) {
17
29
  registry.closeAll();
18
30
  fileIndex.close();
19
31
  });
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) => {
32
+ typedApp.get('/api/agent/presets', {
33
+ schema: {
34
+ tags: TAGS,
35
+ summary: 'List agent presets',
36
+ response: {
37
+ 200: Type.Array(AgentPresetInfoSchema, {
38
+ description: 'Presets from the config.'
39
+ }),
40
+ ...AGENT_ERROR_RESPONSES
41
+ }
42
+ }
43
+ }, async () => registry.listPresets());
44
+ typedApp.get('/api/agent/sessions', {
45
+ schema: {
46
+ tags: TAGS,
47
+ summary: 'List chat sessions',
48
+ response: {
49
+ 200: Type.Array(AgentSessionInfoSchema, {
50
+ description: 'Open and closed sessions.'
51
+ }),
52
+ ...AGENT_ERROR_RESPONSES
53
+ }
54
+ }
55
+ }, async () => registry.list());
56
+ typedApp.post('/api/agent/sessions', {
57
+ schema: {
58
+ tags: TAGS,
59
+ summary: 'Start a chat session',
60
+ body: CreateSessionBody,
61
+ response: {
62
+ 201: {
63
+ ...AgentSessionInfoSchema,
64
+ description: 'The session. The agent starts in the background.'
65
+ },
66
+ ...AGENT_ERROR_RESPONSES
67
+ }
68
+ }
69
+ }, async (request, reply) => {
23
70
  const { presetId, reviewId, name } = request.body;
24
71
  if (reviewId !== undefined && !findReviewById(db, reviewId)) {
25
72
  throw new NotFoundError(`Review ${reviewId} not found`);
@@ -29,21 +76,46 @@ export async function agentRoutes(app, opts) {
29
76
  reply.code(201);
30
77
  return session.info();
31
78
  });
32
- typedApp.get('/api/agent/sessions/:id', { schema: { params: SessionParams } }, async (request) => {
79
+ typedApp.get('/api/agent/sessions/:id', {
80
+ schema: {
81
+ tags: TAGS,
82
+ summary: 'Get a chat session',
83
+ params: SessionParams,
84
+ response: { 200: AgentSessionInfoSchema, ...AGENT_ERROR_RESPONSES }
85
+ }
86
+ }, async (request) => {
33
87
  const session = registry.get(request.params.id);
34
88
  if (!session) {
35
89
  throw new NotFoundError('Session not found');
36
90
  }
37
91
  return session.info();
38
92
  });
39
- typedApp.delete('/api/agent/sessions/:id', { schema: { params: SessionParams } }, async (request, reply) => {
93
+ typedApp.delete('/api/agent/sessions/:id', {
94
+ schema: {
95
+ tags: TAGS,
96
+ summary: 'Close a chat session and stop its agent',
97
+ params: SessionParams,
98
+ response: { 204: NoContentSchema, ...AGENT_ERROR_RESPONSES }
99
+ }
100
+ }, async (request, reply) => {
40
101
  if (!registry.close(request.params.id)) {
41
102
  throw new NotFoundError('Session not found');
42
103
  }
43
- return reply.code(204).send();
104
+ return reply.code(204).send(null);
44
105
  });
45
106
  typedApp.post('/api/agent/sessions/:id/prompt', {
46
- schema: { params: SessionParams, body: PromptBody },
107
+ schema: {
108
+ tags: TAGS,
109
+ summary: 'Send a message to the agent',
110
+ description: 'The answer comes over GET /api/agent/events. 202 means the session took the message.',
111
+ params: SessionParams,
112
+ body: PromptBody,
113
+ response: {
114
+ // No content type: the answer has no body, as before the schema.
115
+ 202: { description: 'The session took the message. No body.' },
116
+ ...AGENT_ERROR_RESPONSES
117
+ }
118
+ },
47
119
  bodyLimit: PROMPT_BODY_LIMIT
48
120
  }, async (request, reply) => {
49
121
  const session = registry.get(request.params.id);
@@ -61,44 +133,99 @@ export async function agentRoutes(app, opts) {
61
133
  session.prompt(blocks, request.body);
62
134
  return reply.code(202).send();
63
135
  });
64
- typedApp.post('/api/agent/sessions/:id/cancel', { schema: { params: SessionParams } }, async (request, reply) => {
136
+ typedApp.post('/api/agent/sessions/:id/cancel', {
137
+ schema: {
138
+ tags: TAGS,
139
+ summary: 'Cancel the current turn',
140
+ params: SessionParams,
141
+ response: { 204: NoContentSchema, ...AGENT_ERROR_RESPONSES }
142
+ }
143
+ }, async (request, reply) => {
65
144
  const session = registry.get(request.params.id);
66
145
  if (!session) {
67
146
  throw new NotFoundError('Session not found');
68
147
  }
69
148
  await session.cancel();
70
- return reply.code(204).send();
149
+ return reply.code(204).send(null);
71
150
  });
72
- typedApp.post('/api/agent/sessions/:id/permissions/:requestId', { schema: { params: PermissionParams, body: PermissionBody } }, async (request, reply) => {
151
+ typedApp.post('/api/agent/sessions/:id/permissions/:requestId', {
152
+ schema: {
153
+ tags: TAGS,
154
+ summary: 'Answer a permission request',
155
+ params: PermissionParams,
156
+ body: PermissionBody,
157
+ response: { 204: NoContentSchema, ...AGENT_ERROR_RESPONSES }
158
+ }
159
+ }, async (request, reply) => {
73
160
  const session = registry.get(request.params.id);
74
161
  if (!session?.answerPermission(request.params.requestId, request.body.optionId)) {
75
162
  throw new NotFoundError('No such pending permission request');
76
163
  }
77
- return reply.code(204).send();
164
+ return reply.code(204).send(null);
78
165
  });
79
- typedApp.post('/api/agent/sessions/:id/config', { schema: { params: SessionParams, body: ConfigBody } }, async (request, reply) => {
166
+ typedApp.post('/api/agent/sessions/:id/config', {
167
+ schema: {
168
+ tags: TAGS,
169
+ summary: 'Change an agent setting',
170
+ params: SessionParams,
171
+ body: ConfigBody,
172
+ response: { 204: NoContentSchema, ...AGENT_ERROR_RESPONSES }
173
+ }
174
+ }, async (request, reply) => {
80
175
  const session = registry.get(request.params.id);
81
176
  if (!session) {
82
177
  throw new NotFoundError('Session not found');
83
178
  }
84
179
  await session.setConfig(request.body.configId, request.body.value);
85
- return reply.code(204).send();
180
+ return reply.code(204).send(null);
86
181
  });
87
- typedApp.post('/api/agent/sessions/:id/auto-approve', { schema: { params: SessionParams, body: AutoApproveBody } }, async (request, reply) => {
182
+ typedApp.post('/api/agent/sessions/:id/auto-approve', {
183
+ schema: {
184
+ tags: TAGS,
185
+ summary: 'Switch auto-approve of a session',
186
+ params: SessionParams,
187
+ body: AutoApproveBody,
188
+ response: { 204: NoContentSchema, ...AGENT_ERROR_RESPONSES }
189
+ }
190
+ }, async (request, reply) => {
88
191
  const session = registry.get(request.params.id);
89
192
  if (!session) {
90
193
  throw new NotFoundError('Session not found');
91
194
  }
92
195
  session.setAutoApprove(request.body.enabled);
93
- return reply.code(204).send();
196
+ return reply.code(204).send(null);
94
197
  });
95
- typedApp.get('/api/agent/files', { schema: { querystring: FileSearchQuery } }, async (request) => ({
198
+ typedApp.get('/api/agent/files', {
199
+ schema: {
200
+ tags: TAGS,
201
+ summary: 'Find repository files for the context picker',
202
+ querystring: FileSearchQuery,
203
+ response: {
204
+ 200: AgentFileSearchResponseSchema,
205
+ ...AGENT_ERROR_RESPONSES
206
+ }
207
+ }
208
+ }, async (request) => ({
96
209
  files: await fileIndex.search(request.query.q ?? '', request.query.limit ?? DEFAULT_FILE_SEARCH_LIMIT)
97
210
  }));
98
211
  // One stream for every chat (ADR 0024): browsers allow ~6 connections per
99
212
  // origin over HTTP/1.1, so a stream per session would starve the page's own
100
213
  // requests. Event ids are shared across sessions, so `Last-Event-ID` works.
101
- typedApp.get('/api/agent/events', { sse: true }, async (request, reply) => {
214
+ typedApp.get('/api/agent/events', {
215
+ sse: true,
216
+ schema: {
217
+ tags: TAGS,
218
+ summary: 'Stream of all chat sessions',
219
+ headers: AgentEventsHeaders,
220
+ response: {
221
+ 200: {
222
+ description: 'An SSE stream that stays open.',
223
+ content: { 'text/event-stream': { schema: AgentStreamSchema } }
224
+ },
225
+ ...AGENT_ERROR_RESPONSES
226
+ }
227
+ }
228
+ }, async (request, reply) => {
102
229
  const lastEventId = Number(request.headers['last-event-id']);
103
230
  reply.sse.keepAlive();
104
231
  // Flushes the headers: with no session yet nothing else would be sent, and
@@ -1,6 +1,13 @@
1
1
  import { getAppVersion } from "../app-version.js";
2
+ import { AppInfoSchema } from "../../shared/app/schemas.js";
2
3
  export async function appInfoRoutes(app) {
3
- app.get('/api/app-info', async () => {
4
+ app.withTypeProvider().get('/api/app-info', {
5
+ schema: {
6
+ tags: ['app'],
7
+ summary: 'Server version',
8
+ response: { 200: AppInfoSchema }
9
+ }
10
+ }, async () => {
4
11
  return { version: getAppVersion() };
5
12
  });
6
13
  }
@@ -2,31 +2,26 @@ import { NotFoundError } from "../errors/http.js";
2
2
  import { Type } from '@sinclair/typebox';
3
3
  import { getReview } from "../services/review.service.js";
4
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.
8
- const CreateCommentBody = Type.Object({
9
- filePath: Type.String({ minLength: 1 }),
10
- lineNumber: Type.Optional(Type.Union([Type.Null(), Type.Integer()])),
11
- lineNumberEnd: Type.Optional(Type.Union([Type.Null(), Type.Integer()])),
12
- lineType: Type.Optional(Type.Union([
13
- Type.Null(),
14
- Type.Literal('added'),
15
- Type.Literal('removed'),
16
- Type.Literal('context')
17
- ])),
18
- content: Type.String(),
19
- suggestion: Type.Optional(Type.Union([Type.Null(), Type.String()]))
20
- });
21
- const UpdateCommentBody = Type.Object({
22
- content: Type.String()
23
- });
24
- const ReviewIdParams = Type.Object({ id: Type.String() });
25
- const CommentIdParams = Type.Object({ id: Type.String() });
5
+ import { CommentIdParams, CommentSchema, CreateCommentBody, UpdateCommentBody } from "../../shared/comments/schemas.js";
6
+ import { ReviewIdParams } from "../../shared/reviews/schemas.js";
7
+ import { ERROR_RESPONSES, NoContentSchema } from "../../shared/http/schemas.js";
8
+ // OpenAPI group of these routes.
9
+ const TAGS = ['comments'];
26
10
  export async function commentRoutes(app, opts) {
27
11
  const { db, eventBus } = opts;
28
12
  const typedApp = app.withTypeProvider();
29
- typedApp.post('/api/reviews/:id/comments', { schema: { params: ReviewIdParams, body: CreateCommentBody } }, async (request, reply) => {
13
+ typedApp.post('/api/reviews/:id/comments', {
14
+ schema: {
15
+ tags: TAGS,
16
+ summary: 'Add a comment to a review',
17
+ params: ReviewIdParams,
18
+ body: CreateCommentBody,
19
+ response: {
20
+ 201: { ...CommentSchema, description: 'The new comment.' },
21
+ ...ERROR_RESPONSES
22
+ }
23
+ }
24
+ }, async (request, reply) => {
30
25
  if (!getReview(db, request.params.id)) {
31
26
  throw new NotFoundError(`Review ${request.params.id} not found`);
32
27
  }
@@ -34,34 +29,85 @@ export async function commentRoutes(app, opts) {
34
29
  reply.code(201);
35
30
  return comment;
36
31
  });
37
- typedApp.get('/api/reviews/:id/comments', { schema: { params: ReviewIdParams } }, async (request) => {
32
+ typedApp.get('/api/reviews/:id/comments', {
33
+ schema: {
34
+ tags: TAGS,
35
+ summary: 'List comments of a review',
36
+ params: ReviewIdParams,
37
+ response: {
38
+ 200: Type.Array(CommentSchema, {
39
+ description: 'Comments of the review.'
40
+ }),
41
+ ...ERROR_RESPONSES
42
+ }
43
+ }
44
+ }, async (request) => {
38
45
  if (!getReview(db, request.params.id)) {
39
46
  throw new NotFoundError(`Review ${request.params.id} not found`);
40
47
  }
41
48
  return getComments(db, request.params.id);
42
49
  });
43
- typedApp.patch('/api/comments/:id', { schema: { params: CommentIdParams, body: UpdateCommentBody } }, async (request) => {
50
+ typedApp.patch('/api/comments/:id', {
51
+ schema: {
52
+ tags: TAGS,
53
+ summary: 'Change the comment text',
54
+ params: CommentIdParams,
55
+ body: UpdateCommentBody,
56
+ response: {
57
+ 200: { ...CommentSchema, description: 'The changed comment.' },
58
+ ...ERROR_RESPONSES
59
+ }
60
+ }
61
+ }, async (request) => {
44
62
  const updated = editComment(db, request.params.id, request.body.content, eventBus);
45
63
  if (!updated) {
46
64
  throw new NotFoundError(`Comment ${request.params.id} not found`);
47
65
  }
48
66
  return updated;
49
67
  });
50
- typedApp.patch('/api/comments/:id/resolve', { schema: { params: CommentIdParams } }, async (request) => {
68
+ typedApp.patch('/api/comments/:id/resolve', {
69
+ schema: {
70
+ tags: TAGS,
71
+ summary: 'Mark a comment as resolved',
72
+ params: CommentIdParams,
73
+ response: {
74
+ 200: { ...CommentSchema, description: 'The changed comment.' },
75
+ ...ERROR_RESPONSES
76
+ }
77
+ }
78
+ }, async (request) => {
51
79
  const updated = resolveComment(db, request.params.id, eventBus);
52
80
  if (!updated) {
53
81
  throw new NotFoundError(`Comment ${request.params.id} not found`);
54
82
  }
55
83
  return updated;
56
84
  });
57
- typedApp.patch('/api/comments/:id/unresolve', { schema: { params: CommentIdParams } }, async (request) => {
85
+ typedApp.patch('/api/comments/:id/unresolve', {
86
+ schema: {
87
+ tags: TAGS,
88
+ summary: 'Mark a comment as not resolved',
89
+ params: CommentIdParams,
90
+ response: {
91
+ 200: { ...CommentSchema, description: 'The changed comment.' },
92
+ ...ERROR_RESPONSES
93
+ }
94
+ }
95
+ }, async (request) => {
58
96
  const updated = unresolveComment(db, request.params.id, eventBus);
59
97
  if (!updated) {
60
98
  throw new NotFoundError(`Comment ${request.params.id} not found`);
61
99
  }
62
100
  return updated;
63
101
  });
64
- typedApp.delete('/api/comments/:id', { schema: { params: CommentIdParams } }, async (request, reply) => {
102
+ typedApp.delete('/api/comments/:id', {
103
+ schema: {
104
+ tags: TAGS,
105
+ summary: 'Delete a comment',
106
+ description: 'A missing comment also gives 204.',
107
+ params: CommentIdParams,
108
+ response: { 204: NoContentSchema, ...ERROR_RESPONSES }
109
+ }
110
+ }, async (request, reply) => {
65
111
  removeComment(db, request.params.id, eventBus);
66
112
  reply.code(204);
67
113
  });