@tequila99/githuman 0.8.0 → 0.9.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (178) hide show
  1. package/README.md +39 -3
  2. package/dist/server/app.js +1 -0
  3. package/dist/server/db/migrations.js +6 -0
  4. package/dist/server/errors/note-assets.js +3 -0
  5. package/dist/server/repositories/notes-catalog.repo.js +4 -3
  6. package/dist/server/repositories/notes.repo.js +69 -27
  7. package/dist/server/routes/agent.js +1 -0
  8. package/dist/server/routes/notes.js +48 -2
  9. package/dist/server/services/agent/context-builder.js +13 -0
  10. package/dist/server/services/notes-external-assets.service.js +177 -0
  11. package/dist/server/services/notes-identity.service.js +112 -27
  12. package/dist/server/services/notes-operations.service.js +3 -5
  13. package/dist/server/services/notes.service.js +29 -4
  14. package/dist/server/utils/note-asset-format.js +1 -1
  15. package/dist/server/utils/remote-asset-request.js +193 -0
  16. package/dist/server/utils/request-abort.js +20 -0
  17. package/dist/shared/agents/schemas.js +14 -0
  18. package/dist/shared/notes/asset-formats.js +19 -1
  19. package/dist/shared/notes/constants.js +5 -0
  20. package/dist/shared/notes/external-assets.js +153 -0
  21. package/dist/shared/notes/note-path.js +17 -0
  22. package/dist/shared/notes/reference-path.js +2 -1
  23. package/dist/shared/notes/references.js +1 -1
  24. package/dist/shared/notes/schemas.js +60 -2
  25. package/dist/shared/utils/web-url.js +15 -0
  26. package/dist/web/assets/AppPage-DdmqiV2R.js +1 -0
  27. package/dist/web/assets/{ClosePopup-D2_HzWXW.js → ClosePopup-DhjIPp8s.js} +1 -1
  28. package/dist/web/assets/{CreateButton-pmDRGwna.js → CreateButton-BoxoJ7Jr.js} +2 -2
  29. package/dist/web/assets/{CreateReviewFabButton-CcXWnmSN.js → CreateReviewFabButton-CA5OsG7d.js} +1 -1
  30. package/dist/web/assets/IndexPage-CROp8XNF.js +1 -0
  31. package/dist/web/assets/IndexPage-F8MMAAkn.css +1 -0
  32. package/dist/web/assets/{MainLayout-DOGoQ-1m.js → MainLayout-DvfVDB2G.js} +4 -4
  33. package/dist/web/assets/{NoteVisualEditor-CClIKW0X.css → NoteVisualEditor-IH3s3d3W.css} +1 -1
  34. package/dist/web/assets/NoteVisualEditor-VfLNFzsD.js +1054 -0
  35. package/dist/web/assets/NotesPage-D_NiwDh8.js +4 -0
  36. package/dist/web/assets/NotesPage-KaCEnfdz.css +1 -0
  37. package/dist/web/assets/PanelResize-9uljPbE4.js +1 -0
  38. package/dist/web/assets/PanelResize-C51WwwLD.css +1 -0
  39. package/dist/web/assets/{PdfPreview-CdgeqqEv.js → PdfPreview-DgwnGJ2E.js} +1 -1
  40. package/dist/web/assets/{QFab-DyUFgj0W.js → QFab-D1X33Wdk.js} +1 -1
  41. package/dist/web/assets/{QForm-BiRkV9BY.js → QForm-BQMj3m1W.js} +1 -1
  42. package/dist/web/assets/QResizeObserver-D-9MeQ9q.js +1 -0
  43. package/dist/web/assets/{QScrollArea-DfPW35JQ.js → QScrollArea-DlLel0SO.js} +1 -1
  44. package/dist/web/assets/{QSelect-BpZ5KfWP.js → QSelect-mw3iS4Cp.js} +1 -1
  45. package/dist/web/assets/QSpace-BbB0w38T.js +1 -0
  46. package/dist/web/assets/QVirtualScroll-FtQn3PJj.js +1 -0
  47. package/dist/web/assets/ReviewDetailPage-D7x0R_2N.js +1 -0
  48. package/dist/web/assets/{ReviewsPage-BaxUZzUZ.js → ReviewsPage-C317IA1b.js} +1 -1
  49. package/dist/web/assets/StatusSwitcher-BQXo4sss.js +1 -0
  50. package/dist/web/assets/{TerminalView-DMJTXE-G.js → TerminalView-CTsFWvWm.js} +2 -2
  51. package/dist/web/assets/{abnfDiagram-O67JEVCF-CiOkMSRz.js → abnfDiagram-O67JEVCF-BYlDf7LI.js} +1 -1
  52. package/dist/web/assets/agent-store-ByB1OrjP.js +113 -0
  53. package/dist/web/assets/app-info-store-Gn-SNU3Y.js +1 -0
  54. package/dist/web/assets/architecture-7GRP2DOG-DNWQGMIX.js +1 -0
  55. package/dist/web/assets/{architectureDiagram-NJMV4G6O-BexHJffj.js → architectureDiagram-NJMV4G6O-CC0SmfK_.js} +1 -1
  56. package/dist/web/assets/{blockDiagram-BEXU5L5S-CEyB_5a1.js → blockDiagram-BEXU5L5S-C1Ny76Gc.js} +1 -1
  57. package/dist/web/assets/{c4Diagram-YGBWAQC7-BRNWA7xI.js → c4Diagram-YGBWAQC7-CP2if62Y.js} +1 -1
  58. package/dist/web/assets/channel-DW-wL7ua.js +1 -0
  59. package/dist/web/assets/{chunk-3FUC2YCW-BcQR1JDF.js → chunk-3FUC2YCW-cjM6hj3T.js} +1 -1
  60. package/dist/web/assets/{chunk-5DYCD2WN-D6aQ1hFt.js → chunk-5DYCD2WN-DAW_EwWQ.js} +1 -1
  61. package/dist/web/assets/{chunk-742MDFTN-BRgrFtkf.js → chunk-742MDFTN-nkStFRF-.js} +1 -1
  62. package/dist/web/assets/{chunk-7INBJB4K-3RBjXeq6.js → chunk-7INBJB4K-EQQsEy8I.js} +1 -1
  63. package/dist/web/assets/{chunk-7M6MHVWA-Malugc_N.js → chunk-7M6MHVWA-DUJ2C21w.js} +1 -1
  64. package/dist/web/assets/{chunk-7PRAP22T-Bb3gaISA.js → chunk-7PRAP22T-C1Bj5-NO.js} +1 -1
  65. package/dist/web/assets/{chunk-GTNCS2PH-Cps7XhUC.js → chunk-GTNCS2PH-DW39t1bj.js} +1 -1
  66. package/dist/web/assets/{chunk-GWA4HPMP-Bfp2Tg3_.js → chunk-GWA4HPMP-i49YwHnS.js} +1 -1
  67. package/dist/web/assets/{chunk-MBY4JIJT-IfXbX1Ur.js → chunk-MBY4JIJT-C4mXlV61.js} +1 -1
  68. package/dist/web/assets/{chunk-NETBCI7D-r2fmahwk.js → chunk-NETBCI7D-DoRSO-kq.js} +1 -1
  69. package/dist/web/assets/{chunk-O7XYJQB3-sadsiWmA.js → chunk-O7XYJQB3-D38ZePQT.js} +1 -1
  70. package/dist/web/assets/{chunk-UA2S7LBM-DibfwtDn.js → chunk-UA2S7LBM-DmOI-u6c.js} +1 -1
  71. package/dist/web/assets/{chunk-WEXAMYUT-DBouMgEO.js → chunk-WEXAMYUT-Dv710fF3.js} +1 -1
  72. package/dist/web/assets/{chunk-Z7XXMR3K-fAHToON5.js → chunk-Z7XXMR3K-CWHlAHIu.js} +1 -1
  73. package/dist/web/assets/{chunk-ZIGJFQKS-CJG1UIHO.js → chunk-ZIGJFQKS-Bid5tGRk.js} +1 -1
  74. package/dist/web/assets/{classDiagram-v2-NBCMYWYE-CPHP5ynj.js → classDiagram-v2-NBCMYWYE-C1770G1t.js} +1 -1
  75. package/dist/web/assets/{cynefin-OW5HDTMX-BU7_LYGR.js → cynefin-OW5HDTMX-CWxF4ZIN.js} +1 -1
  76. package/dist/web/assets/{cynefinDiagram-VND7K2PF-BXFEy9Io.js → cynefinDiagram-VND7K2PF-DcFBTI6m.js} +1 -1
  77. package/dist/web/assets/{dagre-6A5THRUB-aKpdpQ18.js → dagre-6A5THRUB-CP5wTvgN.js} +1 -1
  78. package/dist/web/assets/{diagram-22UHCM2B-ddbRgIQ9.js → diagram-22UHCM2B-mLUm1v32.js} +1 -1
  79. package/dist/web/assets/{diagram-3UASUU5V-vNYy6WP_.js → diagram-3UASUU5V-CP0OFpuS.js} +1 -1
  80. package/dist/web/assets/{diagram-ATOU4E4O-B9Z3zFfp.js → diagram-ATOU4E4O-DolFulHH.js} +1 -1
  81. package/dist/web/assets/{diagram-CDSNMT55-D3TaDPXU.js → diagram-CDSNMT55-Bs17AhwI.js} +1 -1
  82. package/dist/web/assets/{diagram-MLGK6HIB-DOXcXm2Y.js → diagram-MLGK6HIB-BfhsJwV7.js} +1 -1
  83. package/dist/web/assets/{diagram-MPIPVDR6-B5E2J8pO.js → diagram-MPIPVDR6-B1c-Ikp-.js} +1 -1
  84. package/dist/web/assets/{dist-Kd0W7Y_b.js → dist-Bm8OAr9J.js} +1 -1
  85. package/dist/web/assets/{dist-C9STut2J.js → dist-BsjKEQwP.js} +1 -1
  86. package/dist/web/assets/{dist-BFbA030x.js → dist-C6YiC_nY.js} +1 -1
  87. package/dist/web/assets/{dist-BXUopzfH.js → dist-CyyHluOV.js} +1 -1
  88. package/dist/web/assets/{dist-C9cJQvJH.js → dist-D6WXR3TF.js} +1 -1
  89. package/dist/web/assets/{dist-PDsGXJi3.js → dist-DkKvAWYi.js} +1 -1
  90. package/dist/web/assets/{dist-BGzvKGEu.js → dist-DuW-c5so.js} +1 -1
  91. package/dist/web/assets/{dist-DSPzgfNo.js → dist-d8yBJi6g.js} +1 -1
  92. package/dist/web/assets/{dist-Cxd9qKh7.js → dist-g0NXSFab.js} +1 -1
  93. package/dist/web/assets/{dist-BX8UA0fl.js → dist-yQ-fH4og.js} +1 -1
  94. package/dist/web/assets/{dist-CRoJr3t6.js → dist-zqKz9Qrf.js} +1 -1
  95. package/dist/web/assets/{ebnfDiagram-ZINNZB2B-D-OCB_ud.js → ebnfDiagram-ZINNZB2B-BvQ60ZD9.js} +1 -1
  96. package/dist/web/assets/{elk-276RUBZZ-CzjP8RHy.js → elk-276RUBZZ-C27WMGnY.js} +1 -1
  97. package/dist/web/assets/{erDiagram-OPXOYQCR-Clohps7p.js → erDiagram-OPXOYQCR-CdfkOXb9.js} +1 -1
  98. package/dist/web/assets/eventmodeling-NTZA5JFV-C1sONJTj.js +1 -0
  99. package/dist/web/assets/flowDiagram-KWPJA3E3-CU2TsxFx.js +1 -0
  100. package/dist/web/assets/{ganttDiagram-FUAMR5RP-ZWH4HYq5.js → ganttDiagram-FUAMR5RP-CuKNmNbv.js} +1 -1
  101. package/dist/web/assets/{gitGraph-4MIJSDKK-C3AfRnC8.js → gitGraph-4MIJSDKK-DBGr6EfP.js} +1 -1
  102. package/dist/web/assets/{gitGraphDiagram-X574FWY7-BobkQARX.js → gitGraphDiagram-X574FWY7-BUZgiyYd.js} +1 -1
  103. package/dist/web/assets/i18n-Cxfq3B8n.js +1 -0
  104. package/dist/web/assets/index-DKirIJG5.js +2 -0
  105. package/dist/web/assets/{info-A6RAGUB7-Dh_SAe09.js → info-A6RAGUB7-CjnEima-.js} +1 -1
  106. package/dist/web/assets/{infoDiagram-VRGFBTTK-izDlhNTw.js → infoDiagram-VRGFBTTK-D91sa8do.js} +1 -1
  107. package/dist/web/assets/{init-app-C1a6BWWN.js → init-app-B6mIRN6f.js} +1 -1
  108. package/dist/web/assets/{ishikawaDiagram-OU5B5YK6-BJ89XzVJ.js → ishikawaDiagram-OU5B5YK6-2K-kH65E.js} +1 -1
  109. package/dist/web/assets/{journeyDiagram-ZHPQQLJL-poV-f08K.js → journeyDiagram-ZHPQQLJL-CkENPVJA.js} +1 -1
  110. package/dist/web/assets/{kanban-definition-PNTS6WVX-Cn3FEnH7.js → kanban-definition-PNTS6WVX-BHJODz7s.js} +1 -1
  111. package/dist/web/assets/{line-bE9Mqcqy.js → line-C6VvIL_x.js} +1 -1
  112. package/dist/web/assets/markdown-192Ev3_O.js +15 -0
  113. package/dist/web/assets/{mermaid-parser.core-Cua7H5pF.js → mermaid-parser.core-DMJg-lnr.js} +3 -3
  114. package/dist/web/assets/{mermaid.core-BMPOA8T-.js → mermaid.core-Dd40xnyQ.js} +4 -4
  115. package/dist/web/assets/{mindmap-definition-NLK3R4M7-DaTGr8Fs.js → mindmap-definition-NLK3R4M7-DhfAMSga.js} +1 -1
  116. package/dist/web/assets/{packet-AYTQ26CC-DmLfghd9.js → packet-AYTQ26CC-DZUk7rEw.js} +1 -1
  117. package/dist/web/assets/{pegDiagram-GJSIUBJH-CTlxXcbf.js → pegDiagram-GJSIUBJH-W22cBCh2.js} +1 -1
  118. package/dist/web/assets/{pie-WAS4IAKB-52oNsEtc.js → pie-WAS4IAKB-BMvwPiHK.js} +1 -1
  119. package/dist/web/assets/{pieDiagram-5QR66LMP-CbJtB1te.js → pieDiagram-5QR66LMP-CZ2EVCmx.js} +1 -1
  120. package/dist/web/assets/pinia-Cp_DCu4m.js +1 -0
  121. package/dist/web/assets/{quadrantDiagram-O4NWA36T-DCg-uD08.js → quadrantDiagram-O4NWA36T-DBS45bbr.js} +1 -1
  122. package/dist/web/assets/{radar-RG4KPBEZ-9uA3AOZR.js → radar-RG4KPBEZ-BDkD2JF1.js} +1 -1
  123. package/dist/web/assets/{railroad-74A4TZTK-BPxQfpxv.js → railroad-74A4TZTK-BH_sA5bU.js} +1 -1
  124. package/dist/web/assets/railroad-abnf-HS5TGJTU-D6lX11kj.js +1 -0
  125. package/dist/web/assets/railroad-ebnf-LZEXJU2U-D3N0Z6uV.js +1 -0
  126. package/dist/web/assets/railroad-peg-WCYAUIDC-DOaenw8H.js +1 -0
  127. package/dist/web/assets/{railroadDiagram-XR7U4H2S-CwBNr6fr.js → railroadDiagram-XR7U4H2S-CrI1TIG1.js} +1 -1
  128. package/dist/web/assets/{requirementDiagram-PLB6GJNP-BbtZt0St.js → requirementDiagram-PLB6GJNP-BbtaFtGn.js} +1 -1
  129. package/dist/web/assets/{safe-storage-Bn-sHAYw.js → safe-storage-DekXHbgc.js} +1 -1
  130. package/dist/web/assets/{sankeyDiagram-IPEJSGJF-DRWdMnFl.js → sankeyDiagram-IPEJSGJF-DRU9tLf3.js} +1 -1
  131. package/dist/web/assets/{sequenceDiagram-PO4LG4MO-C4tWeWpt.js → sequenceDiagram-PO4LG4MO-RquVuniP.js} +1 -1
  132. package/dist/web/assets/{stateDiagram-v2-GCMORJYK-pE0T1U5E.js → stateDiagram-v2-GCMORJYK-CKjC8JTS.js} +1 -1
  133. package/dist/web/assets/{swimlanes-2SLR337P-DKky6E3p.js → swimlanes-2SLR337P-B-5FN1NY.js} +1 -1
  134. package/dist/web/assets/swimlanesDiagram-TC7HE7FX-B5SL6iu0.js +8 -0
  135. package/dist/web/assets/theme-C1B2V2RV.js +1 -0
  136. package/dist/web/assets/{timeline-definition-EJHVYXUP-CsRzcMLr.js → timeline-definition-EJHVYXUP-DbG_YVkW.js} +1 -1
  137. package/dist/web/assets/{treeView-Q6P3EWNA-CQYqoPG3.js → treeView-Q6P3EWNA-BgkrW_FP.js} +1 -1
  138. package/dist/web/assets/{treemap-WGGIJYW6-BzFw4gFs.js → treemap-WGGIJYW6-Dp8ISpo7.js} +1 -1
  139. package/dist/web/assets/use-add-agent-context-3-KB2D10.js +1 -0
  140. package/dist/web/assets/{use-app-theme-9qFZUBqz.js → use-app-theme-D_HR0B3W.js} +1 -1
  141. package/dist/web/assets/use-panel-width-DIzwb-yt.js +1 -0
  142. package/dist/web/assets/use-server-events-C13Sw-rU.js +1 -0
  143. package/dist/web/assets/{usecaseDiagram-POWQR4AR-IMfiBw6h.js → usecaseDiagram-POWQR4AR-DwRXMhnD.js} +1 -1
  144. package/dist/web/assets/{vennDiagram-UO4OBE2U-JKdzpNy2.js → vennDiagram-UO4OBE2U-CxdeHvjU.js} +1 -1
  145. package/dist/web/assets/{wardley-WFR3VGLG-B9Yiwlp8.js → wardley-WFR3VGLG-C8gq8AfM.js} +1 -1
  146. package/dist/web/assets/{wardleyDiagram-VNRHLVJA-BVoc2hGS.js → wardleyDiagram-VNRHLVJA-C9Jo2KAj.js} +1 -1
  147. package/dist/web/assets/window-store-CnPjWx6-.js +1 -0
  148. package/dist/web/assets/{xychartDiagram-PMCCYNJV-B_XV1aim.js → xychartDiagram-PMCCYNJV-D3tp7hdU.js} +1 -1
  149. package/dist/web/index.html +1 -1
  150. package/package.json +3 -1
  151. package/dist/web/assets/AppPage-cl-3VfAl.js +0 -1
  152. package/dist/web/assets/IndexPage-CYFIGWy9.css +0 -1
  153. package/dist/web/assets/IndexPage-CmBmgu4A.js +0 -1
  154. package/dist/web/assets/NoteVisualEditor-zjZr9aod.js +0 -1056
  155. package/dist/web/assets/NotesPage-C5VXA9Bb.css +0 -1
  156. package/dist/web/assets/NotesPage-Jeva3Jta.js +0 -114
  157. package/dist/web/assets/QSpace-BRLIXkHB.js +0 -1
  158. package/dist/web/assets/ReviewDetailPage-CpJPrSKO.js +0 -1
  159. package/dist/web/assets/StatusSwitcher-D5UG_YWP.js +0 -1
  160. package/dist/web/assets/agent-icon-Dx8WBYX_.js +0 -1
  161. package/dist/web/assets/agent-store-B1VK6S0D.js +0 -1
  162. package/dist/web/assets/app-info-store-D76h_OdJ.js +0 -1
  163. package/dist/web/assets/architecture-7GRP2DOG-Byc4Oz95.js +0 -1
  164. package/dist/web/assets/channel-DbRAFIx1.js +0 -1
  165. package/dist/web/assets/equal-arrays-GA_7ZCAb.js +0 -1
  166. package/dist/web/assets/eventmodeling-NTZA5JFV-FWK4tnN_.js +0 -1
  167. package/dist/web/assets/flowDiagram-KWPJA3E3-C10B5_6J.js +0 -1
  168. package/dist/web/assets/i18n-CC_rvfan.js +0 -1
  169. package/dist/web/assets/index-Ic8yFbJg.js +0 -2
  170. package/dist/web/assets/markdown-D85iiL_9.js +0 -15
  171. package/dist/web/assets/pinia-BlCzIRX-.js +0 -1
  172. package/dist/web/assets/railroad-abnf-HS5TGJTU-CPVHNg05.js +0 -1
  173. package/dist/web/assets/railroad-ebnf-LZEXJU2U-DAjTaOUg.js +0 -1
  174. package/dist/web/assets/railroad-peg-WCYAUIDC-DkxyKsj4.js +0 -1
  175. package/dist/web/assets/swimlanesDiagram-TC7HE7FX-CPhibEqM.js +0 -8
  176. package/dist/web/assets/theme-B-g7wQEs.js +0 -1
  177. package/dist/web/assets/use-panel-width-Dyh49bod.js +0 -1
  178. package/dist/web/assets/use-server-events-24_soFuj.js +0 -1
