@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
@@ -1,51 +1,140 @@
1
1
  import { Type } from '@sinclair/typebox';
2
2
  import { MAX_ATTACHMENT_BYTES, MAX_FILE_SEARCH_LIMIT } from "./constants.js";
3
- export const SessionParams = Type.Object({ id: Type.String() });
3
+ import { DiffSourceNameSchema } from "../diff/schemas.js";
4
+ import { Nullable } from "../utils/schemas.js";
5
+ export const SessionParams = Type.Object({
6
+ id: Type.String({ description: 'Chat session id.' })
7
+ });
4
8
  export const PermissionParams = Type.Object({
5
- id: Type.String(),
6
- requestId: Type.String()
9
+ id: Type.String({ description: 'Chat session id.' }),
10
+ requestId: Type.String({ description: 'Pending permission request id.' })
7
11
  });
8
12
  export const CreateSessionBody = Type.Object({
9
- presetId: Type.String({ minLength: 1 }),
10
- name: Type.Optional(Type.String({ maxLength: 60 })),
11
- reviewId: Type.Optional(Type.String())
12
- });
13
- export const AutoApproveBody = Type.Object({ enabled: Type.Boolean() });
13
+ presetId: Type.String({
14
+ minLength: 1,
15
+ description: 'Preset from GET /api/agent/presets.'
16
+ }),
17
+ name: Type.Optional(Type.String({
18
+ maxLength: 60,
19
+ description: 'Chat name. Omitted: the server makes a unique one.'
20
+ })),
21
+ reviewId: Type.Optional(Type.String({ description: 'Review that the chat belongs to.' }))
22
+ }, { description: 'A new chat session.' });
23
+ export const AutoApproveBody = Type.Object({
24
+ enabled: Type.Boolean({
25
+ description: 'The server answers permission requests itself.'
26
+ })
27
+ }, { description: 'Auto-approve switch of one session.' });
14
28
  export const FileSearchQuery = Type.Object({
15
- q: Type.Optional(Type.String({ maxLength: 200 })),
16
- limit: Type.Optional(Type.Integer({ minimum: 1, maximum: MAX_FILE_SEARCH_LIMIT }))
29
+ q: Type.Optional(Type.String({
30
+ maxLength: 200,
31
+ description: 'Fuzzy text to find in paths. Empty lists files.'
32
+ })),
33
+ limit: Type.Optional(Type.Integer({
34
+ minimum: 1,
35
+ maximum: MAX_FILE_SEARCH_LIMIT,
36
+ description: 'Maximum number of paths.'
37
+ }))
17
38
  });