package/README.md CHANGED
@@ -210,12 +210,25 @@ MIT, см. [LICENSE](LICENSE).
210
210
  каталога запуска приложения. Поддерживаются папки, визуальный редактор, полный
211
211
  исходник Markdown, редактируемые свойства YAML frontmatter и обычные относительные ссылки.
212
212
  Внутренние ссылки активируют заметку в текущем окне; внешние открываются в новой вкладке.
213
+ **Add to agent** в контекстном меню заметки и панели редактора добавляет её в чат
214
+ как удаляемый бейдж. При отправке агент получает актуальный Markdown вместе
215
+ с YAML-свойствами; связанные заметки и изображения не включаются.
216
+ Несохранённые изменения сохраняются перед отправкой, ошибки сохранения её блокируют.
213
217
  Кнопка **Link to note** помогает выбрать заметку для ссылки; теги со счётчиками
214
218
  в дереве фильтруют список. Изменения автоматически сохраняются после паузы ввода.
215
219
 
216
220
  Другой каталог задаётся через `serve --notes-dir ./notes` или
217
221
  `GITHUMAN_NOTES_DIR`; относительный путь считается от каталога запуска.
218
- Такой каталог можно версионировать. Личное хранилище по умолчанию
222
+ Такой каталог можно версионировать.
223
+ При создании вводится короткое имя без пути: файл создаётся в выбранном каталоге,
224
+ расширение `.md` добавляется автоматически. Заметки можно перетаскивать между
225
+ каталогами в дереве слева и в обзоре каталога справа, в том числе между панелями.
226
+ Кнопка корня слева и кнопка родительского каталога справа также принимают заметки.
227
+ Перенос сохраняет UUID и обновляет относительные ссылки; совпадающий файл не перезаписывается.
228
+ **Delete** в контекстном меню скрывает заметку в приложении: отметка хранится в БД,
229
+ сам Markdown остаётся на диске. Сканирование и перезапуск не возвращают её в список;
230
+ восстановление через интерфейс пока не предусмотрено.
231
+ Личное хранилище по умолчанию
219
232
  исключается локально через Git `info/exclude`; уже отслеживаемые файлы