18
39
  export const ContextItem = Type.Union([
19
40
  Type.Object({
20
- kind: Type.Literal('diff'),
21
- source: Type.Union([Type.Literal('staged'), Type.Literal('unstaged')]),
22
- path: Type.Optional(Type.String({ minLength: 1 }))
23
- }),
41
+ kind: Type.Literal('diff', { description: 'Live working-tree diff.' }),
42
+ source: DiffSourceNameSchema,
43
+ path: Type.Optional(Type.String({
44
+ minLength: 1,
45
+ description: 'One file of the diff. Omitted: the whole diff.'
46
+ }))
47
+ }, { description: 'The live diff of one side.' }),
24
48
  Type.Object({
25
- kind: Type.Literal('file'),
26
- path: Type.String({ minLength: 1 })
27
- }),
28
- Type.Object({ kind: Type.Literal('review'), reviewId: Type.String() }),
49
+ kind: Type.Literal('file', { description: 'A repository file.' }),
50
+ path: Type.String({
51
+ minLength: 1,
52
+ description: 'Repository-relative path. The agent reads the file.'
53
+ })
54
+ }, { description: 'A link to a repository file.' }),
29
55
  Type.Object({
30
- kind: Type.Literal('attachment'),
31
- name: Type.String({ minLength: 1, maxLength: 255 }),
32
- mimeType: Type.String({ maxLength: 255 }),
56
+ kind: Type.Literal('review', { description: 'A review.' }),
57
+ reviewId: Type.String({ description: 'Review id.' })
58
+ }, { description: 'A review as markdown with its comments.' }),
59
+ Type.Object({
60
+ kind: Type.Literal('attachment', {
61
+ description: 'A file from the user.'
62
+ }),
63
+ name: Type.String({
64
+ minLength: 1,
65
+ maxLength: 255,
66
+ description: 'File name.'
67
+ }),
68
+ mimeType: Type.String({ maxLength: 255, description: 'MIME type.' }),
33
69
  data: Type.String({
34
- maxLength: Math.ceil(MAX_ATTACHMENT_BYTES / 3) * 4
70
+ maxLength: Math.ceil(MAX_ATTACHMENT_BYTES / 3) * 4,
71
+ description: 'File content in base64.'
35
72
  })
36
- })
37
- ]);
73
+ }, { description: 'An uploaded file or a pasted image.' })
74
+ ], { description: 'Context of a prompt (docs/agent-context-uris.md).' });
38
75
  export const PromptBody = Type.Object({
39
- text: Type.String({ minLength: 1 }),
40
- context: Type.Optional(Type.Array(ContextItem, { maxItems: 20 }))
41
- });
76
+ text: Type.String({ minLength: 1, description: 'Message to the agent.' }),
77
+ context: Type.Optional(Type.Array(ContextItem, {
78
+ maxItems: 20,
79
+ description: 'Context items that go with the message.'
80
+ }))
81
+ }, { description: 'A user message.' });
42
82
  export const PermissionBody = Type.Object({
43
- optionId: Type.Optional(Type.String())
44
- });
83
+ optionId: Type.Optional(Type.String({
84
+ description: 'Option from the permission request. Omitted: the request is cancelled.'
85
+ }))
86
+ }, { description: 'Answer to a permission request.' });
45
87
  export const ConfigBody = Type.Object({
46
- configId: Type.String({ minLength: 1 }),
88
+ configId: Type.String({
89
+ minLength: 1,
90
+ description: 'Setting id from the config event.'
91
+ }),
47
92
  // No schema type on purpose: a Union/anyOf makes Fastify's Ajv coerce `true`
48
93
  // into the string "true", and a `type` array trips Ajv's strictTypes. The
49
94
  // session checks the value against the option the agent actually offered.
50
- value: Type.Unsafe({})
95
+ value: Type.Unsafe({
96
+ description: 'A select value (string) or a boolean.'
97
+ })
98
+ }, { description: 'A new value of an agent setting.' });
99
+ export const AgentPresetInfoSchema = Type.Object({
100
+ id: Type.String({ description: 'Preset id.' }),
101
+ title: Type.String({ description: 'Name in the UI.' }),
102
+ available: Type.Boolean({
103
+ description: 'The launcher binary is in PATH. Says nothing about login or auth.'
104
+ }),
105
+ autoApprovesEdits: Type.Boolean({
106
+ description: 'The agent applies edits without permission requests.'
107
+ })
108
+ }, { description: 'An ACP agent that can start a chat.' });
109
+ export const AgentSessionStatusSchema = Type.Union([
110
+ Type.Literal('starting'),
111
+ Type.Literal('ready'),
112
+ Type.Literal('busy'),
113
+ Type.Literal('closed')
114
+ ], { description: 'Session state.' });
115
+ export const AgentSessionInfoSchema = Type.Object({
116
+ id: Type.String({ description: 'Chat session id.' }),
117
+ presetId: Type.String({ description: 'Preset of the agent.' }),
118
+ name: Type.String({ description: 'Chat name in the UI.' }),
119
+ status: AgentSessionStatusSchema,
120
+ reviewId: Nullable(Type.String({ description: 'Review of the chat. Else null.' })),
121
+ autoApprove: Type.Boolean({
122
+ description: 'The server answers permission requests itself.'
123
+ }),
124
+ error: Nullable(Type.String({
125
+ description: 'Why the session could not start. Else null.'
126
+ }))
127
+ }, { description: 'A chat session.' });
128
+ export const AgentFileSearchResponseSchema = Type.Object({
129
+ files: Type.Array(Type.String(), {
130
+ description: 'Repository-relative paths, best match first.'
131
+ })
132
+ }, { description: 'Files for the context picker.' });
133
+ export const AgentEventsHeaders = Type.Object({
134
+ 'last-event-id': Type.Optional(Type.String({
135
+ description: 'Last event id that the client got. Events after it come again.'
136
+ }))
137
+ });
138
+ export const AgentStreamSchema = Type.String({
139
+ description: 'Server-sent events: connected, then agent (AgentStreamEnvelope, SSE id = event id), state (AgentSessionState), gap ({ sessionId }) and sessions (reread the list).'
51
140
  });
@@ -0,0 +1,7 @@
1
+ import { Type } from '@sinclair/typebox';
2
+ export const AppInfoSchema = Type.Object({
3
+ version: Type.String({ description: 'githuman version from package.json.' })
4
+ }, { description: 'Facts about the running server.' });
5
+ export const HealthSchema = Type.Object({
6
+ status: Type.Literal('ok', { description: 'Always "ok".' })
7
+ }, { description: 'The server is up.' });
@@ -0,0 +1,48 @@
1
+ import { Type } from '@sinclair/typebox';
2
+ import { Nullable } from "../utils/schemas.js";
3
+ import { DiffLineTypeSchema } from "../diff/schemas.js";
4
+ export const CommentSchema = Type.Object({
5
+ id: Type.String({ description: 'Comment id.' }),
6
+ reviewId: Type.String({ description: 'Review that holds the comment.' }),
7
+ filePath: Type.String({ description: 'Repository-relative file path.' }),
8
+ lineNumber: Nullable(Type.Integer({
9
+ description: 'First line of the comment. Null for a file comment.'
10
+ })),
11
+ lineNumberEnd: Nullable(Type.Integer({
12
+ description: 'Last line of a selected range. Equals lineNumber for one line.'
13
+ })),
14
+ lineType: Nullable(DiffLineTypeSchema),
15
+ content: Type.String({ description: 'Comment text in markdown.' }),
16
+ createdAt: Type.String({ description: 'Creation time, ISO 8601.' }),
17
+ updatedAt: Type.String({ description: 'Last change time, ISO 8601.' }),
18
+ resolved: Type.Optional(Type.Boolean({ description: 'The comment is resolved.' })),
19
+ suggestion: Type.Optional(Nullable(Type.String({ description: 'Code that replaces the selected lines.' })))
20
+ }, { description: 'A comment on a file or on lines of a review.' });
21
+ // Null goes first in each union: Fastify's Ajv coerces types, and with
22
+ // Integer (or String) first it turns a JSON null into 0 (or "") before the
23
+ // Null branch is ever tried — a file-level comment would be stored as line 0.
24
+ export const CreateCommentBody = Type.Object({
25
+ filePath: Type.String({
26
+ minLength: 1,
27
+ description: 'Repository-relative file path.'
28
+ }),
29
+ lineNumber: Type.Optional(Type.Union([Type.Null(), Type.Integer()], {
30
+ description: 'First line. Null or omitted for a file comment.'
31
+ })),
32
+ lineNumberEnd: Type.Optional(Type.Union([Type.Null(), Type.Integer()], {
33
+ description: 'Last line of a range. Null or omitted for one line.'
34
+ })),
35
+ lineType: Type.Optional(Type.Union([Type.Null(), ...DiffLineTypeSchema.anyOf], {
36
+ description: 'Line kind of lineNumber. Null for a file comment.'
37
+ })),
38
+ content: Type.String({ description: 'Comment text in markdown.' }),
39
+ suggestion: Type.Optional(Type.Union([Type.Null(), Type.String()], {
40
+ description: 'Code that replaces the selected lines.'
41
+ }))
42
+ }, { description: 'A new comment.' });
43
+ export const UpdateCommentBody = Type.Object({
44
+ content: Type.String({ description: 'New comment text in markdown.' })
45
+ }, { description: 'A comment change.' });
46
+ export const CommentIdParams = Type.Object({
47
+ id: Type.String({ description: 'Comment id.' })
48
+ });
@@ -0,0 +1,77 @@
1
+ // The farthest line, above a hunk, that its preamble may reach (ADR 0036). The reach is a
2
+ // limit, not the size: the preamble starts at a safe line within it. A reach of 60 gave the
3
+ // fewest wrong lines in a test on the history of this repository.
4
+ export const PREAMBLE_REACH = 60;
5
+ // A budget for one preamble, in characters. A minified file has lines of many kilobytes.
6
+ const PREAMBLE_MAX_CHARS = 4000;
7
+ // A line that closes a tag. At the start of a preamble it has no opening tag, and a markup
8
+ // grammar then loses its state.
9
+ const CLOSER = /^\s*(<\/|\/>|>)/;
10
+ // A top-level block of a Vue file: the tag stands alone on its line, at column 0.
11
+ const VUE_BLOCK_OPEN = /^<(template|script|style)\b[^>]*>\s*$/;
12
+ /**
13
+ * For each line index, the opening tag of the Vue block that is open before that line.
14
+ * The list has one more entry than there are lines, so the end of the file has an entry too.
15
+ */
16
+ function vueOpenTags(lines) {
17
+ const tags = [undefined];
18
+ let open;
19
+ for (const text of lines) {
20
+ const match = VUE_BLOCK_OPEN.exec(text);
21
+ if (match) {
22
+ open = { tag: match[1], text };
23
+ }
24
+ else if (open && text.startsWith(`</${open.tag}>`)) {
25
+ open = undefined;
26
+ }
27
+ tags.push(open?.text);
28
+ }
29
+ return tags;
30
+ }
31
+ /**
32
+ * Makes a reader of preambles for one side of a file. Use one reader for all hunks of the
33
+ * file: it scans the Vue blocks once, and only when a hunk needs them.
34
+ *
35
+ * The preamble starts at the farthest safe line within {@link PREAMBLE_REACH} lines above
36
+ * the hunk. A line is safe when it is indented no deeper than every line after it, up to the
37
+ * hunk, and does not close a tag. Then the preamble holds no line that closes a block
38
+ * opened above it, and the grammar keeps a sound state. In a Vue file the opening tag of the
39
+ * enclosing block comes first, so the parser knows the language. `sideLines` are all lines
40
+ * of the file on the side of the hunk.
41
+ */
42
+ export function createPreambleReader(path, sideLines) {
43
+ const isVue = path.toLowerCase().endsWith('.vue');
44
+ let openTags;
45
+ return firstLine => {
46
+ const end = Math.min(firstLine - 1, sideLines.length);
47
+ if (end <= 0)
48
+ return [];
49
+ let start = end;
50
+ let chars = 0;
51
+ let shallowest = Infinity;
52
+ for (let i = end - 1; i >= Math.max(0, end - PREAMBLE_REACH); i--) {
53
+ const line = sideLines[i];
54
+ chars += line.length;
55
+ if (chars > PREAMBLE_MAX_CHARS)
56
+ break;
57
+ if (line.trim() === '')
58
+ continue;
59
+ const indent = line.length - line.trimStart().length;
60
+ if (indent <= shallowest && !CLOSER.test(line))
61
+ start = i;
62
+ shallowest = Math.min(shallowest, indent);
63
+ }
64
+ const lines = sideLines.slice(start, end);
65
+ if (isVue) {
66
+ openTags ??= vueOpenTags(sideLines);
67
+ const tag = openTags[start];
68
+ if (tag !== undefined)
69
+ lines.unshift(tag);
70
+ }
71
+ return lines;
72
+ };
73
+ }
74
+ /** The preamble of one hunk. For many hunks of one file, use {@link createPreambleReader}. */
75
+ export function hunkPreamble(path, sideLines, firstLine) {
76
+ return createPreambleReader(path, sideLines)(firstLine);
77
+ }
@@ -0,0 +1,80 @@
1
+ import { Type } from '@sinclair/typebox';
2
+ import { Nullable } from "../utils/schemas.js";
3
+ export const DiffSourceNameSchema = Type.Union([Type.Literal('staged'), Type.Literal('unstaged')], { description: 'Side of the working tree: the index or the files on disk.' });
4
+ export const DiffLineTypeSchema = Type.Union([Type.Literal('added'), Type.Literal('removed'), Type.Literal('context')], { description: 'Line kind: added, removed or unchanged context.' });
5
+ export const DiffFileStatusSchema = Type.Union([
6
+ Type.Literal('added'),
7
+ Type.Literal('modified'),
8
+ Type.Literal('deleted'),
9
+ Type.Literal('renamed')
10
+ ], { description: 'What the change does to the file.' });
11
+ export const DiffLineSchema = Type.Object({
12
+ type: DiffLineTypeSchema,
13
+ content: Type.String({
14
+ description: 'Line text without the "+", "-" or " " prefix.'
15
+ }),
16
+ oldLineNumber: Nullable(Type.Integer({
17
+ description: 'Line number in the old file. Null for an added line.'
18
+ })),
19
+ newLineNumber: Nullable(Type.Integer({
20
+ description: 'Line number in the new file. Null for a removed line.'
21
+ }))
22
+ }, { description: 'One line of a hunk.' });
23
+ export const DiffHunkSchema = Type.Object({
24
+ oldStart: Type.Integer({ description: 'First line in the old file.' }),
25
+ oldLines: Type.Integer({ description: 'Line count in the old file.' }),
26
+ newStart: Type.Integer({ description: 'First line in the new file.' }),
27
+ newLines: Type.Integer({ description: 'Line count in the new file.' }),
28
+ lines: Type.Array(DiffLineSchema, {
29
+ description: 'Lines of the hunk in file order.'
30
+ }),
31
+ preamble: Type.Optional(Type.Array(Type.String(), {
32
+ description: 'Unchanged lines above the hunk, for syntax highlighting only. Bounded in count and size. In a Vue file the opening tag of the enclosing block comes first. Absent when there are none.'
33
+ }))
34
+ }, { description: 'One hunk of a file diff.' });
35
+ const diffFileFields = {
36
+ oldPath: Type.String({
37
+ description: 'Path before the change. Equals newPath unless renamed.'
38
+ }),
39
+ newPath: Type.String({
40
+ description: 'Path after the change. Equals oldPath for a deleted file.'
41
+ }),
42
+ status: DiffFileStatusSchema,
43
+ additions: Type.Integer({ description: 'Added line count.' }),
44
+ deletions: Type.Integer({ description: 'Removed line count.' }),
45
+ isBinary: Type.Boolean({ description: 'Git sees the file as binary.' })
46
+ };
47
+ export const DiffFileSchema = Type.Object({
48
+ ...diffFileFields,
49
+ hunks: Type.Array(DiffHunkSchema, {
50
+ description: 'Hunks of the file. Empty for a binary file.'
51
+ })
52
+ }, { description: 'Diff of one file with its hunks.' });
53
+ export const DiffFileSummarySchema = Type.Object({
54
+ ...diffFileFields,
55
+ signature: Type.String({
56
+ description: 'Changes when the file content or its diff changes. Equal signatures mean equal hunks.'
57
+ })
58
+ }, { description: 'Diff of one file without hunks.' });
59
+ export const SourceParams = Type.Object({ source: DiffSourceNameSchema });
60
+ export const FileQuery = Type.Object({
61
+ path: Type.String({
62
+ minLength: 1,
63
+ description: 'Repository-relative path after the change.'
64
+ }),
65
+ oldPath: Type.Optional(Type.String({
66
+ minLength: 1,
67
+ description: 'Repository-relative path before a rename.'
68
+ })),
69
+ status: DiffFileStatusSchema
70
+ });
71
+ export const BranchQuery = Type.Object({
72
+ base: Type.String({
73
+ minLength: 1,
74
+ description: 'Ref that the diff compares HEAD with.'
75
+ })
76
+ });
77
+ export const CommitsQuery = Type.Object({
78
+ from: Type.String({ minLength: 1, description: 'Older ref.' }),
79
+ to: Type.String({ minLength: 1, description: 'Newer ref.' })
80
+ });
@@ -1,4 +1 @@
1
- /**
2
- * Shared types between server and web client
3
- */
4
1
  export {};