220
233
  автоматически из индекса не удаляются.
221
234
 
@@ -226,16 +239,39 @@ MIT, см. [LICENSE](LICENSE).
226
239
  в дереве. При альтернативном Git-хранилище исключите `.history/` из Git,
227
240
  если резервные копии не нужно публиковать.
228
241
 
242
+ Полный Markdown с YAML-шапкой можно вставить в пустое тело визуального редактора
243
+ или вместо всего выделенного тела: свойства импортируются в Properties, UUID
244
+ заметки сохраняется. Некорректный YAML отклоняется с сообщением об ошибке.
245
+ При неподдерживаемой разметке сохраняется полный исходник
246
+ и включается режим Markdown.
247
+
229
248
  Визуальное редактирование может нормализовать оформление списков, таблиц
230
249
  и reference-ссылок. Открытие и переключение режимов без правок файл не
231
250
  перезаписывают. Если при открытии визуальный редактор теряет конструкции,
232
251
  приложение сохраняет исходный текст и переключается в Markdown. Вставленные PNG, JPEG, GIF и WebP (до 10 МБ) сохраняются в `assets/` внутри
233
252
  хранилища заметок и доступны в визуальном редакторе после перезапуска. Ссылки
234
- на вложения не зависят от хоста или порта сервиса. Внешние изображения
235
- остаются в исходном режиме. Размер заметки ограничен 1 МБ; символические ссылки в
253
+ на вложения не зависят от хоста или порта сервиса. Внешние HTTP/HTTPS-изображения доступны в визуальном редакторе. Размер заметки ограничен 1 МБ; символические ссылки в
236
254
  пути хранилища не поддерживаются. Черновик после аварии браузера пока