@@ -0,0 +1,4 @@
1
+ import { Type } from '@sinclair/typebox';
2
+ export const EventStreamSchema = Type.String({
3
+ description: 'Server-sent events. The first event is connected with { instanceId } data. Then come review:created, review:updated, review:deleted, comment:created, comment:updated and comment:deleted with { reviewId } data, and files:changed with {} data.'
4
+ });
@@ -0,0 +1,59 @@
1
+ import { Type } from '@sinclair/typebox';
2
+ import { Nullable } from "../utils/schemas.js";
3
+ export const RepositoryInfoSchema = Type.Object({
4
+ name: Type.String({ description: 'Name of the repository directory.' }),
5
+ branch: Type.String({ description: 'Current branch.' }),
6
+ remote: Nullable(Type.String({
7
+ description: 'Fetch URL of the origin remote. Null if none.'
8
+ })),
9
+ path: Type.String({ description: 'Absolute path of the repository.' })
10
+ }, { description: 'The repository that the server shows.' });
11
+ export const FileTreeResponseSchema = Type.Object({
12
+ ref: Type.String({ description: 'Ref from the request.' }),
13
+ files: Type.Array(Type.String(), {
14
+ description: 'Repository-relative paths of all files at the ref.'
15
+ })
16
+ }, { description: 'Files of the repository at a ref.' });
17
+ export const FileContentResponseSchema = Type.Object({
18
+ path: Type.String({ description: 'Repository-relative path.' }),
19
+ ref: Type.String({ description: 'Ref from the request.' }),
20
+ content: Type.String({
21
+ description: 'Full file text. Empty for a binary file or a file that is not at the ref.'
22
+ }),
23
+ lines: Type.Array(Type.String(), {
24
+ description: 'Lines of the text. Empty for a binary file.'
25
+ }),
26
+ lineCount: Type.Integer({ description: 'Length of lines.' }),
27
+ isBinary: Type.Boolean({ description: 'The file is binary.' })
28
+ }, { description: 'One file at a ref.' });
29
+ export const OkSchema = Type.Object({
30
+ ok: Type.Literal(true, { description: 'Always true.' })
31
+ }, { description: 'The git command succeeded.' });
32
+ export const TreeParams = Type.Object({
33
+ ref: Type.String({
34
+ minLength: 1,
35
+ description: 'Commit, branch or tag. INDEX lists the index, WORKTREE the files on disk.'
36
+ })
37
+ });
38
+ export const FileParams = Type.Object({
39
+ '*': Type.String({
40
+ description: 'Repository-relative path. Slashes stay literal.'
41
+ })
42
+ });
43
+ export const FileContentQuery = Type.Object({
44
+ ref: Type.String({
45
+ minLength: 1,
46
+ description: 'Commit, branch or tag. INDEX reads the index, WORKTREE the file on disk.'
47
+ })
48
+ });
49
+ export const OptionalPathsBody = Type.Object({
50
+ paths: Type.Optional(Type.Array(Type.String(), {
51
+ description: 'Repository-relative paths. Omitted or empty means all.'
52
+ }))
53
+ }, { description: 'Paths for stage or unstage.' });
54
+ export const DiscardBody = Type.Object({
55
+ paths: Type.Array(Type.String(), {
56
+ minItems: 1,
57
+ description: 'Repository-relative paths. Required: there is no "discard all".'
58
+ })
59
+ }, { description: 'Paths to discard.' });
@@ -0,0 +1,23 @@
1
+ import { Type } from '@sinclair/typebox';
2
+ /** Fields of the error body that Fastify writes for each error. */
3
+ const API_ERROR_FIELDS = {
4
+ statusCode: Type.Integer({ description: 'HTTP status code.' }),
5
+ code: Type.Optional(Type.String({
6
+ description: 'Stable error code, for example GHT_HTTP_NOT_FOUND.'
7
+ })),
8
+ error: Type.String({ description: 'HTTP status text.' }),
9
+ message: Type.String({ description: 'What went wrong.' })
10
+ };
11
+ /** Error answers that every route can give. */
12
+ export const ERROR_RESPONSES = {
13
+ '4xx': Type.Object(API_ERROR_FIELDS, {
14
+ description: 'The request is not valid.'
15
+ }),
16
+ '5xx': Type.Object(API_ERROR_FIELDS, { description: 'The server failed.' })
17
+ };
18
+ export const ApiErrorSchema = Type.Object(API_ERROR_FIELDS, {
19
+ description: 'Error body of every route.'
20
+ });
21
+ export const NoContentSchema = Type.Null({
22
+ description: 'The request succeeded. The body is empty.'
23
+ });
@@ -0,0 +1,96 @@
1
+ import { Type } from '@sinclair/typebox';
2
+ import { Nullable } from "../utils/schemas.js";
3
+ import { CommentSchema } from "../comments/schemas.js";
4
+ export const ReviewStatusSchema = Type.Union([
5
+ Type.Literal('in_progress'),
6
+ Type.Literal('approved'),
7
+ Type.Literal('changes_requested')
8
+ ], { description: 'Review state.' });
9
+ /**
10
+ * 'local' snapshots staged + unstaged diffs together — the only source type
11
+ * the "Start review" UI offers, since a review's comments apply to both
12
+ * regardless of which tab they were left on (see ADR 0018). 'staged'/
13
+ * 'unstaged' remain valid API inputs (kept for API callers that want one
14
+ * side only) but have no UI entry point anymore. 'branch'/'commits' are
15
+ * unrelated future-MVP source types (ADR 0017), unaffected by this.
16
+ */
17
+ export const ReviewSourceTypeSchema = Type.Union([
18
+ Type.Literal('local'),
19
+ Type.Literal('staged'),
20
+ Type.Literal('unstaged'),
21
+ Type.Literal('branch'),
22
+ Type.Literal('commits')
23
+ ], {
24
+ description: 'Diff in the snapshot: local (staged and unstaged), one side, a branch or commits.'
25
+ });
26
+ const reviewSummaryFields = {
27
+ id: Type.String({ description: 'Review id.' }),
28
+ repositoryPath: Type.String({
29
+ description: 'Absolute repository path at creation.'
30
+ }),
31
+ baseRef: Nullable(Type.String({
32
+ description: 'Base ref for branch or commits reviews. Else null.'
33
+ })),
34
+ sourceType: ReviewSourceTypeSchema,
35
+ sourceRef: Nullable(Type.String({
36
+ description: 'Branch name for branch, the newer ref ("to") for commits. Else null.'
37
+ })),
38
+ status: ReviewStatusSchema,
39
+ name: Nullable(Type.String({
40
+ description: 'User or generated name ("source + date/time"). Unique within branch.'
41
+ })),
42
+ branch: Nullable(Type.String({
43
+ description: 'Branch at creation. The server does not update it.'
44
+ })),
45
+ createdAt: Type.String({ description: 'Creation time, ISO 8601.' }),
46
+ updatedAt: Type.String({ description: 'Last change time, ISO 8601.' })
47
+ };
48
+ export const ReviewSummarySchema = Type.Object(reviewSummaryFields, {
49
+ description: 'A review without its snapshot.'
50
+ });
51
+ export const ReviewSchema = Type.Object({
52
+ ...reviewSummaryFields,
53
+ snapshotData: Type.String({
54
+ description: 'JSON array of DiffFile, frozen at creation.'
55
+ })
56
+ }, { description: 'A review with its diff snapshot.' });
57
+ export const ReviewExportSchema = Type.Object({
58
+ review: ReviewSchema,
59
+ comments: Type.Array(CommentSchema, {
60
+ description: 'All comments of the review.'
61
+ })
62
+ }, { description: 'A review and its comments as JSON.' });
63
+ export const CreateReviewBody = Type.Object({
64
+ sourceType: Type.Optional(ReviewSourceTypeSchema),
65
+ sourceRef: Type.Optional(Type.String({
66
+ description: 'Branch name for branch, or the newer ref ("to") for commits.'
67
+ })),
68
+ baseRef: Type.Optional(Type.String({
69
+ description: 'Required for branch and commits: the base ref or the older ref ("from").'
70
+ })),
71
+ name: Type.Optional(Type.String({
72
+ description: 'Review name. Omitted or blank: the server makes one.'
73
+ }))
74
+ }, { description: 'A new review. sourceType defaults to local.' });
75
+ export const ReviewsQuery = Type.Object({
76
+ branch: Type.Optional(Type.String({ description: 'Branch filter. Default: current branch.' })),
77
+ search: Type.Optional(Type.String({
78
+ description: 'Text to find in review names, case-insensitive.'
79
+ })),
80
+ createdFrom: Type.Optional(Type.String({ description: 'Earliest creation time, ISO 8601.' })),
81
+ createdTo: Type.Optional(Type.String({ description: 'Latest creation time, ISO 8601.' })),
82
+ files: Type.Optional(Type.String({
83
+ description: 'Comma-separated file paths. A review matches if its snapshot has one of them.'
84
+ }))
85
+ });
86
+ export const UpdateReviewBody = Type.Object({
87
+ status: Type.Optional(ReviewStatusSchema)
88
+ }, { description: 'A review change. status is required.' });
89
+ export const ReviewIdParams = Type.Object({
90
+ id: Type.String({ description: 'Review id.' })
91
+ });
92
+ export const ExportQuery = Type.Object({
93
+ format: Type.Union([Type.Literal('json'), Type.Literal('markdown')], {
94
+ description: 'json gives the review and comments, markdown a report.'
95
+ })
96
+ });
@@ -0,0 +1,5 @@
1
+ /** Drops the snapshot, so a list never holds a review's whole diff (#56). */
2
+ export function toReviewSummary(review) {
3
+ const { snapshotData: _snapshot, ...summary } = review;
4
+ return summary;
5
+ }
@@ -0,0 +1,9 @@
1
+ import { Type } from '@sinclair/typebox';
2
+ /**
3
+ * A nullable value for a response schema. Request schemas put `Type.Null()`
4
+ * first instead, because Fastify's Ajv coerces null into the first type.
5
+ */
6
+ export function Nullable(schema) {
7
+ const { description } = schema;
8
+ return Type.Union([schema, Type.Null()], typeof description === 'string' ? { description } : {});
9
+ }