237
255
  не восстанавливается.
238
256
 
257
+ Команда **Download external attachments** в панели заметки или **Ctrl+Shift+D**, пока заметка открыта на экране, независимо от фокуса,
258
+ открывает список внешних вложений.
259
+ Клавиша определяется по физическому коду, независимо от раскладки.
260
+ Картинки и ссылки на PNG/JPEG/GIF/WebP/PDF можно выбрать перед загрузкой;
261
+ одинаковые адреса скачиваются один раз. Размер картинок ограничен 10 МБ,
262
+ PDF до 50 МБ загружаются без дополнительного вопроса, от 50 до 100 МБ требуют
263
+ подтверждения, свыше 100 МБ отклоняются. При неизвестном размере подтверждение
264
+ запрашивается после остановки на пороге 50 МБ; согласие запускает загрузку заново.
265
+ Лимит одной операции — 200 МБ сохранённых файлов. Проверка размера действует
266
+ и во время скачивания, не только по заголовкам сервера.
267
+
268
+ Успешные загрузки заменяют адреса в текущем Markdown, ошибки оставляют внешние
269
+ ссылки. PDF остаются обычными ссылками, свойства и подписи сохраняются.
270
+ Скачивание работает с публичными HTTP/HTTPS-адресами без браузерных cookies;
271
+ обычные ссылки определяются по расширению в пути URL. **Restore external links**
272
+ возвращает последнюю замену, пока текст не изменён. Обновление визуального
273
+ редактора после импорта сбрасывает его прежнюю историю Ctrl+Z.
274
+
239
275
  Заметки имеют UUID в SQLite-каталоге; необязательный `githuman_id` в YAML помогает
240
276
  распознавать внешние перемещения. Back/Forward и история браузера возвращают между
241
277
  заметками. Rename в контекстном меню сохраняет ID и исправляет относительные ссылки;
@@ -105,6 +105,7 @@ export function buildApp(options = {}) {
105
105
  }
106
106
  if (options.agentPresets) {
107
107
  app.register(agentRoutes, {
108
+ notes,
108
109
  registry: createSessionRegistry({
109
110
  presets: options.agentPresets,
110
111
  repositoryPath
@@ -88,6 +88,12 @@ export function applyMigrations(db) {
88
88
  CREATE INDEX IF NOT EXISTS review_files_review_id_idx
89
89
  ON review_files(review_id);
90
90
  `);
91
+ const noteColumns = db
92
+ .prepare('PRAGMA table_info(note_catalog)')
93
+ .all();
94
+ if (!noteColumns.some(column => column.name === 'deleted_at')) {
95
+ db.exec('ALTER TABLE note_catalog ADD COLUMN deleted_at TEXT');
96
+ }
91
97
  const commentColumns = db
92
98
  .prepare('PRAGMA table_info(comments)')
93
99
  .all();
@@ -0,0 +1,3 @@
1
+ import createError from '@fastify/error';
2
+ export const NoteAssetLimitError = createError('GHT_NOTE_ASSET_LIMIT', '%s', 413);
3
+ export const NoteAssetQueueError = createError('GHT_NOTE_ASSET_QUEUE', 'Attachment download queue is full', 429);
@@ -7,6 +7,7 @@ function toNote(row) {
7
7
  version: row.version,
8
8
  createdAt: row.created_at,
9
9
  updatedAt: row.updated_at,
10
+ deletedAt: row.deleted_at,
10
11
  redirectId: row.redirect_id
11
12
  };
12
13
  }
@@ -48,9 +49,9 @@ export class NotesCatalogRepository {
48
49
  }
49
50
  put(note) {
50
51
  this.db
51
- .prepare(`INSERT INTO note_catalog(storage_id,id,path,presence,version,created_at,updated_at,path_changed_at,redirect_id) VALUES(?,?,?,?,?,?,?,?,?)
52
- ON CONFLICT(storage_id,id) DO UPDATE SET path=excluded.path,presence=excluded.presence,version=excluded.version,updated_at=excluded.updated_at,path_changed_at=CASE WHEN note_catalog.path != excluded.path THEN strftime('%Y-%m-%dT%H:%M:%fZ','now') ELSE note_catalog.path_changed_at END,redirect_id=excluded.redirect_id`)
53
- .run(this.storageId, note.id, note.path, note.presence, note.version, note.createdAt, note.updatedAt, note.updatedAt, note.redirectId);
52
+ .prepare(`INSERT INTO note_catalog(storage_id,id,path,presence,version,created_at,updated_at,path_changed_at,redirect_id,deleted_at) VALUES(?,?,?,?,?,?,?,?,?,?)
53
+ ON CONFLICT(storage_id,id) DO UPDATE SET path=excluded.path,presence=excluded.presence,version=excluded.version,updated_at=excluded.updated_at,path_changed_at=CASE WHEN note_catalog.path != excluded.path THEN strftime('%Y-%m-%dT%H:%M:%fZ','now') ELSE note_catalog.path_changed_at END,redirect_id=excluded.redirect_id,deleted_at=excluded.deleted_at`)
54
+ .run(this.storageId, note.id, note.path, note.presence, note.version, note.createdAt, note.updatedAt, note.updatedAt, note.redirectId, note.deletedAt);
54
55
  }
55
56
  links(id) {
56
57
  const rows = this.db
@@ -1,12 +1,13 @@
1
+ import { noteChildPath, noteParentPath } from "../../shared/notes/note-path.js";
1
2
  import { constants } from 'node:fs';
2
- import { randomUUID } from 'node:crypto';
3
+ import { createHash, randomUUID } from 'node:crypto';
3
4
  import { lstat, link, mkdir, open, readdir, realpath, rename, unlink, writeFile } from 'node:fs/promises';
4
5
  import { basename, dirname, join, resolve, sep } from 'node:path';
5
6
  import { BadRequestError, NotFoundError } from "../errors/http.js";
6
7
  import { isSystemError } from "../utils/system-error.js";
7
8
  import { sha256Hex } from "../utils/hash.js";
8
9
  import { NoteConflictError, NoteExistsError } from "../errors/notes.js";
9
- import { MAX_NOTE_ASSET_BYTES } from "../../shared/notes/constants.js";
10
+ import { noteAssetFormat } from "../../shared/notes/asset-formats.js";
10
11
  // Bound file reads before allocating document buffers.
11
12
  const MAX_BYTES = 1000000;
12
13
  export class NotesRepository {
@@ -213,36 +214,72 @@ export class NotesRepository {
213
214
  });
214
215
  }
215
216
  async saveAsset(bytes, extension) {
216
- const id = sha256Hex(bytes) + '.' + extension;
217
- const target = await this.path('assets/' + id, false);
218
- await mkdir(dirname(target), { recursive: true });
219
- await this.path('assets/' + id, false);
220
- const temporary = join(dirname(target), '.asset-' + randomUUID());
217
+ const staged = await this.stageAsset((async function* () {
218
+ yield bytes;
219
+ })(), extension, new AbortController().signal);
220
+ try {
221
+ return await staged.commit();
222
+ }
223
+ finally {
224
+ await staged.dispose();
225
+ }
226
+ }
227
+ async stageAsset(chunks, extension, signal) {
228
+ const format = noteAssetFormat(extension);
229
+ if (!format)
230
+ throw new BadRequestError('Unsupported attachment format');
231
+ const probe = await this.path('assets/probe.png', false);
232
+ await mkdir(dirname(probe), { recursive: true });
233
+ await this.path('assets/probe.png', false);
234
+ const temporary = join(dirname(probe), '.asset-' + randomUUID());
235
+ const dispose = async () => {
236
+ await unlink(temporary).catch(error => {
237
+ if (!isSystemError(error, 'ENOENT'))
238
+ throw error;
239
+ });
240
+ };
221
241
  try {
222
242
  const file = await open(temporary, 'wx', 0o600);
243
+ const hash = createHash('sha256');
244
+ let bytes = 0;
223
245
  try {
224
- await file.writeFile(bytes);
246
+ for await (const chunk of chunks) {
247
+ signal.throwIfAborted();
248
+ bytes += chunk.length;
249
+ if (bytes > format.maxBytes) {
250
+ throw new BadRequestError('Attachment exceeds its size limit');
251
+ }
252
+ hash.update(chunk);
253
+ await file.writeFile(chunk);
254
+ }
255
+ if (!bytes)
256
+ throw new BadRequestError('Empty attachment');
225
257
  await file.sync();
226
258
  }
227
259
  finally {
228
260
  await file.close();
229
261
  }
230
- await this.path('assets/' + id, false);
231
- try {
232
- await link(temporary, target);
233
- }
234
- catch (error) {
235
- if (!isSystemError(error, 'EEXIST'))
236
- throw error;
237
- }
262
+ const id = hash.digest('hex') + '.' + extension;
263
+ return {
264
+ dispose,
265
+ commit: async () => {
266
+ signal.throwIfAborted();
267
+ const target = await this.path('assets/' + id, false);
268
+ try {
269
+ await link(temporary, target);
270
+ }
271
+ catch (error) {
272
+ if (!isSystemError(error, 'EEXIST'))
273
+ throw error;
274
+ }
275
+ return id;
276
+ }
277
+ };
238
278
  }
239
- finally {
240
- await unlink(temporary).catch(error => {
241
- if (!isSystemError(error, 'ENOENT'))
242
- throw error;
243
- });
279
+ catch (error) {
280
+ await dispose();
281
+ throw error;
244
282
  }
245
- return id;
246
283
  }
247
284
  async readAsset(id) {
248
285
  const target = await this.path('assets/' + id, false);
@@ -250,13 +287,18 @@ export class NotesRepository {
250
287
  const file = await open(target, constants.O_RDONLY | constants.O_NOFOLLOW);
251
288
  try {
252
289
  const stat = await file.stat();
253
- if (!stat.isFile() || stat.size > MAX_NOTE_ASSET_BYTES) {
254
- throw new BadRequestError('Invalid image attachment');
290
+ const format = noteAssetFormat(id.split('.').at(-1));
291
+ if (!format ||
292
+ !stat.isFile() ||
293
+ !stat.size ||
294
+ stat.size > format.maxBytes) {
295
+ throw new BadRequestError('Invalid attachment');
255
296
  }
256
- return await file.readFile();
297
+ return file.createReadStream({ autoClose: true, end: stat.size - 1 });
257
298
  }
258
- finally {
299
+ catch (error) {
259
300
  await file.close();
301
+ throw error;
260
302
  }
261
303
  }
262
304
  catch (error) {
@@ -340,7 +382,7 @@ export class NotesRepository {
340
382
  if (path.split('/')[0] === 'assets') {
341
383
  throw new BadRequestError('Cannot rename attachments');
342
384
  }
343
- const targetPath = [...path.split('/').slice(0, -1), name].join('/');
385
+ const targetPath = noteChildPath(noteParentPath(path), name);
344
386
  if (targetPath.split('/')[0] === 'assets') {
345
387
  throw new BadRequestError('Reserved folder name');
346
388
  }
@@ -124,6 +124,7 @@ export async function agentRoutes(app, opts) {
124
124
  }
125
125
  session.assertCanPrompt();
126
126
  const blocks = await buildPromptBlocks(request.body, {
127
+ notes: opts.notes,
127
128
  db,
128
129
  repositoryPath,
129
130
  embeddedContext: session.supportsEmbeddedContext,
@@ -1,11 +1,14 @@
1
+ import { NotesExternalAssetsService } from "../services/notes-external-assets.service.js";
2
+ import { InspectExternalAssetBody, PreviewExternalAssetBody, ImportExternalAssetBody, ExternalAssetInspectionSchema, ExternalAssetResultSchema } from "../../shared/notes/schemas.js";
1
3
  import multipart from '@fastify/multipart';
2
4
  import { MAX_NOTE_ASSET_BYTES } from "../../shared/notes/constants.js";
3
5
  import { noteAssetFormat } from "../../shared/notes/asset-formats.js";
4
6
  import { NoteRecordSchema, NoteIdQuery, NoteDescriptorSchema, NoteResolveBody, NoteResolutionSchema, NoteGraphSchema, ResolveIdentityBody, NoteMutationSchema, UploadNoteAssetBody, NoteAssetSchema, NoteAssetContentSchema, NoteAssetParams, NoteQuery, NoteSchema, NotesIndexSchema, CreateNoteBody, SaveNoteBody, RenameNoteBody, CreateFolderBody } from "../../shared/notes/schemas.js";
5
7
  import { ERROR_RESPONSES, NoContentSchema } from "../../shared/http/schemas.js";
6
8
  import { BadRequestError } from "../errors/http.js";
9
+ import { withRequestSignal } from "../utils/request-abort.js";
7
10
  import { createNotesOriginHook } from "../hooks/notes/same-origin.js";
8
- export async function notesRoutes(app, { service, loopbackBind = false }) {
11
+ export async function notesRoutes(app, { service, loopbackBind = false, externalAssets = new NotesExternalAssetsService() }) {
9
12
  await app.register(multipart, {
10
13
  attachFieldsToBody: true,
11
14
  limits: { fileSize: MAX_NOTE_ASSET_BYTES, files: 1, fields: 0, parts: 1 }
@@ -31,10 +34,42 @@ export async function notesRoutes(app, { service, loopbackBind = false }) {
31
34
  reply.code(201);
32
35
  return asset;
33
36
  });
37
+ typed.post('/api/notes/assets/inspect', {
38
+ schema: {
39
+ tags: ['notes'],
40
+ summary: 'Inspect an external attachment',
41
+ body: InspectExternalAssetBody,
42
+ response: { 200: ExternalAssetInspectionSchema, ...ERROR_RESPONSES }
43
+ }
44
+ }, (request, reply) => withRequestSignal(reply, signal => externalAssets.inspect(request.body.url, signal)));
45
+ typed.post('/api/notes/assets/import', {
46
+ schema: {
47
+ tags: ['notes'],
48
+ summary: 'Import an external attachment',
49
+ body: ImportExternalAssetBody,
50
+ response: { 200: ExternalAssetResultSchema, ...ERROR_RESPONSES }
51
+ }
52
+ }, (request, reply) => withRequestSignal(reply, signal => externalAssets.importAsset(request.body, signal, (chunks, extension, saveSignal) => service.importAsset(chunks, extension, saveSignal))));
53
+ // The typed provider rejects a binary payload for the content schema.
54
+ app.post('/api/notes/assets/preview', {
55
+ schema: {
56
+ tags: ['notes'],
57
+ summary: 'Preview an external image',
58
+ body: PreviewExternalAssetBody,
59
+ response: { 200: NoteAssetContentSchema, ...ERROR_RESPONSES }
60
+ }
61
+ }, async (request, reply) => {
62
+ const preview = await withRequestSignal(reply, signal => externalAssets.preview(request.body.url, signal));
63
+ reply
64
+ .header('Cache-Control', 'no-store')
65
+ .header('X-Content-Type-Options', 'nosniff')
66
+ .type(preview.mime);
67
+ return reply.send(preview.body);
68
+ });
34
69
  app.get('/api/notes/assets/:id', {
35
70
  schema: {
36
71
  tags: ['notes'],
37
- summary: 'Read a stored note image',
72
+ summary: 'Read a stored note attachment',
38
73
  params: NoteAssetParams,
39
74
  response: { 200: NoteAssetContentSchema, ...ERROR_RESPONSES }
40
75
  }
@@ -143,6 +178,17 @@ export async function notesRoutes(app, { service, loopbackBind = false }) {
143
178
  response: { 200: NoteSchema, ...ERROR_RESPONSES }
144
179
  }
145
180
  }, request => service.save(request.body.path, request.body.text, request.body.version, request.body.id, request.body.storageId));
181
+ typed.post('/api/notes/delete', {
182
+ schema: {
183
+ tags: ['notes'],
184
+ summary: 'Hide a note without removing its file',
185
+ body: NoteIdQuery,
186
+ response: { 204: NoContentSchema, ...ERROR_RESPONSES }
187
+ }
188
+ }, async (request, reply) => {
189
+ await service.delete(request.body.id, request.body.storageId);
190
+ reply.code(204).send(null);
191
+ });
146
192
  typed.post('/api/notes/rename', {
147
193
  schema: {
148
194
  tags: ['notes'],
@@ -162,7 +162,20 @@ async function buildAttachmentItem(item, deps) {
162
162
  ...(item.mimeType === '' ? {} : { mimeType: item.mimeType })
163
163
  };
164
164
  }
165
+ async function buildNoteItem(item, deps) {
166
+ try {
167
+ if (!deps.notes)
168
+ throw new AgentContextError('Notes are unavailable');
169
+ const note = await deps.notes.identity.readId(item.noteId, item.storageId);
170
+ return embed(deps, `githuman://note/${encodeURIComponent(item.storageId)}/${encodeURIComponent(item.noteId)}`, `Note ${note.path}:`, 'text/markdown', `Note: ${note.path}\n\n${note.text}`);
171
+ }
172
+ catch (error) {
173
+ throw asContextError(error);
174
+ }
175
+ }
165
176
  function buildItem(item, deps) {
177
+ if (item.kind === 'note')
178
+ return buildNoteItem(item, deps);
166
179
  if (item.kind === 'diff') {
167
180
  return buildDiffItem(item, deps);
168
181
  }
@@ -0,0 +1,177 @@
1
+ import PQueue from 'p-queue';
2
+ import { BadRequestError } from "../errors/http.js";
3
+ import { NoteAssetLimitError, NoteAssetQueueError } from "../errors/note-assets.js";
4
+ import { remoteAssetRequest, translateRemoteAssetError } from "../utils/remote-asset-request.js";
5
+ import { detectNoteAssetFormat } from "../utils/note-asset-format.js";
6
+ import { MAX_NOTE_ASSET_BYTES, NOTE_PDF_WARNING_BYTES } from "../../shared/notes/constants.js";
7
+ import { maxNoteAssetBytes } from "../../shared/notes/asset-formats.js";
8
+ // Limit disk and memory pressure across imports and background previews.
9
+ const ACTIVE_LIMIT = 3;
10
+ const QUEUE_LIMIT = 30;
11
+ // Bound active operations so stalled imports cannot occupy queue slots indefinitely.
12
+ const OPERATION_TIMEOUT_MS = 120000;
13
+ // A small prefix is sufficient for supported image and PDF signatures.
14
+ const FORMAT_PREFIX_BYTES = 8192;
15
+ // Explain the preview limit to the client in megabytes.
16
+ const PREVIEW_LIMIT_MESSAGE = `Image exceeds ${MAX_NOTE_ASSET_BYTES / 1_000_000} MB`;
17
+ // Yields Buffer chunks and fails once the total passes the limit.
18
+ async function* boundedChunks(source, limit, counter, signal, message) {
19
+ for await (const value of source) {
20
+ signal.throwIfAborted();
21
+ const chunk = Buffer.isBuffer(value) ? value : Buffer.from(value);
22
+ counter.bytes += chunk.length;
23
+ if (counter.bytes > limit)
24
+ throw new NoteAssetLimitError(message);
25
+ yield chunk;
26
+ }
27
+ }
28
+ function isSuccess(response) {
29
+ return (!!response.statusCode &&
30
+ response.statusCode >= 200 &&
31
+ response.statusCode < 300);
32
+ }
33
+ export class NotesExternalAssetsService {
34
+ queue = new PQueue({ concurrency: ACTIVE_LIMIT });
35
+ request;
36
+ constructor(request = remoteAssetRequest) {
37
+ this.request = request;
38
+ }
39
+ run(signal, priority, work) {
40
+ signal.throwIfAborted();
41
+ if (this.queue.pending >= ACTIVE_LIMIT && this.queue.size >= QUEUE_LIMIT) {
42
+ throw new NoteAssetQueueError();
43
+ }
44
+ // The queue removes a task that waits when its signal aborts. A running
45
+ // task stops through the signal that it receives.
46
+ return this.queue.add(async () => {
47
+ try {
48
+ return await work(AbortSignal.any([signal, AbortSignal.timeout(OPERATION_TIMEOUT_MS)]));
49
+ }
50
+ catch (error) {
51
+ throw translateRemoteAssetError(error);
52
+ }
53
+ }, { priority: priority ? 1 : 0, signal });
54
+ }
55
+ async inspect(url, signal) {
56
+ return this.run(signal, false, async (abort) => {
57
+ const response = await this.request(url, 'HEAD', abort);
58
+ try {
59
+ return {
60
+ size: isSuccess(response) ? this.length(response) : null
61
+ };
62
+ }
63
+ finally {
64
+ response.destroy();
65
+ }
66
+ });
67
+ }
68
+ length(response) {
69
+ const value = response.headers['content-length'];
70
+ if (!value || !/^\d+$/.test(value))
71
+ return null;
72
+ const size = Number(value);
73
+ return Number.isSafeInteger(size) ? size : null;
74
+ }
75
+ async importAsset(input, signal, save) {
76
+ return this.run(signal, true, async (abort) => {
77
+ const response = await this.request(input.url, 'GET', abort);
78
+ try {
79
+ this.checkResponse(response);
80
+ const fileLimit = maxNoteAssetBytes(input.kind);
81
+ const warningLimit = input.kind === 'pdf' && !input.allowLargePdf
82
+ ? NOTE_PDF_WARNING_BYTES
83
+ : fileLimit;
84
+ const limit = Math.min(input.remainingBytes, fileLimit, warningLimit);
85
+ const over = (size) => {
86
+ if (size <= limit)
87
+ return undefined;
88
+ if (size > input.remainingBytes) {
89
+ return { status: 'rejected', reason: 'operation-limit' };
90
+ }
91
+ if (size > fileLimit) {
92
+ return { status: 'rejected', reason: 'file-too-large' };
93
+ }
94
+ return {
95
+ status: 'confirmation-required',
96
+ size: this.length(response)
97
+ };
98
+ };
99
+ const known = this.length(response);
100
+ if (known !== null) {
101
+ const result = over(known);
102
+ if (result)
103
+ return result;
104
+ }
105
+ const counter = { bytes: 0 };
106
+ const iterator = boundedChunks(response, limit, counter, abort, 'Attachment download limit reached');
107
+ const prefix = [];
108
+ let prefixBytes = 0;
109
+ try {
110
+ while (prefixBytes < FORMAT_PREFIX_BYTES) {
111
+ const next = await iterator.next();
112
+ if (next.done)
113
+ break;
114
+ prefix.push(next.value);
115
+ prefixBytes += next.value.length;
116
+ }
117
+ const format = await detectNoteAssetFormat(Buffer.concat(prefix));
118
+ if ((format.display === 'link' ? 'pdf' : 'image') !== input.kind) {
119
+ throw new BadRequestError('Attachment type does not match the selected resource');
120
+ }
121
+ const body = async function* () {
122
+ yield* prefix;
123
+ yield* iterator;
124
+ };
125
+ const url = await save(body(), format.extension, abort);
126
+ return {
127
+ status: 'stored',
128
+ url,
129
+ bytes: counter.bytes,
130
+ format: format.extension
131
+ };
132
+ }
133
+ catch (error) {
134
+ if (error instanceof NoteAssetLimitError)
135
+ return over(counter.bytes);
136
+ throw error;
137
+ }
138
+ }
139
+ finally {
140
+ response.destroy();
141
+ }
142
+ });
143
+ }
144
+ async preview(url, signal) {
145
+ return this.run(signal, false, async (abort) => {
146
+ const response = await this.request(url, 'GET', abort);
147
+ try {
148
+ this.checkResponse(response);
149
+ if ((this.length(response) ?? 0) > MAX_NOTE_ASSET_BYTES) {
150
+ throw new NoteAssetLimitError(PREVIEW_LIMIT_MESSAGE);
151
+ }
152
+ const chunks = [];
153
+ for await (const chunk of boundedChunks(response, MAX_NOTE_ASSET_BYTES, { bytes: 0 }, abort, PREVIEW_LIMIT_MESSAGE)) {
154
+ chunks.push(chunk);
155
+ }
156
+ const body = Buffer.concat(chunks);
157
+ const format = await detectNoteAssetFormat(body);
158
+ if (format.display !== 'image') {
159
+ throw new BadRequestError('Preview is not an image');
160
+ }
161
+ return { body, mime: format.mime };
162
+ }
163
+ finally {
164
+ response.destroy();
165
+ }
166
+ });
167
+ }
168
+ checkResponse(response) {
169
+ if (!isSuccess(response)) {
170
+ throw new BadRequestError('Attachment server returned HTTP ' + response.statusCode);
171
+ }
172
+ if (response.headers['content-encoding'] &&
173
+ response.headers['content-encoding'] !== 'identity') {
174
+ throw new BadRequestError('Compressed attachment responses are not supported');
175
+ }
176
+ }
177
+ }