pi-cloud 2.0.0 → 2.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (271) hide show
  1. package/.env.example +29 -0
  2. package/CHANGELOG.md +22 -0
  3. package/README.md +1 -1
  4. package/README.zh-CN.md +1 -1
  5. package/bin/pi-cloud.mjs +1 -1
  6. package/client/dist/.vite/manifest.json +303 -303
  7. package/client/dist/assets/{EditorPanel-qAJ9i_Hs.css → EditorPanel-5KsKffCI.css} +1 -1
  8. package/client/dist/assets/{EditorPanel-C4j_LnbI.js → EditorPanel-jZgIBqB6.js} +189 -189
  9. package/client/dist/assets/{GitHistoryView-i__uu43I.js → GitHistoryView-fz83ZRs_.js} +1 -1
  10. package/client/dist/assets/{MemoryCenter-DOvz1FIy.js → MemoryCenter-Di8khKRv.js} +1 -1
  11. package/client/dist/assets/{PhCaretLeft.vue-C3HxQ-jr.js → PhCaretLeft.vue-C_mgOR0C.js} +1 -1
  12. package/client/dist/assets/SettingsDialog-BsTvb8AX.js +2 -0
  13. package/client/dist/assets/SettingsDialog-JpBlJzlR.css +1 -0
  14. package/client/dist/assets/{TaskQueuePanel-DPnDhZFb.js → TaskQueuePanel-Bbk8CKW5.js} +1 -1
  15. package/client/dist/assets/{TerminalPanel-BQhGwCE5.js → TerminalPanel-CWNxgT84.js} +1 -1
  16. package/client/dist/assets/{abnfDiagram-VCTEODGH-DyGBNBbN.js → abnfDiagram-VCTEODGH-o53BjVqy.js} +1 -1
  17. package/client/dist/assets/{arc-B_21hOEg.js → arc-DsZjnKYA.js} +1 -1
  18. package/client/dist/assets/{architectureDiagram-5GKGNRK7-CjyAJ-2z.js → architectureDiagram-5GKGNRK7-u5Bv1Ym6.js} +1 -1
  19. package/client/dist/assets/{blockDiagram-NRAW4CY4-CmrEp6Fe.js → blockDiagram-NRAW4CY4-BSDeSfLw.js} +1 -1
  20. package/client/dist/assets/{c4Diagram-UCG6FXSJ-DfT2xU1s.js → c4Diagram-UCG6FXSJ-B6NfOWtd.js} +1 -1
  21. package/client/dist/assets/channel-vm9b-dk7.js +1 -0
  22. package/client/dist/assets/{chunk-2Q5K7J3B-BVEJoy95.js → chunk-2Q5K7J3B-CgX2R8z-.js} +1 -1
  23. package/client/dist/assets/{chunk-5VM5RSS4-AYOc2H0F.js → chunk-5VM5RSS4-ha_fObnM.js} +1 -1
  24. package/client/dist/assets/{chunk-F27PBJKO-CAdIcM4C.js → chunk-F27PBJKO-DI1DZ-wc.js} +1 -1
  25. package/client/dist/assets/{chunk-G27WJ6UU-Bk4Qjg6Y.js → chunk-G27WJ6UU-Dg1UIr6p.js} +1 -1
  26. package/client/dist/assets/{chunk-JWPE2WC7-u-wi2HbJ.js → chunk-JWPE2WC7-B3XeLm0J.js} +1 -1
  27. package/client/dist/assets/{chunk-LCL6LL3I-BgicKCMi.js → chunk-LCL6LL3I-qz7b1c_I.js} +1 -1
  28. package/client/dist/assets/{chunk-POPQ4Y6H-DhJ3_oti.js → chunk-POPQ4Y6H-B5NSvr4G.js} +1 -1
  29. package/client/dist/assets/{chunk-SVP7TREG-B-FKnWYy.js → chunk-SVP7TREG-7QwKLsdb.js} +1 -1
  30. package/client/dist/assets/{chunk-XXDRQBXY-Dnsv7iwr.js → chunk-XXDRQBXY-CAosos5-.js} +1 -1
  31. package/client/dist/assets/classDiagram-DTDB5LWJ-BZ5qRTrF.js +1 -0
  32. package/client/dist/assets/classDiagram-v2-JRS7N3AN-BZ5qRTrF.js +1 -0
  33. package/client/dist/assets/{cose-bilkent-JH36ORCC-DL2lIpOf.js → cose-bilkent-JH36ORCC-BsDQpeGC.js} +1 -1
  34. package/client/dist/assets/{cssMode-BcZYukVu.js → cssMode-Bixb-eY-.js} +1 -1
  35. package/client/dist/assets/{cynefin-OW5HDTMX-tZ1QaCp9.js → cynefin-OW5HDTMX-Drahq6GQ.js} +1 -1
  36. package/client/dist/assets/{cynefinDiagram-5FMLGOSQ-GLzVU_pJ.js → cynefinDiagram-5FMLGOSQ-CnkS0oyb.js} +1 -1
  37. package/client/dist/assets/{dagre-3AP2YEHR-B8V1RvVr.js → dagre-3AP2YEHR-BKQcTp5_.js} +1 -1
  38. package/client/dist/assets/{diagram-S7CK7UJ4-MTofUt2F.js → diagram-S7CK7UJ4-CRJVo1Nm.js} +1 -1
  39. package/client/dist/assets/{diagram-UQ7AKVKN-Dw6Onbe3.js → diagram-UQ7AKVKN-DsP-WSSg.js} +1 -1
  40. package/client/dist/assets/{diagram-VSXAHHWV-C4WupsI1.js → diagram-VSXAHHWV-Ccd1iqfn.js} +1 -1
  41. package/client/dist/assets/{diagram-VX7I27RA-CF78AvRx.js → diagram-VX7I27RA-D5w0ryS3.js} +1 -1
  42. package/client/dist/assets/{diagram-Z3DM3KII-xT7zx4S2.js → diagram-Z3DM3KII-DpUTugp8.js} +1 -1
  43. package/client/dist/assets/{ebnfDiagram-PWID7BFC-zkR-_zOG.js → ebnfDiagram-PWID7BFC-Donm234O.js} +1 -1
  44. package/client/dist/assets/{erDiagram-SSCWMZ5O-BzU9E1j8.js → erDiagram-SSCWMZ5O-CvtobvjZ.js} +1 -1
  45. package/client/dist/assets/{flowDiagram-A5DVABFB-BM-160SB.js → flowDiagram-A5DVABFB-v8UIYgnu.js} +1 -1
  46. package/client/dist/assets/{freemarker2-B4PXq-2i.js → freemarker2-CCBoOScX.js} +1 -1
  47. package/client/dist/assets/{ganttDiagram-EL5Y4UJY-LVhMiHMo.js → ganttDiagram-EL5Y4UJY-TCK2YgJA.js} +1 -1
  48. package/client/dist/assets/{gitGraphDiagram-WWUBYQGX-CYGEpNO-.js → gitGraphDiagram-WWUBYQGX-BTVfzqSz.js} +1 -1
  49. package/client/dist/assets/{handlebars-DleyW4LO.js → handlebars-D_iO3oKV.js} +1 -1
  50. package/client/dist/assets/{html-BJ95-ybC.js → html-BxskKvwE.js} +1 -1
  51. package/client/dist/assets/{htmlMode-Cqq8N0XF.js → htmlMode-BRqzPJmn.js} +1 -1
  52. package/client/dist/assets/index-2q9oNVFR.js +407 -0
  53. package/client/dist/assets/{index-PSBDlhaw.js → index-B4PeEJ0f.js} +1 -1
  54. package/client/dist/assets/{index-D1Z3F2UU.css → index-DBGNeJhr.css} +1 -1
  55. package/client/dist/assets/{infoDiagram-RXCK75RN-Ctmoz63S.js → infoDiagram-RXCK75RN-BkzEFABK.js} +1 -1
  56. package/client/dist/assets/{ishikawaDiagram-5VMMS53U-DiWG9VHU.js → ishikawaDiagram-5VMMS53U-DuVtEdHP.js} +1 -1
  57. package/client/dist/assets/{javascript-BIGigzp1.js → javascript-DLM6N2IL.js} +1 -1
  58. package/client/dist/assets/{journeyDiagram-EYS64GPL-CImdI3K_.js → journeyDiagram-EYS64GPL-Cx5cuIcS.js} +1 -1
  59. package/client/dist/assets/{jsonMode-l6_K9bFf.js → jsonMode-BZHZxAtU.js} +1 -1
  60. package/client/dist/assets/{kanban-definition-3QL26DDD-CSSranPL.js → kanban-definition-3QL26DDD-wXihmZ_e.js} +1 -1
  61. package/client/dist/assets/{layout-CE5PddK3.js → layout-C2KDikn-.js} +1 -1
  62. package/client/dist/assets/{linear-CkzMiZL-.js → linear-C6ueyh3n.js} +1 -1
  63. package/client/dist/assets/{liquid-C8hVjUZv.js → liquid-CK9kFoUD.js} +1 -1
  64. package/client/dist/assets/{lspLanguageFeatures-ZfiCKJ_J.js → lspLanguageFeatures-C5jWIiYu.js} +1 -1
  65. package/client/dist/assets/{mdx-DWmYYep4.js → mdx-DJw_6jAn.js} +1 -1
  66. package/client/dist/assets/{mermaid.core-BlcsAN4s.js → mermaid.core-CcwuCXLB.js} +5 -5
  67. package/client/dist/assets/{mindmap-definition-FBJOCRG2-D01B1gH8.js → mindmap-definition-FBJOCRG2-JL1NZYBY.js} +1 -1
  68. package/client/dist/assets/{pegDiagram-XKGWAZYB-DhdSTBt5.js → pegDiagram-XKGWAZYB-DS69_m-N.js} +1 -1
  69. package/client/dist/assets/{pieDiagram-E7YTZNPT-hVSzmo7b.js → pieDiagram-E7YTZNPT-DkXpPONo.js} +1 -1
  70. package/client/dist/assets/{python-AbIuqkub.js → python-Z3BmknC5.js} +1 -1
  71. package/client/dist/assets/{quadrantDiagram-AXDQQJYC-CVexQ_Dn.js → quadrantDiagram-AXDQQJYC-DkkBvxWI.js} +1 -1
  72. package/client/dist/assets/{railroadDiagram-O6MQD6OU-BPCMbhAx.js → railroadDiagram-O6MQD6OU-DCC05goQ.js} +1 -1
  73. package/client/dist/assets/{razor-PLr1HLk0.js → razor-CQA_N38O.js} +1 -1
  74. package/client/dist/assets/{requirementDiagram-EFPCY7ZU-EntABUqc.js → requirementDiagram-EFPCY7ZU-BwD-Jp5O.js} +1 -1
  75. package/client/dist/assets/{sankeyDiagram-P5KCCOFB-D68oih_M.js → sankeyDiagram-P5KCCOFB-GlrDZlAs.js} +1 -1
  76. package/client/dist/assets/{sequenceDiagram-WJ2MYXX4-DWzaT5vI.js → sequenceDiagram-WJ2MYXX4-u5NKUnHb.js} +1 -1
  77. package/client/dist/assets/{sizeCapture-X5ZJPWSS-HbYBaSbU.js → sizeCapture-X5ZJPWSS-DWoXbO58.js} +1 -1
  78. package/client/dist/assets/{stateDiagram-HBIQ2CUA-BteRSnYL.js → stateDiagram-HBIQ2CUA-Bm1dFyez.js} +1 -1
  79. package/client/dist/assets/stateDiagram-v2-4QOOHH4V-DjQoyKBb.js +1 -0
  80. package/client/dist/assets/{swimlanes-XN3QIQJK-BAvVt5mV.js → swimlanes-XN3QIQJK-DUNKblS5.js} +1 -1
  81. package/client/dist/assets/swimlanesDiagram-VK2B7HYN-7MB5gr5J.js +8 -0
  82. package/client/dist/assets/{timeline-definition-24CTP7MA-D6yKUiYl.js → timeline-definition-24CTP7MA-toWxO6Z2.js} +1 -1
  83. package/client/dist/assets/{tsMode-DrakxHLR.js → tsMode-DZAgDpaL.js} +1 -1
  84. package/client/dist/assets/{typescript-DcMOCMAD.js → typescript-BGesFGk_.js} +1 -1
  85. package/client/dist/assets/{useTerminal-Dhk5K03J.js → useTerminal-BcxidJuH.js} +1 -1
  86. package/client/dist/assets/{vennDiagram-4TSXK5OY-CGU61Pyq.js → vennDiagram-4TSXK5OY-BkCayCPN.js} +1 -1
  87. package/client/dist/assets/{wardleyDiagram-VM6X3IG4-BrTe4Lxv.js → wardleyDiagram-VM6X3IG4-BqKWO2eU.js} +1 -1
  88. package/client/dist/assets/{workers-CqVatS52.js → workers-CPOEug1v.js} +1 -1
  89. package/client/dist/assets/{xml-xUSiNagm.js → xml-BLpTRmDm.js} +1 -1
  90. package/client/dist/assets/{xychartDiagram-S5SC5T6Z-CnMmHAmD.js → xychartDiagram-S5SC5T6Z-BIAgMyo_.js} +1 -1
  91. package/client/dist/assets/{yaml-CSavWraE.js → yaml-PqYnl7Hj.js} +1 -1
  92. package/client/dist/index.html +2 -2
  93. package/client/dist/pdfjs/cmaps/78-EUC-H.bcmap +0 -0
  94. package/client/dist/pdfjs/cmaps/78-EUC-V.bcmap +0 -0
  95. package/client/dist/pdfjs/cmaps/78-H.bcmap +0 -0
  96. package/client/dist/pdfjs/cmaps/78-RKSJ-H.bcmap +0 -0
  97. package/client/dist/pdfjs/cmaps/78-RKSJ-V.bcmap +0 -0
  98. package/client/dist/pdfjs/cmaps/78-V.bcmap +0 -0
  99. package/client/dist/pdfjs/cmaps/78ms-RKSJ-H.bcmap +0 -0
  100. package/client/dist/pdfjs/cmaps/78ms-RKSJ-V.bcmap +0 -0
  101. package/client/dist/pdfjs/cmaps/83pv-RKSJ-H.bcmap +0 -0
  102. package/client/dist/pdfjs/cmaps/90ms-RKSJ-H.bcmap +0 -0
  103. package/client/dist/pdfjs/cmaps/90ms-RKSJ-V.bcmap +0 -0
  104. package/client/dist/pdfjs/cmaps/90msp-RKSJ-H.bcmap +0 -0
  105. package/client/dist/pdfjs/cmaps/90msp-RKSJ-V.bcmap +0 -0
  106. package/client/dist/pdfjs/cmaps/90pv-RKSJ-H.bcmap +0 -0
  107. package/client/dist/pdfjs/cmaps/90pv-RKSJ-V.bcmap +0 -0
  108. package/client/dist/pdfjs/cmaps/Add-H.bcmap +0 -0
  109. package/client/dist/pdfjs/cmaps/Add-RKSJ-H.bcmap +0 -0
  110. package/client/dist/pdfjs/cmaps/Add-RKSJ-V.bcmap +0 -0
  111. package/client/dist/pdfjs/cmaps/Add-V.bcmap +0 -0
  112. package/client/dist/pdfjs/cmaps/Adobe-CNS1-0.bcmap +0 -0
  113. package/client/dist/pdfjs/cmaps/Adobe-CNS1-1.bcmap +0 -0
  114. package/client/dist/pdfjs/cmaps/Adobe-CNS1-2.bcmap +0 -0
  115. package/client/dist/pdfjs/cmaps/Adobe-CNS1-3.bcmap +0 -0
  116. package/client/dist/pdfjs/cmaps/Adobe-CNS1-4.bcmap +0 -0
  117. package/client/dist/pdfjs/cmaps/Adobe-CNS1-5.bcmap +0 -0
  118. package/client/dist/pdfjs/cmaps/Adobe-CNS1-6.bcmap +0 -0
  119. package/client/dist/pdfjs/cmaps/Adobe-CNS1-UCS2.bcmap +0 -0
  120. package/client/dist/pdfjs/cmaps/Adobe-GB1-0.bcmap +0 -0
  121. package/client/dist/pdfjs/cmaps/Adobe-GB1-1.bcmap +0 -0
  122. package/client/dist/pdfjs/cmaps/Adobe-GB1-2.bcmap +0 -0
  123. package/client/dist/pdfjs/cmaps/Adobe-GB1-3.bcmap +0 -0
  124. package/client/dist/pdfjs/cmaps/Adobe-GB1-4.bcmap +0 -0
  125. package/client/dist/pdfjs/cmaps/Adobe-GB1-5.bcmap +0 -0
  126. package/client/dist/pdfjs/cmaps/Adobe-GB1-UCS2.bcmap +0 -0
  127. package/client/dist/pdfjs/cmaps/Adobe-Japan1-0.bcmap +0 -0
  128. package/client/dist/pdfjs/cmaps/Adobe-Japan1-1.bcmap +0 -0
  129. package/client/dist/pdfjs/cmaps/Adobe-Japan1-2.bcmap +0 -0
  130. package/client/dist/pdfjs/cmaps/Adobe-Japan1-3.bcmap +0 -0
  131. package/client/dist/pdfjs/cmaps/Adobe-Japan1-4.bcmap +0 -0
  132. package/client/dist/pdfjs/cmaps/Adobe-Japan1-5.bcmap +0 -0
  133. package/client/dist/pdfjs/cmaps/Adobe-Japan1-6.bcmap +0 -0
  134. package/client/dist/pdfjs/cmaps/Adobe-Japan1-UCS2.bcmap +0 -0
  135. package/client/dist/pdfjs/cmaps/Adobe-Korea1-0.bcmap +0 -0
  136. package/client/dist/pdfjs/cmaps/Adobe-Korea1-1.bcmap +0 -0
  137. package/client/dist/pdfjs/cmaps/Adobe-Korea1-2.bcmap +0 -0
  138. package/client/dist/pdfjs/cmaps/Adobe-Korea1-UCS2.bcmap +0 -0
  139. package/client/dist/pdfjs/cmaps/B5-H.bcmap +0 -0
  140. package/client/dist/pdfjs/cmaps/B5-V.bcmap +0 -0
  141. package/client/dist/pdfjs/cmaps/B5pc-H.bcmap +0 -0
  142. package/client/dist/pdfjs/cmaps/B5pc-V.bcmap +0 -0
  143. package/client/dist/pdfjs/cmaps/CNS-EUC-H.bcmap +0 -0
  144. package/client/dist/pdfjs/cmaps/CNS-EUC-V.bcmap +0 -0
  145. package/client/dist/pdfjs/cmaps/CNS1-H.bcmap +0 -0
  146. package/client/dist/pdfjs/cmaps/CNS1-V.bcmap +0 -0
  147. package/client/dist/pdfjs/cmaps/CNS2-H.bcmap +0 -0
  148. package/client/dist/pdfjs/cmaps/CNS2-V.bcmap +3 -0
  149. package/client/dist/pdfjs/cmaps/ETHK-B5-H.bcmap +0 -0
  150. package/client/dist/pdfjs/cmaps/ETHK-B5-V.bcmap +0 -0
  151. package/client/dist/pdfjs/cmaps/ETen-B5-H.bcmap +0 -0
  152. package/client/dist/pdfjs/cmaps/ETen-B5-V.bcmap +0 -0
  153. package/client/dist/pdfjs/cmaps/ETenms-B5-H.bcmap +3 -0
  154. package/client/dist/pdfjs/cmaps/ETenms-B5-V.bcmap +0 -0
  155. package/client/dist/pdfjs/cmaps/EUC-H.bcmap +0 -0
  156. package/client/dist/pdfjs/cmaps/EUC-V.bcmap +0 -0
  157. package/client/dist/pdfjs/cmaps/Ext-H.bcmap +0 -0
  158. package/client/dist/pdfjs/cmaps/Ext-RKSJ-H.bcmap +0 -0
  159. package/client/dist/pdfjs/cmaps/Ext-RKSJ-V.bcmap +0 -0
  160. package/client/dist/pdfjs/cmaps/Ext-V.bcmap +0 -0
  161. package/client/dist/pdfjs/cmaps/GB-EUC-H.bcmap +0 -0
  162. package/client/dist/pdfjs/cmaps/GB-EUC-V.bcmap +0 -0
  163. package/client/dist/pdfjs/cmaps/GB-H.bcmap +4 -0
  164. package/client/dist/pdfjs/cmaps/GB-V.bcmap +0 -0
  165. package/client/dist/pdfjs/cmaps/GBK-EUC-H.bcmap +0 -0
  166. package/client/dist/pdfjs/cmaps/GBK-EUC-V.bcmap +0 -0
  167. package/client/dist/pdfjs/cmaps/GBK2K-H.bcmap +0 -0
  168. package/client/dist/pdfjs/cmaps/GBK2K-V.bcmap +0 -0
  169. package/client/dist/pdfjs/cmaps/GBKp-EUC-H.bcmap +0 -0
  170. package/client/dist/pdfjs/cmaps/GBKp-EUC-V.bcmap +0 -0
  171. package/client/dist/pdfjs/cmaps/GBT-EUC-H.bcmap +0 -0
  172. package/client/dist/pdfjs/cmaps/GBT-EUC-V.bcmap +0 -0
  173. package/client/dist/pdfjs/cmaps/GBT-H.bcmap +0 -0
  174. package/client/dist/pdfjs/cmaps/GBT-V.bcmap +0 -0
  175. package/client/dist/pdfjs/cmaps/GBTpc-EUC-H.bcmap +0 -0
  176. package/client/dist/pdfjs/cmaps/GBTpc-EUC-V.bcmap +0 -0
  177. package/client/dist/pdfjs/cmaps/GBpc-EUC-H.bcmap +0 -0
  178. package/client/dist/pdfjs/cmaps/GBpc-EUC-V.bcmap +0 -0
  179. package/client/dist/pdfjs/cmaps/H.bcmap +0 -0
  180. package/client/dist/pdfjs/cmaps/HKdla-B5-H.bcmap +0 -0
  181. package/client/dist/pdfjs/cmaps/HKdla-B5-V.bcmap +0 -0
  182. package/client/dist/pdfjs/cmaps/HKdlb-B5-H.bcmap +0 -0
  183. package/client/dist/pdfjs/cmaps/HKdlb-B5-V.bcmap +0 -0
  184. package/client/dist/pdfjs/cmaps/HKgccs-B5-H.bcmap +0 -0
  185. package/client/dist/pdfjs/cmaps/HKgccs-B5-V.bcmap +0 -0
  186. package/client/dist/pdfjs/cmaps/HKm314-B5-H.bcmap +0 -0
  187. package/client/dist/pdfjs/cmaps/HKm314-B5-V.bcmap +0 -0
  188. package/client/dist/pdfjs/cmaps/HKm471-B5-H.bcmap +0 -0
  189. package/client/dist/pdfjs/cmaps/HKm471-B5-V.bcmap +0 -0
  190. package/client/dist/pdfjs/cmaps/HKscs-B5-H.bcmap +0 -0
  191. package/client/dist/pdfjs/cmaps/HKscs-B5-V.bcmap +0 -0
  192. package/client/dist/pdfjs/cmaps/Hankaku.bcmap +0 -0
  193. package/client/dist/pdfjs/cmaps/Hiragana.bcmap +0 -0
  194. package/client/dist/pdfjs/cmaps/KSC-EUC-H.bcmap +0 -0
  195. package/client/dist/pdfjs/cmaps/KSC-EUC-V.bcmap +0 -0
  196. package/client/dist/pdfjs/cmaps/KSC-H.bcmap +0 -0
  197. package/client/dist/pdfjs/cmaps/KSC-Johab-H.bcmap +0 -0
  198. package/client/dist/pdfjs/cmaps/KSC-Johab-V.bcmap +0 -0
  199. package/client/dist/pdfjs/cmaps/KSC-V.bcmap +0 -0
  200. package/client/dist/pdfjs/cmaps/KSCms-UHC-H.bcmap +0 -0
  201. package/client/dist/pdfjs/cmaps/KSCms-UHC-HW-H.bcmap +0 -0
  202. package/client/dist/pdfjs/cmaps/KSCms-UHC-HW-V.bcmap +0 -0
  203. package/client/dist/pdfjs/cmaps/KSCms-UHC-V.bcmap +0 -0
  204. package/client/dist/pdfjs/cmaps/KSCpc-EUC-H.bcmap +0 -0
  205. package/client/dist/pdfjs/cmaps/KSCpc-EUC-V.bcmap +0 -0
  206. package/client/dist/pdfjs/cmaps/Katakana.bcmap +0 -0
  207. package/client/dist/pdfjs/cmaps/LICENSE +36 -0
  208. package/client/dist/pdfjs/cmaps/NWP-H.bcmap +0 -0
  209. package/client/dist/pdfjs/cmaps/NWP-V.bcmap +0 -0
  210. package/client/dist/pdfjs/cmaps/RKSJ-H.bcmap +0 -0
  211. package/client/dist/pdfjs/cmaps/RKSJ-V.bcmap +0 -0
  212. package/client/dist/pdfjs/cmaps/Roman.bcmap +0 -0
  213. package/client/dist/pdfjs/cmaps/UniCNS-UCS2-H.bcmap +0 -0
  214. package/client/dist/pdfjs/cmaps/UniCNS-UCS2-V.bcmap +0 -0
  215. package/client/dist/pdfjs/cmaps/UniCNS-UTF16-H.bcmap +0 -0
  216. package/client/dist/pdfjs/cmaps/UniCNS-UTF16-V.bcmap +0 -0
  217. package/client/dist/pdfjs/cmaps/UniCNS-UTF32-H.bcmap +0 -0
  218. package/client/dist/pdfjs/cmaps/UniCNS-UTF32-V.bcmap +0 -0
  219. package/client/dist/pdfjs/cmaps/UniCNS-UTF8-H.bcmap +0 -0
  220. package/client/dist/pdfjs/cmaps/UniCNS-UTF8-V.bcmap +0 -0
  221. package/client/dist/pdfjs/cmaps/UniGB-UCS2-H.bcmap +0 -0
  222. package/client/dist/pdfjs/cmaps/UniGB-UCS2-V.bcmap +0 -0
  223. package/client/dist/pdfjs/cmaps/UniGB-UTF16-H.bcmap +0 -0
  224. package/client/dist/pdfjs/cmaps/UniGB-UTF16-V.bcmap +0 -0
  225. package/client/dist/pdfjs/cmaps/UniGB-UTF32-H.bcmap +0 -0
  226. package/client/dist/pdfjs/cmaps/UniGB-UTF32-V.bcmap +0 -0
  227. package/client/dist/pdfjs/cmaps/UniGB-UTF8-H.bcmap +0 -0
  228. package/client/dist/pdfjs/cmaps/UniGB-UTF8-V.bcmap +0 -0
  229. package/client/dist/pdfjs/cmaps/UniJIS-UCS2-H.bcmap +0 -0
  230. package/client/dist/pdfjs/cmaps/UniJIS-UCS2-HW-H.bcmap +0 -0
  231. package/client/dist/pdfjs/cmaps/UniJIS-UCS2-HW-V.bcmap +0 -0
  232. package/client/dist/pdfjs/cmaps/UniJIS-UCS2-V.bcmap +0 -0
  233. package/client/dist/pdfjs/cmaps/UniJIS-UTF16-H.bcmap +0 -0
  234. package/client/dist/pdfjs/cmaps/UniJIS-UTF16-V.bcmap +0 -0
  235. package/client/dist/pdfjs/cmaps/UniJIS-UTF32-H.bcmap +0 -0
  236. package/client/dist/pdfjs/cmaps/UniJIS-UTF32-V.bcmap +0 -0
  237. package/client/dist/pdfjs/cmaps/UniJIS-UTF8-H.bcmap +0 -0
  238. package/client/dist/pdfjs/cmaps/UniJIS-UTF8-V.bcmap +0 -0
  239. package/client/dist/pdfjs/cmaps/UniJIS2004-UTF16-H.bcmap +0 -0
  240. package/client/dist/pdfjs/cmaps/UniJIS2004-UTF16-V.bcmap +0 -0
  241. package/client/dist/pdfjs/cmaps/UniJIS2004-UTF32-H.bcmap +0 -0
  242. package/client/dist/pdfjs/cmaps/UniJIS2004-UTF32-V.bcmap +0 -0
  243. package/client/dist/pdfjs/cmaps/UniJIS2004-UTF8-H.bcmap +0 -0
  244. package/client/dist/pdfjs/cmaps/UniJIS2004-UTF8-V.bcmap +0 -0
  245. package/client/dist/pdfjs/cmaps/UniJISPro-UCS2-HW-V.bcmap +0 -0
  246. package/client/dist/pdfjs/cmaps/UniJISPro-UCS2-V.bcmap +0 -0
  247. package/client/dist/pdfjs/cmaps/UniJISPro-UTF8-V.bcmap +0 -0
  248. package/client/dist/pdfjs/cmaps/UniJISX0213-UTF32-H.bcmap +0 -0
  249. package/client/dist/pdfjs/cmaps/UniJISX0213-UTF32-V.bcmap +0 -0
  250. package/client/dist/pdfjs/cmaps/UniJISX02132004-UTF32-H.bcmap +0 -0
  251. package/client/dist/pdfjs/cmaps/UniJISX02132004-UTF32-V.bcmap +0 -0
  252. package/client/dist/pdfjs/cmaps/UniKS-UCS2-H.bcmap +0 -0
  253. package/client/dist/pdfjs/cmaps/UniKS-UCS2-V.bcmap +0 -0
  254. package/client/dist/pdfjs/cmaps/UniKS-UTF16-H.bcmap +0 -0
  255. package/client/dist/pdfjs/cmaps/UniKS-UTF16-V.bcmap +0 -0
  256. package/client/dist/pdfjs/cmaps/UniKS-UTF32-H.bcmap +0 -0
  257. package/client/dist/pdfjs/cmaps/UniKS-UTF32-V.bcmap +0 -0
  258. package/client/dist/pdfjs/cmaps/UniKS-UTF8-H.bcmap +0 -0
  259. package/client/dist/pdfjs/cmaps/UniKS-UTF8-V.bcmap +0 -0
  260. package/client/dist/pdfjs/cmaps/V.bcmap +0 -0
  261. package/client/dist/pdfjs/cmaps/WP-Symbol.bcmap +0 -0
  262. package/package.json +1 -1
  263. package/server/dist/index.js +1232 -75
  264. package/client/dist/assets/SettingsDialog-BTyTnZYM.css +0 -1
  265. package/client/dist/assets/SettingsDialog-VlpollL1.js +0 -2
  266. package/client/dist/assets/channel-CROBmx9n.js +0 -1
  267. package/client/dist/assets/classDiagram-DTDB5LWJ-D2KSupgp.js +0 -1
  268. package/client/dist/assets/classDiagram-v2-JRS7N3AN-D2KSupgp.js +0 -1
  269. package/client/dist/assets/index-C1R3qRwd.js +0 -407
  270. package/client/dist/assets/stateDiagram-v2-4QOOHH4V-CN8Njcij.js +0 -1
  271. package/client/dist/assets/swimlanesDiagram-VK2B7HYN-p55moRoN.js +0 -8
@@ -1,5 +1,5 @@
1
1
  // src/index.ts
2
- import { randomBytes as randomBytes3 } from "node:crypto";
2
+ import { randomBytes as randomBytes4 } from "node:crypto";
3
3
  import { mkdirSync as mkdirSync2, readFileSync as readFileSync4, writeFileSync } from "node:fs";
4
4
  import { spawn as spawn3 } from "node:child_process";
5
5
  import * as os9 from "node:os";
@@ -53,8 +53,8 @@ function configuredAllowedRoots() {
53
53
  return raw.split(",").map((root) => root.trim()).filter(Boolean);
54
54
  }
55
55
  function isInsideRoot(candidate, root) {
56
- const relative3 = path2.relative(root, candidate);
57
- return relative3 === "" || !relative3.startsWith("..") && !path2.isAbsolute(relative3);
56
+ const relative4 = path2.relative(root, candidate);
57
+ return relative4 === "" || !relative4.startsWith("..") && !path2.isAbsolute(relative4);
58
58
  }
59
59
  async function realpathIfExists(candidate) {
60
60
  try {
@@ -101,6 +101,23 @@ async function resolveAllowedPath(requestedPath) {
101
101
  }
102
102
  return resolvedPath;
103
103
  }
104
+ async function resolveAllowedExistingPath(requestedPath) {
105
+ const resolvedPath = path2.resolve(expandHomePath(requestedPath));
106
+ if (isPathCheckDisabled()) return fs.realpath(resolvedPath);
107
+ const configuredRoots = configuredAllowedRoots().map((root) => path2.resolve(expandHomePath(root)));
108
+ const matchedRoot = configuredRoots.find((root) => isInsideRoot(resolvedPath, root));
109
+ if (!matchedRoot) throw new PathAccessError(resolvedPath);
110
+ const relativePath = path2.relative(matchedRoot, resolvedPath);
111
+ if (relativePath.startsWith("..") || path2.isAbsolute(relativePath)) {
112
+ throw new PathAccessError(resolvedPath);
113
+ }
114
+ const realPath = await fs.realpath(resolvedPath);
115
+ const roots = await allowedRoots();
116
+ if (!roots.some((root) => isInsideRoot(realPath, root))) {
117
+ throw new PathAccessError(resolvedPath);
118
+ }
119
+ return realPath;
120
+ }
104
121
 
105
122
  // src/services/project-mover.ts
106
123
  var ProjectMover = class {
@@ -1762,11 +1779,11 @@ function sendError(reply, error) {
1762
1779
  // src/routes/files.ts
1763
1780
  import { spawn } from "child_process";
1764
1781
  import { isUtf8 } from "buffer";
1765
- import { createReadStream as createReadStream2 } from "fs";
1782
+ import { constants as fsConstants, createReadStream as createReadStream2 } from "fs";
1766
1783
  import * as fs2 from "fs/promises";
1767
1784
  import * as path3 from "path";
1768
1785
  import { ZipArchive } from "archiver";
1769
- import { glob } from "glob";
1786
+ import { globIterate } from "glob";
1770
1787
 
1771
1788
  // src/utils/archive-preview.ts
1772
1789
  import { createReadStream } from "node:fs";
@@ -1882,6 +1899,13 @@ function listTarEntries(filePath, gzipped) {
1882
1899
  }
1883
1900
 
1884
1901
  // src/routes/files.ts
1902
+ var MAX_FILE_TREE_DEPTH = 10;
1903
+ var MAX_FILE_TREE_NODES = 5e3;
1904
+ var FILE_TREE_TIMEOUT_MS = 5e3;
1905
+ var MAX_TEXT_FILE_BYTES = 10 * 1024 * 1024;
1906
+ var MAX_FILE_SEARCH_RESULTS = 5e3;
1907
+ var FILE_SEARCH_TIMEOUT_MS = 5e3;
1908
+ var FILE_SEARCH_IGNORES = ["**/.git/**", "**/node_modules/**"];
1885
1909
  var imageMimeTypes = /* @__PURE__ */ new Map([
1886
1910
  [".apng", "image/apng"],
1887
1911
  [".avif", "image/avif"],
@@ -1995,7 +2019,7 @@ async function createTreeNode(dirPath, entry) {
1995
2019
  const rawTarget = await fs2.readlink(fullPath);
1996
2020
  linkTarget = path3.isAbsolute(rawTarget) ? rawTarget : path3.resolve(dirPath, rawTarget);
1997
2021
  try {
1998
- const targetStats = await fs2.stat(fullPath);
2022
+ const targetStats = await fs2.stat(await resolveAllowedExistingPath(fullPath));
1999
2023
  if (targetStats.isDirectory()) {
2000
2024
  isDirectory = true;
2001
2025
  targetType = "directory";
@@ -2020,29 +2044,41 @@ async function createTreeNode(dirPath, entry) {
2020
2044
  }
2021
2045
  return node;
2022
2046
  }
2023
- async function buildFileTree(dirPath, depth, currentDepth = 0, options = {
2024
- includeHidden: false,
2025
- excludeNames: /* @__PURE__ */ new Set(["node_modules"]),
2026
- filterType: "all",
2027
- sort: "name"
2028
- }) {
2047
+ async function buildFileTree(dirPath, depth, currentDepth, options, budget) {
2029
2048
  if (currentDepth >= depth) return [];
2030
- const entries = await fs2.readdir(dirPath, { withFileTypes: true });
2031
2049
  const nodes = [];
2032
- for (const entry of entries) {
2033
- if (!options.includeHidden && entry.name.startsWith(".")) continue;
2034
- if (options.excludeNames.has(entry.name)) continue;
2035
- const node = await createTreeNode(dirPath, entry);
2036
- if (options.sort === "modified") {
2037
- node.mtime = (await fs2.lstat(node.path)).mtimeMs;
2038
- }
2039
- if (options.filterType !== "all" && options.filterType !== node.type) {
2040
- continue;
2041
- }
2042
- if (node.type === "directory" && !node.isSymlink) {
2043
- node.children = await buildFileTree(node.path, depth, currentDepth + 1, options);
2050
+ const accessDirPath = await resolveAllowedExistingPath(dirPath);
2051
+ const directory = await fs2.opendir(accessDirPath);
2052
+ try {
2053
+ let entry = await directory.read();
2054
+ while (entry !== null) {
2055
+ if (budget.remainingNodes <= 0 || Date.now() >= budget.deadline) {
2056
+ budget.truncated = true;
2057
+ break;
2058
+ }
2059
+ if (!options.includeHidden && entry.name.startsWith(".")) {
2060
+ entry = await directory.read();
2061
+ continue;
2062
+ }
2063
+ if (options.excludeNames.has(entry.name)) {
2064
+ entry = await directory.read();
2065
+ continue;
2066
+ }
2067
+ const node = await createTreeNode(dirPath, entry);
2068
+ if (options.sort === "modified") {
2069
+ node.mtime = (await fs2.lstat(path3.join(accessDirPath, entry.name))).mtimeMs;
2070
+ }
2071
+ if (options.filterType === "all" || options.filterType === node.type) {
2072
+ budget.remainingNodes--;
2073
+ if (node.type === "directory" && !node.isSymlink) {
2074
+ node.children = await buildFileTree(node.path, depth, currentDepth + 1, options, budget);
2075
+ }
2076
+ nodes.push(node);
2077
+ }
2078
+ entry = await directory.read();
2044
2079
  }
2045
- nodes.push(node);
2080
+ } finally {
2081
+ await directory.close();
2046
2082
  }
2047
2083
  return nodes.sort((a, b) => {
2048
2084
  if (options.sort === "modified") {
@@ -2053,7 +2089,7 @@ async function buildFileTree(dirPath, depth, currentDepth = 0, options = {
2053
2089
  });
2054
2090
  }
2055
2091
  async function fileRoutes(app) {
2056
- app.get("/tree", async (req) => {
2092
+ app.get("/tree", async (req, reply) => {
2057
2093
  const {
2058
2094
  path: dirPath,
2059
2095
  depth,
@@ -2062,21 +2098,31 @@ async function fileRoutes(app) {
2062
2098
  exclude,
2063
2099
  sort
2064
2100
  } = req.query;
2101
+ const requestedDepth = depth === void 0 ? 3 : Number(depth);
2102
+ if (!Number.isInteger(requestedDepth) || requestedDepth < 1 || requestedDepth > MAX_FILE_TREE_DEPTH) {
2103
+ return reply.code(400).send({ error: `depth must be an integer between 1 and ${MAX_FILE_TREE_DEPTH}` });
2104
+ }
2065
2105
  const resolvedPath = await resolveAllowedPath(dirPath);
2066
2106
  const excludeNames = new Set(
2067
2107
  (exclude === void 0 ? "node_modules" : exclude).split(",").map((name) => name.trim()).filter(Boolean)
2068
2108
  );
2069
- const tree = await buildFileTree(resolvedPath, parseInt(depth || "3"), 0, {
2109
+ const budget = {
2110
+ remainingNodes: MAX_FILE_TREE_NODES,
2111
+ deadline: Date.now() + FILE_TREE_TIMEOUT_MS,
2112
+ truncated: false
2113
+ };
2114
+ const tree = await buildFileTree(resolvedPath, requestedDepth, 0, {
2070
2115
  includeHidden: parseBoolean(hidden, false),
2071
2116
  excludeNames,
2072
2117
  filterType: parseFilterType(type),
2073
2118
  sort: parseTreeSort(sort)
2074
- });
2119
+ }, budget);
2075
2120
  const parentPath = path3.dirname(resolvedPath);
2076
2121
  return {
2077
2122
  path: resolvedPath,
2078
2123
  parentPath: parentPath === resolvedPath ? null : parentPath,
2079
- tree
2124
+ tree,
2125
+ truncated: budget.truncated
2080
2126
  };
2081
2127
  });
2082
2128
  app.get("/download", async (req, reply) => {
@@ -2119,9 +2165,11 @@ async function fileRoutes(app) {
2119
2165
  app.get("/read", async (req, reply) => {
2120
2166
  const { path: filePath } = req.query;
2121
2167
  const resolvedPath = await resolveAllowedPath(filePath);
2168
+ let accessPath;
2122
2169
  let stats;
2123
2170
  try {
2124
- stats = await fs2.stat(resolvedPath);
2171
+ accessPath = await resolveAllowedExistingPath(resolvedPath);
2172
+ stats = await fs2.stat(accessPath);
2125
2173
  } catch (error) {
2126
2174
  if (error.code === "ENOENT") {
2127
2175
  return reply.code(404).send({ error: "File not found", path: resolvedPath });
@@ -2133,7 +2181,7 @@ async function fileRoutes(app) {
2133
2181
  try {
2134
2182
  return {
2135
2183
  path: resolvedPath,
2136
- content: await archivePreview(resolvedPath),
2184
+ content: await archivePreview(accessPath),
2137
2185
  kind: "archive",
2138
2186
  mtime: stats.mtimeMs
2139
2187
  };
@@ -2165,24 +2213,52 @@ async function fileRoutes(app) {
2165
2213
  mtime: stats.mtimeMs
2166
2214
  });
2167
2215
  }
2168
- let content;
2216
+ let handle;
2169
2217
  try {
2170
- content = await fs2.readFile(resolvedPath);
2218
+ handle = await fs2.open(accessPath, fsConstants.O_RDONLY | (fsConstants.O_NOFOLLOW ?? 0));
2219
+ const openedStats = await handle.stat();
2220
+ if (openedStats.size > MAX_TEXT_FILE_BYTES) {
2221
+ return reply.code(413).send({
2222
+ error: "File is too large to open as text",
2223
+ maxBytes: MAX_TEXT_FILE_BYTES,
2224
+ path: resolvedPath
2225
+ });
2226
+ }
2227
+ const chunks = [];
2228
+ let totalBytes = 0;
2229
+ while (true) {
2230
+ const remainingBytes = MAX_TEXT_FILE_BYTES + 1 - totalBytes;
2231
+ const buffer = Buffer.allocUnsafe(Math.min(64 * 1024, remainingBytes));
2232
+ const { bytesRead } = await handle.read(buffer, 0, buffer.byteLength, null);
2233
+ if (bytesRead === 0) break;
2234
+ totalBytes += bytesRead;
2235
+ if (totalBytes > MAX_TEXT_FILE_BYTES) {
2236
+ return reply.code(413).send({
2237
+ error: "File is too large to open as text",
2238
+ maxBytes: MAX_TEXT_FILE_BYTES,
2239
+ path: resolvedPath
2240
+ });
2241
+ }
2242
+ chunks.push(buffer.subarray(0, bytesRead));
2243
+ }
2244
+ const content = Buffer.concat(chunks, totalBytes);
2245
+ if (isBinaryFile(resolvedPath, content)) {
2246
+ return reply.code(415).send({
2247
+ error: "Unsupported file type",
2248
+ kind: "binary",
2249
+ path: resolvedPath,
2250
+ mtime: openedStats.mtimeMs
2251
+ });
2252
+ }
2253
+ return { path: resolvedPath, content: content.toString("utf-8"), mtime: openedStats.mtimeMs };
2171
2254
  } catch (error) {
2172
2255
  if (error.code === "ENOENT") {
2173
2256
  return reply.code(404).send({ error: "File not found", path: resolvedPath });
2174
2257
  }
2175
2258
  throw error;
2259
+ } finally {
2260
+ await handle?.close();
2176
2261
  }
2177
- if (isBinaryFile(resolvedPath, content)) {
2178
- return reply.code(415).send({
2179
- error: "Unsupported file type",
2180
- kind: "binary",
2181
- path: resolvedPath,
2182
- mtime: stats.mtimeMs
2183
- });
2184
- }
2185
- return { path: resolvedPath, content: content.toString("utf-8"), mtime: stats.mtimeMs };
2186
2262
  });
2187
2263
  app.get("/preview-asset", async (req, reply) => {
2188
2264
  const { root, path: relativePath } = req.query;
@@ -2223,14 +2299,22 @@ async function fileRoutes(app) {
2223
2299
  if (!imageMimeType && !isPdf) {
2224
2300
  return reply.code(415).send({ error: "Unsupported file type" });
2225
2301
  }
2226
- if (isPdf) {
2227
- return reply.type("application/pdf").send(createReadStream2(resolvedPath));
2228
- }
2229
- const content = await fs2.readFile(resolvedPath);
2230
- if (imageMimeType === "image/svg+xml") {
2231
- reply.header("Content-Security-Policy", "sandbox; default-src 'none'");
2302
+ let handle;
2303
+ try {
2304
+ const accessPath = await resolveAllowedExistingPath(resolvedPath);
2305
+ handle = await fs2.open(accessPath, fsConstants.O_RDONLY | (fsConstants.O_NOFOLLOW ?? 0));
2306
+ const stream = handle.createReadStream();
2307
+ handle = void 0;
2308
+ if (isPdf) {
2309
+ return reply.type("application/pdf").send(stream);
2310
+ }
2311
+ if (imageMimeType === "image/svg+xml") {
2312
+ reply.header("Content-Security-Policy", "sandbox; default-src 'none'");
2313
+ }
2314
+ return reply.type(imageMimeType).send(stream);
2315
+ } finally {
2316
+ await handle?.close();
2232
2317
  }
2233
- return reply.type(imageMimeType).send(content);
2234
2318
  });
2235
2319
  app.post("/write", async (req) => {
2236
2320
  const { path: filePath, content } = req.body;
@@ -2384,11 +2468,45 @@ async function fileRoutes(app) {
2384
2468
  });
2385
2469
  return { success: true };
2386
2470
  });
2387
- app.get("/search", async (req) => {
2471
+ app.get("/search", async (req, reply) => {
2388
2472
  const { pattern, path: searchPath } = req.query;
2473
+ if (!pattern?.trim()) {
2474
+ return reply.code(400).send({ error: "Search pattern is required" });
2475
+ }
2476
+ const normalizedPattern = pattern.replace(/\\/g, "/");
2477
+ const patternSegments = normalizedPattern.split("/");
2478
+ const unsafeMagic = ["{", "}", "[", "]"].some((character) => normalizedPattern.includes(character)) || /[?*+@!]\(/.test(normalizedPattern);
2479
+ const unsafeDotSegment = patternSegments.some((segment) => segment === ".." || segment.includes(".") && /^[.*?]+$/.test(segment));
2480
+ if (path3.isAbsolute(pattern) || unsafeMagic || unsafeDotSegment) {
2481
+ return reply.code(400).send({ error: "Search pattern must stay within the requested path" });
2482
+ }
2389
2483
  const resolvedSearchPath = await resolveAllowedPath(searchPath || ".");
2390
- const files = await glob(pattern, { cwd: resolvedSearchPath });
2391
- return { files };
2484
+ const files = [];
2485
+ const signal = AbortSignal.timeout(FILE_SEARCH_TIMEOUT_MS);
2486
+ try {
2487
+ for await (const file of globIterate(normalizedPattern, {
2488
+ cwd: resolvedSearchPath,
2489
+ root: resolvedSearchPath,
2490
+ ignore: FILE_SEARCH_IGNORES,
2491
+ nodir: true,
2492
+ signal
2493
+ })) {
2494
+ const relativePath = path3.relative(resolvedSearchPath, path3.resolve(resolvedSearchPath, file));
2495
+ if (relativePath.startsWith("..") || path3.isAbsolute(relativePath)) {
2496
+ return reply.code(400).send({ error: "Search pattern must stay within the requested path" });
2497
+ }
2498
+ if (files.length === MAX_FILE_SEARCH_RESULTS) {
2499
+ return { files, truncated: true };
2500
+ }
2501
+ files.push(file);
2502
+ }
2503
+ } catch (error) {
2504
+ if (signal.aborted) {
2505
+ return reply.code(408).send({ error: "File search timed out" });
2506
+ }
2507
+ throw error;
2508
+ }
2509
+ return { files, truncated: false };
2392
2510
  });
2393
2511
  }
2394
2512
 
@@ -2674,6 +2792,205 @@ async function changelogRoutes(app) {
2674
2792
  });
2675
2793
  }
2676
2794
 
2795
+ // src/services/speech-to-text.ts
2796
+ var DEFAULT_BASE_URL = "https://api.openai.com/v1";
2797
+ var DEFAULT_MODEL = "gpt-4o-mini-transcribe";
2798
+ var MAX_TRANSCRIPTION_ATTEMPTS = 3;
2799
+ var RETRY_DELAY_MS = 500;
2800
+ var MAX_AUDIO_BYTES = 25 * 1024 * 1024;
2801
+ function audioExtension(contentType) {
2802
+ if (contentType.includes("amr")) return "amr";
2803
+ if (contentType.includes("mp4")) return "m4a";
2804
+ if (contentType.includes("mpeg")) return "mp3";
2805
+ if (contentType.includes("ogg")) return "ogg";
2806
+ if (contentType.includes("wav")) return "wav";
2807
+ return "webm";
2808
+ }
2809
+ function isRetryableStatus(status) {
2810
+ return status === 429 || status >= 500;
2811
+ }
2812
+ function wait(ms) {
2813
+ return new Promise((resolve10) => setTimeout(resolve10, ms));
2814
+ }
2815
+ function isSpeechToTextConfigured() {
2816
+ return Boolean(process.env.PI_CLOUD_STT_API_KEY?.trim() || process.env.OPENAI_API_KEY?.trim());
2817
+ }
2818
+ async function transcribeAudio(audio, contentType, options = {}) {
2819
+ const apiKey = process.env.PI_CLOUD_STT_API_KEY?.trim() || process.env.OPENAI_API_KEY?.trim();
2820
+ if (!apiKey) throw new Error("Speech-to-text is not configured.");
2821
+ const baseUrl = (process.env.PI_CLOUD_STT_BASE_URL?.trim() || DEFAULT_BASE_URL).replace(/\/+$/, "");
2822
+ const model = process.env.PI_CLOUD_STT_MODEL?.trim() || DEFAULT_MODEL;
2823
+ const language = process.env.PI_CLOUD_STT_LANGUAGE?.trim();
2824
+ const fetchImpl = options.fetch ?? fetch;
2825
+ const retryDelayMs = options.retryDelayMs ?? RETRY_DELAY_MS;
2826
+ const form = new FormData();
2827
+ form.append("file", new Blob([audio], { type: contentType }), `recording.${audioExtension(contentType)}`);
2828
+ form.append("model", model);
2829
+ if (language) form.append("language", language);
2830
+ for (let attempt = 1; attempt <= MAX_TRANSCRIPTION_ATTEMPTS; attempt += 1) {
2831
+ let response;
2832
+ try {
2833
+ response = await fetchImpl(`${baseUrl}/audio/transcriptions`, {
2834
+ method: "POST",
2835
+ headers: { Authorization: `Bearer ${apiKey}` },
2836
+ body: form
2837
+ });
2838
+ } catch (error) {
2839
+ if (attempt === MAX_TRANSCRIPTION_ATTEMPTS) {
2840
+ throw new Error("Unable to reach the speech-to-text provider.", { cause: error });
2841
+ }
2842
+ await wait(retryDelayMs * attempt);
2843
+ continue;
2844
+ }
2845
+ const result = await response.json().catch(() => ({}));
2846
+ if (!response.ok) {
2847
+ if (isRetryableStatus(response.status) && attempt < MAX_TRANSCRIPTION_ATTEMPTS) {
2848
+ await wait(retryDelayMs * attempt);
2849
+ continue;
2850
+ }
2851
+ throw new Error(typeof result.error?.message === "string" ? result.error.message : "Transcription failed.");
2852
+ }
2853
+ if (typeof result.text !== "string") throw new Error("The speech-to-text provider returned an invalid response.");
2854
+ return result.text;
2855
+ }
2856
+ throw new Error("Unable to reach the speech-to-text provider.");
2857
+ }
2858
+
2859
+ // src/services/text-to-speech.ts
2860
+ var DEFAULT_MODEL2 = "mlx-community/Kokoro-82M-bf16";
2861
+ var DEFAULT_VOICE = "af_heart";
2862
+ var DEFAULT_FORMAT = "wav";
2863
+ var MAX_SYNTHESIS_ATTEMPTS = 3;
2864
+ var RETRY_DELAY_MS2 = 500;
2865
+ var MAX_TTS_TEXT_LENGTH = 1e5;
2866
+ var FORMAT_CONTENT_TYPES = {
2867
+ aac: "audio/aac",
2868
+ flac: "audio/flac",
2869
+ mp3: "audio/mpeg",
2870
+ ogg: "audio/ogg",
2871
+ opus: "audio/ogg",
2872
+ pcm: "audio/L16",
2873
+ wav: "audio/wav"
2874
+ };
2875
+ function isRetryableStatus2(status) {
2876
+ return status === 429 || status >= 500;
2877
+ }
2878
+ function wait2(ms) {
2879
+ return new Promise((resolve10) => setTimeout(resolve10, ms));
2880
+ }
2881
+ function languageCode(language) {
2882
+ if (language === "zh") return "z";
2883
+ if (language === "en") return "a";
2884
+ return language;
2885
+ }
2886
+ async function providerError(response) {
2887
+ const result = await response.json().catch(() => ({}));
2888
+ if (typeof result.error?.message === "string") return result.error.message;
2889
+ if (typeof result.detail === "string") return result.detail;
2890
+ return "Speech synthesis failed.";
2891
+ }
2892
+ function isTextToSpeechConfigured() {
2893
+ return Boolean(process.env.PI_CLOUD_TTS_BASE_URL?.trim());
2894
+ }
2895
+ async function synthesizeSpeech(text, options = {}) {
2896
+ const baseUrl = process.env.PI_CLOUD_TTS_BASE_URL?.trim().replace(/\/+$/, "");
2897
+ if (!baseUrl) throw new Error("Text-to-speech is not configured.");
2898
+ const model = process.env.PI_CLOUD_TTS_MODEL?.trim() || DEFAULT_MODEL2;
2899
+ const voice = process.env.PI_CLOUD_TTS_VOICE?.trim() || DEFAULT_VOICE;
2900
+ const language = process.env.PI_CLOUD_TTS_LANGUAGE?.trim();
2901
+ const format = process.env.PI_CLOUD_TTS_FORMAT?.trim().toLowerCase() || DEFAULT_FORMAT;
2902
+ const apiKey = process.env.PI_CLOUD_TTS_API_KEY?.trim();
2903
+ const fetchImpl = options.fetch ?? fetch;
2904
+ const retryDelayMs = options.retryDelayMs ?? RETRY_DELAY_MS2;
2905
+ const body = {
2906
+ model,
2907
+ input: text,
2908
+ voice,
2909
+ response_format: format,
2910
+ ...language ? { language } : {},
2911
+ ...language ? { lang_code: languageCode(language) } : {}
2912
+ };
2913
+ for (let attempt = 1; attempt <= MAX_SYNTHESIS_ATTEMPTS; attempt += 1) {
2914
+ let response;
2915
+ try {
2916
+ response = await fetchImpl(`${baseUrl}/audio/speech`, {
2917
+ method: "POST",
2918
+ headers: {
2919
+ "Content-Type": "application/json",
2920
+ ...apiKey ? { Authorization: `Bearer ${apiKey}` } : {}
2921
+ },
2922
+ body: JSON.stringify(body)
2923
+ });
2924
+ } catch (error) {
2925
+ if (attempt === MAX_SYNTHESIS_ATTEMPTS) {
2926
+ throw new Error("Unable to reach the text-to-speech provider.", { cause: error });
2927
+ }
2928
+ await wait2(retryDelayMs * attempt);
2929
+ continue;
2930
+ }
2931
+ if (!response.ok) {
2932
+ const message = await providerError(response);
2933
+ if (isRetryableStatus2(response.status) && attempt < MAX_SYNTHESIS_ATTEMPTS) {
2934
+ await wait2(retryDelayMs * attempt);
2935
+ continue;
2936
+ }
2937
+ throw new Error(message);
2938
+ }
2939
+ const audio = Buffer.from(await response.arrayBuffer());
2940
+ if (audio.length === 0) throw new Error("The text-to-speech provider returned an empty response.");
2941
+ return {
2942
+ audio,
2943
+ contentType: response.headers.get("content-type")?.split(";")[0] || FORMAT_CONTENT_TYPES[format] || "application/octet-stream"
2944
+ };
2945
+ }
2946
+ throw new Error("Unable to reach the text-to-speech provider.");
2947
+ }
2948
+
2949
+ // src/routes/speech.ts
2950
+ async function speechRoutes(app, options = {}) {
2951
+ app.addContentTypeParser(/^audio\//, { parseAs: "buffer", bodyLimit: MAX_AUDIO_BYTES }, (_req, body, done) => {
2952
+ done(null, body);
2953
+ });
2954
+ app.get("/status", async () => ({
2955
+ available: isSpeechToTextConfigured(),
2956
+ ttsAvailable: isTextToSpeechConfigured()
2957
+ }));
2958
+ app.post("/transcribe", async (req, reply) => {
2959
+ if (!isSpeechToTextConfigured()) {
2960
+ return reply.status(503).send({ error: "Speech-to-text is not configured." });
2961
+ }
2962
+ const contentType = req.headers["content-type"]?.split(";")[0]?.trim().toLowerCase() || "";
2963
+ if (!contentType.startsWith("audio/") || !Buffer.isBuffer(req.body) || req.body.length === 0) {
2964
+ return reply.status(400).send({ error: "A non-empty audio request body is required." });
2965
+ }
2966
+ try {
2967
+ return { text: await transcribeAudio(req.body, contentType, options) };
2968
+ } catch (error) {
2969
+ req.log.error({ err: error }, "Speech-to-text request failed");
2970
+ return reply.status(502).send({ error: error instanceof Error ? error.message : "Transcription failed." });
2971
+ }
2972
+ });
2973
+ app.post("/synthesize", async (req, reply) => {
2974
+ if (!isTextToSpeechConfigured()) {
2975
+ return reply.status(503).send({ error: "Text-to-speech is not configured." });
2976
+ }
2977
+ const text = req.body?.text;
2978
+ if (typeof text !== "string" || !text.trim()) {
2979
+ return reply.status(400).send({ error: "Non-empty text is required." });
2980
+ }
2981
+ if (text.length > MAX_TTS_TEXT_LENGTH) {
2982
+ return reply.status(413).send({ error: `Text must not exceed ${MAX_TTS_TEXT_LENGTH} characters.` });
2983
+ }
2984
+ try {
2985
+ const result = await synthesizeSpeech(text, options);
2986
+ return reply.type(result.contentType).send(result.audio);
2987
+ } catch (error) {
2988
+ req.log.error({ err: error }, "Text-to-speech request failed");
2989
+ return reply.status(502).send({ error: error instanceof Error ? error.message : "Speech synthesis failed." });
2990
+ }
2991
+ });
2992
+ }
2993
+
2677
2994
  // src/routes/git.ts
2678
2995
  import { execFile } from "node:child_process";
2679
2996
  import { createHash } from "node:crypto";
@@ -2798,6 +3115,17 @@ async function runGit(cwd, args, maxBuffer3 = MAX_SLASH_COMMAND_OUTPUT_BYTES) {
2798
3115
  throw error;
2799
3116
  }
2800
3117
  }
3118
+ async function runGitNoIndex(cwd, args, maxBuffer3) {
3119
+ try {
3120
+ const { stdout } = await execFileAsync("git", ["diff", "--no-index", ...args], { cwd, maxBuffer: maxBuffer3 });
3121
+ return stdout;
3122
+ } catch (error) {
3123
+ if (isMaxBufferError(error)) throw new OversizedGitOutputError();
3124
+ const result = error;
3125
+ if (result.code === 1) return result.stdout || "";
3126
+ throw error;
3127
+ }
3128
+ }
2801
3129
  async function isGitRepository(cwd) {
2802
3130
  try {
2803
3131
  return (await runGit(cwd, ["rev-parse", "--is-inside-work-tree"])).trim() === "true";
@@ -2880,6 +3208,20 @@ function textFromAssistantMessage(message) {
2880
3208
  function joinGitOutput(...parts) {
2881
3209
  return parts.map((part) => part.replace(/^(?:\r?\n)+|(?:\r?\n)+$/g, "")).filter((part) => part.trim()).join("\n\n");
2882
3210
  }
3211
+ async function appendUntrackedDiff(cwd, diff, args, path13, maxBytes) {
3212
+ const pathArgs = path13 ? ["--", path13] : [];
3213
+ const untracked = await runGit(cwd, ["ls-files", "--others", "--exclude-standard", "-z", ...pathArgs]);
3214
+ const files = untracked.split("\0").filter(Boolean);
3215
+ let output = diff;
3216
+ for (const file of files) {
3217
+ const separatorBytes = output.trim() ? 2 : 0;
3218
+ const remainingBytes = maxBytes - Buffer.byteLength(output) - separatorBytes;
3219
+ if (remainingBytes <= 0) throw new OversizedGitOutputError();
3220
+ const fileDiff = await runGitNoIndex(cwd, [...args, "--", "/dev/null", file], remainingBytes);
3221
+ output = joinGitOutput(output, fileDiff);
3222
+ }
3223
+ return output;
3224
+ }
2883
3225
  async function getCombinedDiff(cwd, args, maxBytes = MAX_SLASH_COMMAND_OUTPUT_BYTES) {
2884
3226
  const unstaged = await runGit(cwd, ["diff", ...args], maxBytes);
2885
3227
  const remainingBytes = maxBytes - Buffer.byteLength(unstaged);
@@ -3382,22 +3724,28 @@ async function gitRoutes(app, options = {}) {
3382
3724
  }
3383
3725
  });
3384
3726
  app.get("/diff", async (req, reply) => {
3385
- const { cwd, scope: rawScope, commit: rawCommit, path: path13 } = req.query;
3727
+ const { cwd, scope: rawScope, commit: rawCommit, path: path13, includeUntracked } = req.query;
3386
3728
  const resolvedCwd = await resolveGitCwd(cwd);
3387
3729
  try {
3388
3730
  const commit = rawCommit ? parseCommit(rawCommit) : void 0;
3389
3731
  if (commit) {
3390
3732
  const diff2 = await runGit(resolvedCwd, ["show", "--format=", "--patch", commit]);
3391
3733
  const remainingBytes2 = MAX_SLASH_COMMAND_OUTPUT_BYTES - Buffer.byteLength(diff2);
3392
- const stat9 = remainingBytes2 > 0 ? await runGit(resolvedCwd, ["show", "--format=", "--stat", commit], remainingBytes2) : "";
3393
- return { cwd: resolvedCwd, scope: `commit-${commit}`, commit, stat: stat9, diff: diff2 };
3734
+ const stat10 = remainingBytes2 > 0 ? await runGit(resolvedCwd, ["show", "--format=", "--stat", commit], remainingBytes2) : "";
3735
+ return { cwd: resolvedCwd, scope: `commit-${commit}`, commit, stat: stat10, diff: diff2 };
3394
3736
  }
3395
3737
  const scope = parseDiffScope(rawScope);
3396
3738
  const pathArgs = path13 ? ["--", path13] : [];
3397
- const diff = await getDiff(resolvedCwd, pathArgs, scope);
3739
+ let diff = await getDiff(resolvedCwd, pathArgs, scope);
3740
+ if (includeUntracked === "true" && scope !== "staged") {
3741
+ diff = await appendUntrackedDiff(resolvedCwd, diff, [], path13, MAX_SLASH_COMMAND_OUTPUT_BYTES);
3742
+ }
3398
3743
  const remainingBytes = MAX_SLASH_COMMAND_OUTPUT_BYTES - Buffer.byteLength(diff);
3399
- const stat8 = remainingBytes > 0 ? await getDiff(resolvedCwd, ["--stat", ...pathArgs], scope, remainingBytes) : "";
3400
- return { cwd: resolvedCwd, scope, path: path13, stat: stat8, diff };
3744
+ let stat9 = remainingBytes > 0 ? await getDiff(resolvedCwd, ["--stat", ...pathArgs], scope, remainingBytes) : "";
3745
+ if (includeUntracked === "true" && scope !== "staged" && remainingBytes > 0) {
3746
+ stat9 = await appendUntrackedDiff(resolvedCwd, stat9, ["--stat"], path13, remainingBytes);
3747
+ }
3748
+ return { cwd: resolvedCwd, scope, path: path13, stat: stat9, diff };
3401
3749
  } catch (error) {
3402
3750
  if (error instanceof OversizedGitOutputError) {
3403
3751
  return {
@@ -3914,6 +4262,65 @@ function sendGatewayError(reply, error) {
3914
4262
  return reply.status(400).send({ error: message });
3915
4263
  }
3916
4264
 
4265
+ // src/routes/wecom-gateway.ts
4266
+ async function wecomGatewayRoutes(app, options) {
4267
+ app.addContentTypeParser(["application/xml", "text/xml"], { parseAs: "string" }, (_req, body, done) => done(null, body));
4268
+ app.get("/callback", async (req, reply) => {
4269
+ try {
4270
+ const challenge = options.service.handleVerification(req.query);
4271
+ return reply.type("text/plain; charset=utf-8").send(challenge);
4272
+ } catch (error) {
4273
+ return sendWecomError(reply, error);
4274
+ }
4275
+ });
4276
+ app.post("/callback", async (req, reply) => {
4277
+ try {
4278
+ const acknowledgement = options.service.handleCallback(
4279
+ req.query,
4280
+ typeof req.body === "string" ? req.body : ""
4281
+ );
4282
+ return reply.type("text/plain; charset=utf-8").send(acknowledgement);
4283
+ } catch (error) {
4284
+ return sendWecomError(reply, error);
4285
+ }
4286
+ });
4287
+ app.get("/status", async () => ({ status: options.service.status() }));
4288
+ app.put("/configuration", async (req, reply) => {
4289
+ try {
4290
+ return options.service.saveConfiguration(req.body);
4291
+ } catch (error) {
4292
+ return sendWecomError(reply, error);
4293
+ }
4294
+ });
4295
+ app.delete("/configuration", async (_req, reply) => {
4296
+ try {
4297
+ options.service.disconnect();
4298
+ return { status: options.service.status() };
4299
+ } catch (error) {
4300
+ return sendWecomError(reply, error);
4301
+ }
4302
+ });
4303
+ app.post("/callback-secrets", async (_req, reply) => {
4304
+ try {
4305
+ return options.service.regenerateCallbackSecrets();
4306
+ } catch (error) {
4307
+ return sendWecomError(reply, error);
4308
+ }
4309
+ });
4310
+ app.post("/test", async (_req, reply) => {
4311
+ try {
4312
+ return { status: await options.service.testConnection() };
4313
+ } catch (error) {
4314
+ return sendWecomError(reply, error);
4315
+ }
4316
+ });
4317
+ }
4318
+ function sendWecomError(reply, error) {
4319
+ const statusCode = Number(error?.statusCode) || 400;
4320
+ const message = error instanceof Error ? error.message : "WeCom gateway request failed";
4321
+ return reply.status(statusCode).send({ error: message });
4322
+ }
4323
+
3917
4324
  // src/routes/git-hosting.ts
3918
4325
  import { execFile as execFile2 } from "node:child_process";
3919
4326
  import { createHash as createHash3 } from "node:crypto";
@@ -4742,7 +5149,8 @@ async function authRoutes(app, options) {
4742
5149
  autoExtractMemory: getPreferenceValue("memory.autoExtract") === "true",
4743
5150
  theme: getTheme(),
4744
5151
  language: getLanguage(),
4745
- soundNotification: getSoundNotification()
5152
+ soundNotification: getSoundNotification(),
5153
+ autoSpeakAssistant: getPreferenceValue("ui.autoSpeakAssistant") === "true"
4746
5154
  };
4747
5155
  }
4748
5156
  function requireAuth(req, reply) {
@@ -4855,8 +5263,9 @@ async function authRoutes(app, options) {
4855
5263
  const hasTheme = hasPreference(body, "theme");
4856
5264
  const hasLanguage = hasPreference(body, "language");
4857
5265
  const hasSoundNotification = hasPreference(body, "soundNotification");
5266
+ const hasAutoSpeakAssistant = hasPreference(body, "autoSpeakAssistant");
4858
5267
  const hasGitCloneParentPath = hasPreference(body, "gitCloneParentPath");
4859
- if (!hasShowHintInfo && !hasShowCodeBlockLanguageHeaders && !hasStreamingMessageBehavior && !hasEditorAutoRefresh && !hasConfirmSessionDelete && !hasNewSessionShortcut && !hasFullscreenShortcut && !hasShowGoToTopButton && !hasShowChatViewOptionsButton && !hasAutoExtractMemory && !hasTheme && !hasLanguage && !hasSoundNotification && !hasGitCloneParentPath) {
5268
+ if (!hasShowHintInfo && !hasShowCodeBlockLanguageHeaders && !hasStreamingMessageBehavior && !hasEditorAutoRefresh && !hasConfirmSessionDelete && !hasNewSessionShortcut && !hasFullscreenShortcut && !hasShowGoToTopButton && !hasShowChatViewOptionsButton && !hasAutoExtractMemory && !hasTheme && !hasLanguage && !hasSoundNotification && !hasAutoSpeakAssistant && !hasGitCloneParentPath) {
4860
5269
  return reply.status(400).send({ error: "At least one preference must be provided" });
4861
5270
  }
4862
5271
  if (hasShowHintInfo && typeof body.showHintInfo !== "boolean") {
@@ -4898,6 +5307,9 @@ async function authRoutes(app, options) {
4898
5307
  if (hasSoundNotification && body.soundNotification !== "off" && body.soundNotification !== "beep" && body.soundNotification !== "chime" && body.soundNotification !== "ding") {
4899
5308
  return reply.status(400).send({ error: "soundNotification must be off, beep, chime, or ding" });
4900
5309
  }
5310
+ if (hasAutoSpeakAssistant && typeof body.autoSpeakAssistant !== "boolean") {
5311
+ return reply.status(400).send({ error: "autoSpeakAssistant must be a boolean" });
5312
+ }
4901
5313
  if (hasGitCloneParentPath && (typeof body.gitCloneParentPath !== "string" || !body.gitCloneParentPath.trim())) {
4902
5314
  return reply.status(400).send({ error: "gitCloneParentPath must be a non-empty string" });
4903
5315
  }
@@ -4914,6 +5326,7 @@ async function authRoutes(app, options) {
4914
5326
  if (hasTheme) setPreferenceValue("ui.theme", String(body.theme));
4915
5327
  if (hasLanguage) setPreferenceValue("ui.language", String(body.language));
4916
5328
  if (hasSoundNotification) setPreferenceValue("ui.soundNotification", String(body.soundNotification));
5329
+ if (hasAutoSpeakAssistant) setPreferenceValue("ui.autoSpeakAssistant", String(body.autoSpeakAssistant));
4917
5330
  if (hasGitCloneParentPath) setPreferenceValue("ui.gitCloneParentPath", String(body.gitCloneParentPath).trim());
4918
5331
  return getPreferences();
4919
5332
  });
@@ -6081,11 +6494,52 @@ var projectHistorySchemaMigration = {
6081
6494
  }
6082
6495
  };
6083
6496
 
6497
+ // src/db/migrations/004-wecom-gateway-schema.ts
6498
+ var wecomGatewaySchemaMigration = {
6499
+ version: 4,
6500
+ name: "wecom-gateway-schema",
6501
+ up(db) {
6502
+ db.exec(`
6503
+ CREATE TABLE IF NOT EXISTS wecom_gateway_credentials (
6504
+ id INTEGER PRIMARY KEY CHECK (id = 1),
6505
+ corp_id TEXT NOT NULL,
6506
+ corp_secret TEXT NOT NULL,
6507
+ agent_id TEXT NOT NULL,
6508
+ callback_token TEXT NOT NULL,
6509
+ encoding_aes_key TEXT NOT NULL,
6510
+ allowed_users_json TEXT NOT NULL DEFAULT '[]',
6511
+ callback_verified_at TEXT,
6512
+ created_at TEXT NOT NULL,
6513
+ updated_at TEXT NOT NULL
6514
+ );
6515
+
6516
+ CREATE TABLE IF NOT EXISTS wecom_gateway_sessions (
6517
+ session_key TEXT PRIMARY KEY,
6518
+ client_id TEXT NOT NULL,
6519
+ pi_session_id TEXT NOT NULL,
6520
+ created_at TEXT NOT NULL,
6521
+ updated_at TEXT NOT NULL
6522
+ );
6523
+
6524
+ CREATE TABLE IF NOT EXISTS wecom_gateway_configs (
6525
+ client_id TEXT PRIMARY KEY,
6526
+ agent_profile TEXT,
6527
+ default_cwd TEXT,
6528
+ skill_mode TEXT,
6529
+ skill_preset_id TEXT,
6530
+ created_at TEXT NOT NULL,
6531
+ updated_at TEXT NOT NULL
6532
+ );
6533
+ `);
6534
+ }
6535
+ };
6536
+
6084
6537
  // src/db/migrations/index.ts
6085
6538
  var migrations = [
6086
6539
  applicationSchemaMigration,
6087
6540
  gatewaySchemaMigration,
6088
- projectHistorySchemaMigration
6541
+ projectHistorySchemaMigration,
6542
+ wecomGatewaySchemaMigration
6089
6543
  ];
6090
6544
  function runDatabaseMigrations(db) {
6091
6545
  db.exec(`
@@ -11431,7 +11885,7 @@ function createMemoryRuntime(options) {
11431
11885
 
11432
11886
  // src/services/worktree-manager.ts
11433
11887
  import { execFile as execFileCallback } from "child_process";
11434
- import { copyFile, mkdir as mkdir5, readdir as readdir4 } from "fs/promises";
11888
+ import { copyFile, mkdir as mkdir5, readdir as readdir3 } from "fs/promises";
11435
11889
  import { basename as basename5, dirname as dirname8, isAbsolute as isAbsolute3, join as join13 } from "path";
11436
11890
  import { promisify as promisify4 } from "util";
11437
11891
  var defaultExecFile = promisify4(execFileCallback);
@@ -11460,7 +11914,7 @@ var WorktreeManager = class {
11460
11914
  const baseRepoPath = await this.getRepoRoot(projectPath).catch(() => void 0);
11461
11915
  if (!baseRepoPath) return [];
11462
11916
  const files = [];
11463
- const entries = await readdir4(baseRepoPath, { withFileTypes: true });
11917
+ const entries = await readdir3(baseRepoPath, { withFileTypes: true });
11464
11918
  for (const entry of entries) {
11465
11919
  if (!entry.isFile() || !this.isRootFileName(entry.name)) continue;
11466
11920
  if (await this.isIgnored(baseRepoPath, entry.name)) files.push(entry.name);
@@ -12562,9 +13016,9 @@ var FeishuGatewayService = class {
12562
13016
  console.warn("[feishu-gateway] failed to decrypt encrypted callback payload", {
12563
13017
  reason: error instanceof Error ? error.message : "unknown error"
12564
13018
  });
12565
- const gatewayError = new Error("Invalid Feishu encrypted callback payload");
12566
- gatewayError.statusCode = 400;
12567
- throw gatewayError;
13019
+ const gatewayError2 = new Error("Invalid Feishu encrypted callback payload");
13020
+ gatewayError2.statusCode = 400;
13021
+ throw gatewayError2;
12568
13022
  }
12569
13023
  }
12570
13024
  verifyToken(payload, config) {
@@ -14077,6 +14531,704 @@ function extractLastAssistantText2(messages) {
14077
14531
  return void 0;
14078
14532
  }
14079
14533
 
14534
+ // src/services/wecom-gateway.ts
14535
+ import { randomBytes as randomBytes3 } from "node:crypto";
14536
+ import { stat as stat8 } from "node:fs/promises";
14537
+ import { isAbsolute as isAbsolute6 } from "node:path";
14538
+
14539
+ // src/services/wecom-crypto.ts
14540
+ import { createDecipheriv as createDecipheriv3, createHash as createHash8, timingSafeEqual as timingSafeEqual2 } from "node:crypto";
14541
+ function verifyWecomSignature(input) {
14542
+ const expected = createHash8("sha1").update([input.token, input.timestamp, input.nonce, input.encrypted].sort().join("")).digest();
14543
+ if (!/^[0-9a-f]{40}$/i.test(input.signature)) return false;
14544
+ const actual = Buffer.from(input.signature, "hex");
14545
+ return timingSafeEqual2(expected, actual);
14546
+ }
14547
+ function decryptWecomPayload(encrypted, encodingAesKey, expectedReceiveId) {
14548
+ const key = parseEncodingAesKey(encodingAesKey);
14549
+ const ciphertext = Buffer.from(encrypted, "base64");
14550
+ if (!ciphertext.length || ciphertext.length % 16 !== 0) throw new Error("Invalid WeCom encrypted payload");
14551
+ const decipher = createDecipheriv3("aes-256-cbc", key, key.subarray(0, 16));
14552
+ decipher.setAutoPadding(false);
14553
+ const padded = Buffer.concat([decipher.update(ciphertext), decipher.final()]);
14554
+ const plaintext = removePkcs7Padding(padded);
14555
+ if (plaintext.length < 20) throw new Error("Invalid WeCom decrypted payload");
14556
+ const messageLength = plaintext.readUInt32BE(16);
14557
+ const messageStart = 20;
14558
+ const messageEnd = messageStart + messageLength;
14559
+ if (messageEnd > plaintext.length) throw new Error("Invalid WeCom message length");
14560
+ const receiveId = plaintext.subarray(messageEnd).toString("utf8");
14561
+ if (receiveId !== expectedReceiveId) throw new Error("Invalid WeCom callback receive ID");
14562
+ return plaintext.subarray(messageStart, messageEnd).toString("utf8");
14563
+ }
14564
+ function parseEncodingAesKey(value) {
14565
+ if (!/^[A-Za-z0-9]{43}$/.test(value)) throw new Error("WeCom EncodingAESKey must contain 43 letters or digits");
14566
+ const key = Buffer.from(`${value}=`, "base64");
14567
+ if (key.length !== 32) throw new Error("Invalid WeCom EncodingAESKey");
14568
+ return key;
14569
+ }
14570
+ function removePkcs7Padding(value) {
14571
+ const padding = value[value.length - 1];
14572
+ if (!padding || padding > 32 || padding > value.length) throw new Error("Invalid WeCom PKCS#7 padding");
14573
+ for (let index = value.length - padding; index < value.length; index += 1) {
14574
+ if (value[index] !== padding) throw new Error("Invalid WeCom PKCS#7 padding");
14575
+ }
14576
+ return value.subarray(0, value.length - padding);
14577
+ }
14578
+
14579
+ // src/services/wecom-gateway.ts
14580
+ var WECOM_API_BASE_URL = "https://qyapi.weixin.qq.com";
14581
+ var TOKEN_REFRESH_SKEW_MS2 = 5 * 60 * 1e3;
14582
+ var DEDUP_TTL_MS3 = 5 * 60 * 1e3;
14583
+ var TEXT_CHUNK_MAX_BYTES = 1900;
14584
+ function generateEncodingAesKey() {
14585
+ while (true) {
14586
+ const key = randomBytes3(32).toString("base64").slice(0, 43);
14587
+ if (/^[A-Za-z0-9]+$/.test(key)) return key;
14588
+ }
14589
+ }
14590
+ var WecomGatewayService = class {
14591
+ constructor(db, gatewaySettings, sessionService) {
14592
+ this.db = db;
14593
+ this.sessionService = sessionService;
14594
+ this.gatewaySettings = gatewaySettings || new GatewaySettingsStore(db);
14595
+ this.presetStore = new SkillPresetStore(db);
14596
+ }
14597
+ db;
14598
+ sessionService;
14599
+ gatewaySettings;
14600
+ presetStore;
14601
+ seen = /* @__PURE__ */ new Map();
14602
+ queues = /* @__PURE__ */ new Map();
14603
+ tokenCache;
14604
+ environmentCallbackVerified = false;
14605
+ status() {
14606
+ const config = this.loadConfig();
14607
+ const managedBy = this.isEnvironmentManaged() ? "environment" : this.loadSavedCredentials() ? "database" : "none";
14608
+ return {
14609
+ configured: Boolean(config.corpId && config.corpSecret && config.agentId && config.callbackToken && config.encodingAesKey),
14610
+ managedBy,
14611
+ ...config.corpId ? { corpId: config.corpId } : {},
14612
+ ...config.agentId ? { agentId: config.agentId } : {},
14613
+ allowedUsers: config.allowedUsers,
14614
+ callbackVerified: managedBy === "database" ? Boolean(this.loadSavedCredentials()?.callbackVerified) : this.environmentCallbackVerified
14615
+ };
14616
+ }
14617
+ saveConfiguration(input) {
14618
+ if (this.isEnvironmentManaged()) throw new Error("WeCom credentials are managed by environment variables");
14619
+ const corpId = input.corpId.trim();
14620
+ const corpSecret = input.corpSecret.trim();
14621
+ const agentId = input.agentId.trim();
14622
+ if (!corpId || !corpSecret || !/^\d+$/.test(agentId)) throw new Error("Corp ID, app secret, and a numeric Agent ID are required");
14623
+ const allowedUsers = normalizeList(input.allowedUsers);
14624
+ const existing = this.loadSavedCredentials();
14625
+ const callbackToken = existing?.callbackToken || randomBytes3(16).toString("hex");
14626
+ const encodingAesKey = existing?.encodingAesKey || generateEncodingAesKey();
14627
+ const now = (/* @__PURE__ */ new Date()).toISOString();
14628
+ this.db.prepare(`
14629
+ INSERT INTO wecom_gateway_credentials (
14630
+ id, corp_id, corp_secret, agent_id, callback_token, encoding_aes_key,
14631
+ allowed_users_json, callback_verified_at, created_at, updated_at
14632
+ ) VALUES (1, ?, ?, ?, ?, ?, ?, NULL, ?, ?)
14633
+ ON CONFLICT(id) DO UPDATE SET
14634
+ corp_id = excluded.corp_id,
14635
+ corp_secret = excluded.corp_secret,
14636
+ agent_id = excluded.agent_id,
14637
+ callback_token = excluded.callback_token,
14638
+ encoding_aes_key = excluded.encoding_aes_key,
14639
+ allowed_users_json = excluded.allowed_users_json,
14640
+ callback_verified_at = NULL,
14641
+ updated_at = excluded.updated_at
14642
+ `).run(corpId, corpSecret, agentId, callbackToken, encodingAesKey, JSON.stringify(allowedUsers), now, now);
14643
+ this.tokenCache = void 0;
14644
+ return { status: this.status(), callbackToken, encodingAesKey };
14645
+ }
14646
+ regenerateCallbackSecrets() {
14647
+ if (this.isEnvironmentManaged()) throw new Error("WeCom credentials are managed by environment variables");
14648
+ const saved = this.loadSavedCredentials();
14649
+ if (!saved) throw new Error("Save the WeCom configuration first");
14650
+ const callbackToken = randomBytes3(16).toString("hex");
14651
+ const encodingAesKey = generateEncodingAesKey();
14652
+ this.db.prepare(`
14653
+ UPDATE wecom_gateway_credentials
14654
+ SET callback_token = ?, encoding_aes_key = ?, callback_verified_at = NULL, updated_at = ?
14655
+ WHERE id = 1
14656
+ `).run(callbackToken, encodingAesKey, (/* @__PURE__ */ new Date()).toISOString());
14657
+ return { status: this.status(), callbackToken, encodingAesKey };
14658
+ }
14659
+ disconnect() {
14660
+ if (this.isEnvironmentManaged()) throw new Error("WeCom credentials are managed by environment variables");
14661
+ const clients = this.db.prepare("SELECT client_id FROM wecom_gateway_sessions").all();
14662
+ for (const { client_id: clientId } of clients) this.sessionService.disposeSession(clientId);
14663
+ this.db.exec("DELETE FROM wecom_gateway_sessions; DELETE FROM wecom_gateway_configs; DELETE FROM wecom_gateway_credentials;");
14664
+ this.tokenCache = void 0;
14665
+ }
14666
+ async testConnection() {
14667
+ const config = this.requireConfig();
14668
+ this.tokenCache = void 0;
14669
+ await this.getAccessToken(config);
14670
+ return this.status();
14671
+ }
14672
+ handleVerification(query) {
14673
+ const config = this.requireConfig();
14674
+ const encrypted = stringValue3(query.echostr);
14675
+ this.verifySignature(query, encrypted, config);
14676
+ const challenge = decryptWecomPayload(encrypted, config.encodingAesKey, config.corpId);
14677
+ this.markCallbackVerified();
14678
+ return challenge;
14679
+ }
14680
+ handleCallback(query, body) {
14681
+ const config = this.requireConfig();
14682
+ const encrypted = xmlValue(body, "Encrypt");
14683
+ if (!encrypted) throw gatewayError("Invalid WeCom callback envelope", 400);
14684
+ this.verifySignature(query, encrypted, config);
14685
+ const message = parseWecomMessage(decryptWecomPayload(encrypted, config.encodingAesKey, config.corpId));
14686
+ this.markCallbackVerified();
14687
+ if (!message || message.agentId !== config.agentId) return "success";
14688
+ if (config.allowedUsers.length && !config.allowedUsers.includes(message.userId)) return "success";
14689
+ if (this.markDuplicate(message.messageId)) return "success";
14690
+ if (!["text", "image", "voice"].includes(message.messageType)) {
14691
+ void this.sendReply(config, message.userId, "This WeCom gateway supports text, image, and voice messages only.").catch((error) => {
14692
+ console.warn("[wecom-gateway] unsupported-message reply failed:", error instanceof Error ? error.message : error);
14693
+ });
14694
+ return "success";
14695
+ }
14696
+ this.enqueue({
14697
+ messageId: message.messageId,
14698
+ userId: message.userId,
14699
+ text: message.text,
14700
+ messageType: message.messageType,
14701
+ mediaId: message.mediaId,
14702
+ picUrl: message.picUrl
14703
+ }, config);
14704
+ return "success";
14705
+ }
14706
+ verifySignature(query, encrypted, config) {
14707
+ const valid = encrypted && verifyWecomSignature({
14708
+ token: config.callbackToken,
14709
+ timestamp: stringValue3(query.timestamp),
14710
+ nonce: stringValue3(query.nonce),
14711
+ encrypted,
14712
+ signature: stringValue3(query.msg_signature)
14713
+ });
14714
+ if (!valid) throw gatewayError("Invalid WeCom callback signature", 401);
14715
+ }
14716
+ requireConfig() {
14717
+ const config = this.loadConfig();
14718
+ if (!config.corpId || !config.corpSecret || !config.agentId || !config.callbackToken || !config.encodingAesKey) {
14719
+ throw gatewayError("WeCom gateway is not configured", 503);
14720
+ }
14721
+ return config;
14722
+ }
14723
+ loadConfig() {
14724
+ const gatewaySettings = this.gatewaySettings.get();
14725
+ const saved = this.loadSavedCredentials();
14726
+ const skillset = this.resolveSkillsetConfig(gatewaySettings.defaultSkillset);
14727
+ return {
14728
+ corpId: process.env.PI_CLOUD_WECOM_CORP_ID?.trim() || saved?.corpId || "",
14729
+ corpSecret: process.env.PI_CLOUD_WECOM_CORP_SECRET?.trim() || saved?.corpSecret || "",
14730
+ agentId: process.env.PI_CLOUD_WECOM_AGENT_ID?.trim() || saved?.agentId || "",
14731
+ callbackToken: process.env.PI_CLOUD_WECOM_CALLBACK_TOKEN?.trim() || saved?.callbackToken || "",
14732
+ encodingAesKey: process.env.PI_CLOUD_WECOM_ENCODING_AES_KEY?.trim() || saved?.encodingAesKey || "",
14733
+ allowedUsers: process.env.PI_CLOUD_WECOM_ALLOWED_USERS !== void 0 ? parseList2(process.env.PI_CLOUD_WECOM_ALLOWED_USERS) : saved?.allowedUsers || [],
14734
+ defaultCwd: gatewaySettings.cwds[0],
14735
+ cwdChoices: gatewaySettings.cwds,
14736
+ agentProfile: gatewaySettings.defaultProfile,
14737
+ modelProvider: gatewaySettings.defaultModelProvider,
14738
+ modelId: gatewaySettings.defaultModelId,
14739
+ ...skillset
14740
+ };
14741
+ }
14742
+ isEnvironmentManaged() {
14743
+ return [
14744
+ "PI_CLOUD_WECOM_CORP_ID",
14745
+ "PI_CLOUD_WECOM_CORP_SECRET",
14746
+ "PI_CLOUD_WECOM_AGENT_ID",
14747
+ "PI_CLOUD_WECOM_CALLBACK_TOKEN",
14748
+ "PI_CLOUD_WECOM_ENCODING_AES_KEY"
14749
+ ].some((key) => Boolean(process.env[key]?.trim()));
14750
+ }
14751
+ loadSavedCredentials() {
14752
+ const row = this.db.prepare(`
14753
+ SELECT corp_id, corp_secret, agent_id, callback_token, encoding_aes_key,
14754
+ allowed_users_json, callback_verified_at
14755
+ FROM wecom_gateway_credentials WHERE id = 1
14756
+ `).get();
14757
+ if (!row) return null;
14758
+ let allowedUsers = [];
14759
+ try {
14760
+ allowedUsers = normalizeList(JSON.parse(row.allowed_users_json));
14761
+ } catch {
14762
+ }
14763
+ return {
14764
+ corpId: row.corp_id,
14765
+ corpSecret: row.corp_secret,
14766
+ agentId: row.agent_id,
14767
+ callbackToken: row.callback_token,
14768
+ encodingAesKey: row.encoding_aes_key,
14769
+ allowedUsers,
14770
+ callbackVerified: Boolean(row.callback_verified_at)
14771
+ };
14772
+ }
14773
+ markCallbackVerified() {
14774
+ if (this.isEnvironmentManaged()) {
14775
+ this.environmentCallbackVerified = true;
14776
+ return;
14777
+ }
14778
+ this.db.prepare("UPDATE wecom_gateway_credentials SET callback_verified_at = ?, updated_at = ? WHERE id = 1").run((/* @__PURE__ */ new Date()).toISOString(), (/* @__PURE__ */ new Date()).toISOString());
14779
+ }
14780
+ markDuplicate(id) {
14781
+ const now = Date.now();
14782
+ for (const [key, expiresAt] of this.seen) {
14783
+ if (expiresAt <= now) this.seen.delete(key);
14784
+ }
14785
+ if (this.seen.has(id)) return true;
14786
+ this.seen.set(id, now + DEDUP_TTL_MS3);
14787
+ return false;
14788
+ }
14789
+ enqueue(message, config) {
14790
+ const clientId = `wecom:dm:${message.userId}`;
14791
+ const previous = this.queues.get(clientId) || Promise.resolve();
14792
+ const next = previous.catch(() => void 0).then(() => this.processMessage(clientId, message, config)).catch((error) => console.error("[wecom-gateway] message handling failed:", error)).finally(() => {
14793
+ if (this.queues.get(clientId) === next) this.queues.delete(clientId);
14794
+ });
14795
+ this.queues.set(clientId, next);
14796
+ }
14797
+ async processMessage(clientId, message, config) {
14798
+ const effectiveConfig = this.resolveChatConfig(clientId, config);
14799
+ if (await this.handleCommand(clientId, message, config, effectiveConfig)) return;
14800
+ if (!effectiveConfig.defaultCwd) {
14801
+ await this.sendReply(config, message.userId, "No gateway folders configured. Add at least one allowed folder in Web UI Settings > Gateway.");
14802
+ return;
14803
+ }
14804
+ let promptText = message.text;
14805
+ if (message.messageType === "voice") {
14806
+ if (!message.mediaId) {
14807
+ await this.sendReply(config, message.userId, "The WeCom voice message did not include downloadable media. Try sending it again.");
14808
+ return;
14809
+ }
14810
+ try {
14811
+ promptText = (await transcribeAudio(await this.downloadVoice(config, message.mediaId), "audio/amr")).trim();
14812
+ } catch (error) {
14813
+ console.warn("[wecom-gateway] failed to transcribe inbound voice:", error instanceof Error ? error.message : error);
14814
+ await this.sendReply(config, message.userId, "The voice message could not be transcribed. Check the speech-to-text configuration and try again.");
14815
+ return;
14816
+ }
14817
+ if (!promptText) {
14818
+ await this.sendReply(config, message.userId, "The voice message did not contain recognizable speech. Try sending it again.");
14819
+ return;
14820
+ }
14821
+ }
14822
+ const session = await this.ensureSession(clientId, effectiveConfig);
14823
+ let images = [];
14824
+ if (message.messageType === "image") {
14825
+ if (!message.mediaId) {
14826
+ await this.sendReply(config, message.userId, "The WeCom image message did not include downloadable media. Try sending it again.");
14827
+ return;
14828
+ }
14829
+ try {
14830
+ images = [await this.downloadImage(config, message.mediaId)];
14831
+ } catch (error) {
14832
+ console.warn("[wecom-gateway] failed to download inbound image:", error instanceof Error ? error.message : error);
14833
+ await this.sendReply(config, message.userId, "The image could not be downloaded from WeCom. Try sending it again.");
14834
+ return;
14835
+ }
14836
+ }
14837
+ const imageResult = validateImages(images, session.model);
14838
+ if (!imageResult.ok) {
14839
+ await this.sendReply(config, message.userId, imageResult.message);
14840
+ return;
14841
+ }
14842
+ promptText ||= imageResult.images.length ? "Please analyze the attached image." : "";
14843
+ const chunks = [];
14844
+ const unsubscribe = session.subscribe((event) => {
14845
+ const update = event?.type === "message_update" ? event.assistantMessageEvent : void 0;
14846
+ if (update?.type === "text_delta" && typeof update.delta === "string") chunks.push(update.delta);
14847
+ });
14848
+ try {
14849
+ await this.sessionService.runForegroundWithClientProfileProxy(clientId, () => session.prompt(promptText, imageResult.images.length ? { images: imageResult.images } : void 0));
14850
+ } finally {
14851
+ unsubscribe();
14852
+ }
14853
+ const response = chunks.join("").trim() || extractLastAssistantText3(session.messages) || "(Pi finished without a text response.)";
14854
+ await this.sendReply(config, message.userId, response);
14855
+ }
14856
+ async ensureSession(clientId, config) {
14857
+ await this.sessionService.setClientAgentProfile(clientId, config.agentProfile || "default");
14858
+ const active = this.sessionService.getSession(clientId);
14859
+ if (active) return active;
14860
+ const mapped = this.db.prepare("SELECT pi_session_id FROM wecom_gateway_sessions WHERE client_id = ?").get(clientId);
14861
+ if (mapped) {
14862
+ const persisted = await this.sessionService.findPersistedSession(clientId, mapped.pi_session_id);
14863
+ if (persisted) return this.sessionService.resumeSession(clientId, persisted.path);
14864
+ }
14865
+ const preset = config.skillPresetId ? this.presetStore.getById(config.skillPresetId) : null;
14866
+ const result = await this.sessionService.createSession(clientId, {
14867
+ cwd: config.defaultCwd,
14868
+ modelProvider: config.modelProvider,
14869
+ modelId: config.modelId,
14870
+ enabledSkills: preset?.mode === "enabled" ? preset.skills : void 0,
14871
+ disabledSkills: preset?.mode === "disabled" ? preset.skills : void 0,
14872
+ presetId: preset?.id,
14873
+ skillMode: preset?.mode || config.skillMode || "all"
14874
+ });
14875
+ const now = (/* @__PURE__ */ new Date()).toISOString();
14876
+ this.db.prepare(`
14877
+ INSERT INTO wecom_gateway_sessions (session_key, client_id, pi_session_id, created_at, updated_at)
14878
+ VALUES (?, ?, ?, ?, ?)
14879
+ ON CONFLICT(session_key) DO UPDATE SET pi_session_id = excluded.pi_session_id, updated_at = excluded.updated_at
14880
+ `).run(clientId, clientId, result.session.sessionId, now, now);
14881
+ return result.session;
14882
+ }
14883
+ async handleCommand(clientId, message, baseConfig, effectiveConfig) {
14884
+ const commandText = normalizeGatewayCommandText(message.text);
14885
+ const [command = "", ...args] = commandText.split(/\s+/);
14886
+ const normalized = command.toLowerCase();
14887
+ if (!normalized.startsWith("/")) return false;
14888
+ try {
14889
+ if (normalized === "/new" || normalized === "/reset") {
14890
+ this.resetSession(clientId);
14891
+ await this.sendReply(baseConfig, message.userId, await this.formatStatus(clientId, this.resolveChatConfig(clientId, baseConfig), "Reset this WeCom chat. A fresh Pi session will start with your next message."));
14892
+ return true;
14893
+ }
14894
+ if (normalized === "/help") {
14895
+ await this.sendReply(baseConfig, message.userId, this.formatHelp());
14896
+ return true;
14897
+ }
14898
+ if (normalized === "/status") {
14899
+ await this.sendReply(baseConfig, message.userId, await this.formatStatus(clientId, effectiveConfig));
14900
+ return true;
14901
+ }
14902
+ if (normalized === "/profiles") {
14903
+ const profiles = await this.sessionService.listAgentProfiles();
14904
+ await this.sendReply(baseConfig, message.userId, ["Available profiles:", ...profiles.map((profile) => `${profile.id === (effectiveConfig.agentProfile || "default") ? "*" : " "} ${profile.id}`)].join("\n"));
14905
+ return true;
14906
+ }
14907
+ if (normalized === "/profile" || normalized === "/use-profile" || normalized === "/set-profile") {
14908
+ const profile = args[0]?.trim();
14909
+ if (!profile) throw new Error("Usage: /profile <profile-id>");
14910
+ const profiles = await this.sessionService.listAgentProfiles();
14911
+ if (!profiles.some((item) => item.id === profile)) throw new Error(`Unknown profile: ${profile}`);
14912
+ this.saveChatConfig(clientId, { agentProfile: profile });
14913
+ this.resetSession(clientId);
14914
+ await this.sendReply(baseConfig, message.userId, await this.formatStatus(clientId, this.resolveChatConfig(clientId, baseConfig), "Profile updated."));
14915
+ return true;
14916
+ }
14917
+ if (normalized === "/cwds" || normalized === "/pwds") {
14918
+ await this.sendReply(baseConfig, message.userId, baseConfig.cwdChoices.length ? baseConfig.cwdChoices.map((cwd, index) => `${index + 1}: ${cwd}`).join("\n") : "No gateway folders configured.");
14919
+ return true;
14920
+ }
14921
+ if (normalized === "/cwd") {
14922
+ const slot = Number(args[0]);
14923
+ if (!Number.isInteger(slot) || slot < 1 || slot > baseConfig.cwdChoices.length) throw new Error(`Usage: /cwd <1-${baseConfig.cwdChoices.length}>`);
14924
+ const target = baseConfig.cwdChoices[slot - 1];
14925
+ if (!isAbsolute6(target) || !(await stat8(target)).isDirectory()) throw new Error(`Configured CWD ${slot} must be an existing absolute directory`);
14926
+ this.saveChatConfig(clientId, { defaultCwd: target });
14927
+ this.resetSession(clientId);
14928
+ await this.sendReply(baseConfig, message.userId, await this.formatStatus(clientId, this.resolveChatConfig(clientId, baseConfig), "Working directory updated."));
14929
+ return true;
14930
+ }
14931
+ if (normalized === "/skillsets") {
14932
+ await this.sendReply(baseConfig, message.userId, this.formatSkillsets(effectiveConfig.skillPresetId));
14933
+ return true;
14934
+ }
14935
+ if (normalized === "/skillset") {
14936
+ const selected = args.join(" ").trim();
14937
+ if (!selected) throw new Error("Usage: /skillset <all|preset-name>");
14938
+ this.saveChatConfig(clientId, this.resolveSkillsetConfig(selected, true));
14939
+ this.resetSession(clientId);
14940
+ await this.sendReply(baseConfig, message.userId, await this.formatStatus(clientId, this.resolveChatConfig(clientId, baseConfig), "Skillset updated."));
14941
+ return true;
14942
+ }
14943
+ if (normalized === "/clear-config") {
14944
+ this.db.prepare("DELETE FROM wecom_gateway_configs WHERE client_id = ?").run(clientId);
14945
+ this.resetSession(clientId);
14946
+ await this.sendReply(baseConfig, message.userId, await this.formatStatus(clientId, this.resolveChatConfig(clientId, baseConfig), "WeCom chat config cleared."));
14947
+ return true;
14948
+ }
14949
+ } catch (error) {
14950
+ await this.sendReply(baseConfig, message.userId, `Command failed: ${error instanceof Error ? error.message : "unknown error"}
14951
+
14952
+ ${this.formatHelp()}`);
14953
+ return true;
14954
+ }
14955
+ await this.sendReply(baseConfig, message.userId, `Unknown command: ${command}
14956
+
14957
+ ${this.formatHelp()}`);
14958
+ return true;
14959
+ }
14960
+ resolveChatConfig(clientId, base) {
14961
+ const chat = this.loadChatConfig(clientId);
14962
+ const useBaseModel = !chat.agentProfile || chat.agentProfile === base.agentProfile;
14963
+ return {
14964
+ ...base,
14965
+ agentProfile: chat.agentProfile || base.agentProfile,
14966
+ defaultCwd: chat.defaultCwd && base.cwdChoices.includes(chat.defaultCwd) ? chat.defaultCwd : base.defaultCwd,
14967
+ modelProvider: useBaseModel ? base.modelProvider : void 0,
14968
+ modelId: useBaseModel ? base.modelId : void 0,
14969
+ skillMode: chat.skillMode || base.skillMode,
14970
+ skillPresetId: chat.skillMode ? chat.skillPresetId : base.skillPresetId
14971
+ };
14972
+ }
14973
+ loadChatConfig(clientId) {
14974
+ const row = this.db.prepare("SELECT agent_profile, default_cwd, skill_mode, skill_preset_id FROM wecom_gateway_configs WHERE client_id = ?").get(clientId);
14975
+ return row ? {
14976
+ agentProfile: row.agent_profile || void 0,
14977
+ defaultCwd: row.default_cwd || void 0,
14978
+ skillMode: row.skill_mode || void 0,
14979
+ skillPresetId: row.skill_preset_id
14980
+ } : {};
14981
+ }
14982
+ saveChatConfig(clientId, patch) {
14983
+ const next = { ...this.loadChatConfig(clientId), ...patch };
14984
+ const now = (/* @__PURE__ */ new Date()).toISOString();
14985
+ this.db.prepare(`
14986
+ INSERT INTO wecom_gateway_configs (client_id, agent_profile, default_cwd, skill_mode, skill_preset_id, created_at, updated_at)
14987
+ VALUES (?, ?, ?, ?, ?, ?, ?)
14988
+ ON CONFLICT(client_id) DO UPDATE SET
14989
+ agent_profile = excluded.agent_profile,
14990
+ default_cwd = excluded.default_cwd,
14991
+ skill_mode = excluded.skill_mode,
14992
+ skill_preset_id = excluded.skill_preset_id,
14993
+ updated_at = excluded.updated_at
14994
+ `).run(clientId, next.agentProfile || null, next.defaultCwd || null, next.skillMode || null, next.skillPresetId || null, now, now);
14995
+ }
14996
+ resetSession(clientId) {
14997
+ this.sessionService.disposeSession(clientId);
14998
+ this.db.prepare("DELETE FROM wecom_gateway_sessions WHERE client_id = ?").run(clientId);
14999
+ }
15000
+ async formatStatus(clientId, config, prefix) {
15001
+ const profiles = await this.sessionService.listAgentProfiles();
15002
+ const profile = profiles.find((item) => item.id === (config.agentProfile || "default")) || profiles[0];
15003
+ const active = this.sessionService.getSession(clientId);
15004
+ return [
15005
+ prefix,
15006
+ "Pi session status",
15007
+ "",
15008
+ `- Profile: ${profile?.id || "default"}`,
15009
+ `- CWD: ${config.defaultCwd || "(not configured)"}`,
15010
+ `- Provider: ${active?.model?.provider || config.modelProvider || profile?.defaultProvider || "(profile default)"}`,
15011
+ `- Model: ${active?.model?.id || config.modelId || profile?.defaultModel || "(profile default)"}`,
15012
+ `- Skillset: ${this.formatSkillsetName(config.skillPresetId)}`,
15013
+ `- Session: ${active ? `active (${active.sessionId})` : "not started"}`
15014
+ ].filter((line) => line !== void 0).join("\n");
15015
+ }
15016
+ formatHelp() {
15017
+ return [
15018
+ "Pi Cloud WeCom commands",
15019
+ "/new - reset this chat session",
15020
+ "/status - show current configuration",
15021
+ "/profiles - list profiles",
15022
+ "/profile <id> - switch profile",
15023
+ "/cwds - list allowed folders",
15024
+ "/cwd <number> - switch folder",
15025
+ "/skillsets - list skillsets",
15026
+ "/skillset <name> - switch skillset",
15027
+ "/clear-config - restore gateway defaults",
15028
+ "/help - show this help",
15029
+ "",
15030
+ GATEWAY_COMMON_ALIAS_HELP
15031
+ ].join("\n");
15032
+ }
15033
+ resolveSkillsetConfig(skillset, strict = false) {
15034
+ const selected = skillset?.trim();
15035
+ if (!selected || selected.toLowerCase() === "all") return { skillMode: "all", skillPresetId: null };
15036
+ const preset = this.findSkillPreset(selected);
15037
+ if (preset) return { skillMode: preset.mode, skillPresetId: preset.id };
15038
+ if (strict) throw new Error(`Unknown skillset: ${selected}`);
15039
+ console.warn("[wecom-gateway] default gateway skillset was not found; using all skills", { skillset: selected });
15040
+ return { skillMode: "all", skillPresetId: null };
15041
+ }
15042
+ formatSkillsets(currentPresetId) {
15043
+ const presets = this.presetStore.list();
15044
+ return [
15045
+ "Available skillsets:",
15046
+ `${currentPresetId ? " " : "*"} all - use all skills`,
15047
+ ...presets.map((preset) => `${preset.id === currentPresetId ? "*" : " "} ${preset.name} (${preset.mode}: ${preset.skills.join(", ") || "none"})`)
15048
+ ].join("\n");
15049
+ }
15050
+ formatSkillsetName(presetId) {
15051
+ if (!presetId) return "all";
15052
+ const preset = this.presetStore.getById(presetId);
15053
+ return preset ? `${preset.name} (${preset.mode})` : `unknown preset (${presetId})`;
15054
+ }
15055
+ findSkillPreset(nameOrId) {
15056
+ const normalized = nameOrId.trim().toLowerCase();
15057
+ return this.presetStore.list().find((preset) => preset.id === nameOrId || preset.name.toLowerCase() === normalized) || null;
15058
+ }
15059
+ async sendReply(config, userId, text) {
15060
+ for (const chunk of splitUtf8Text(formatWecomMarkdown(text), TEXT_CHUNK_MAX_BYTES)) {
15061
+ let token = await this.getAccessToken(config);
15062
+ let data = await this.sendApplicationMarkdown(config, token, userId, chunk);
15063
+ if ([40014, 42001].includes(Number(data.errcode))) {
15064
+ this.tokenCache = void 0;
15065
+ token = await this.getAccessToken(config);
15066
+ data = await this.sendApplicationMarkdown(config, token, userId, chunk);
15067
+ }
15068
+ if (Number(data.errcode) !== 0) throw new Error(`WeCom send message failed: ${stringValue3(data.errmsg) || `errcode=${data.errcode}`}`);
15069
+ }
15070
+ }
15071
+ async sendApplicationMarkdown(config, token, userId, text) {
15072
+ const response = await fetch(`${WECOM_API_BASE_URL}/cgi-bin/message/send?access_token=${encodeURIComponent(token)}`, {
15073
+ method: "POST",
15074
+ headers: { "content-type": "application/json" },
15075
+ body: JSON.stringify({
15076
+ touser: userId,
15077
+ msgtype: "markdown",
15078
+ agentid: Number(config.agentId),
15079
+ markdown: { content: text },
15080
+ safe: 0,
15081
+ enable_duplicate_check: 1,
15082
+ duplicate_check_interval: 1800
15083
+ })
15084
+ });
15085
+ return parseWecomResponse(response);
15086
+ }
15087
+ async downloadImage(config, mediaId) {
15088
+ const bytes = await this.downloadMedia(config, mediaId, MAX_IMAGE_BYTES, "An image is larger than 10 MB.");
15089
+ const mimeType = sniffImageMimeType(bytes);
15090
+ if (!mimeType) throw new Error("WeCom media response was not a supported image");
15091
+ return { type: "image", data: bytes.toString("base64"), mimeType };
15092
+ }
15093
+ downloadVoice(config, mediaId) {
15094
+ return this.downloadMedia(config, mediaId, MAX_AUDIO_BYTES, "A voice message is larger than 25 MB.");
15095
+ }
15096
+ async downloadMedia(config, mediaId, maxBytes, tooLargeMessage) {
15097
+ for (let attempt = 0; attempt < 2; attempt += 1) {
15098
+ const token = await this.getAccessToken(config);
15099
+ const response = await fetch(`${WECOM_API_BASE_URL}/cgi-bin/media/get?access_token=${encodeURIComponent(token)}&media_id=${encodeURIComponent(mediaId)}`);
15100
+ const contentType = response.headers.get("content-type") || "";
15101
+ if (contentType.toLowerCase().includes("application/json")) {
15102
+ const data = await parseWecomResponse(response);
15103
+ const errorCode = Number(data.errcode);
15104
+ if (attempt === 0 && [40014, 42001].includes(errorCode)) {
15105
+ this.tokenCache = void 0;
15106
+ continue;
15107
+ }
15108
+ throw new Error(`WeCom media download failed: ${stringValue3(data.errmsg) || `errcode=${data.errcode}`}`);
15109
+ }
15110
+ if (!response.ok) throw new Error(`WeCom media download failed: HTTP ${response.status}`);
15111
+ return readResponseBytes(response, maxBytes, tooLargeMessage);
15112
+ }
15113
+ throw new Error("WeCom media download failed after refreshing the access token");
15114
+ }
15115
+ async getAccessToken(config) {
15116
+ const now = Date.now();
15117
+ if (this.tokenCache && this.tokenCache.expiresAt - TOKEN_REFRESH_SKEW_MS2 > now) return this.tokenCache.token;
15118
+ const response = await fetch(`${WECOM_API_BASE_URL}/cgi-bin/gettoken?corpid=${encodeURIComponent(config.corpId)}&corpsecret=${encodeURIComponent(config.corpSecret)}`);
15119
+ const data = await parseWecomResponse(response);
15120
+ const token = stringValue3(data.access_token);
15121
+ if (Number(data.errcode) !== 0 || !token) throw new Error(`WeCom access token failed: ${stringValue3(data.errmsg) || `errcode=${data.errcode}`}`);
15122
+ this.tokenCache = { token, expiresAt: now + (Number(data.expires_in) || 7200) * 1e3 };
15123
+ return token;
15124
+ }
15125
+ };
15126
+ async function readResponseBytes(response, maxBytes, tooLargeMessage) {
15127
+ const contentLength = Number(response.headers.get("content-length"));
15128
+ if (Number.isFinite(contentLength) && contentLength > maxBytes) {
15129
+ await response.body?.cancel();
15130
+ throw new Error(tooLargeMessage);
15131
+ }
15132
+ if (!response.body) return Buffer.alloc(0);
15133
+ const reader = response.body.getReader();
15134
+ const chunks = [];
15135
+ let totalBytes = 0;
15136
+ try {
15137
+ while (true) {
15138
+ const { done, value } = await reader.read();
15139
+ if (done) break;
15140
+ totalBytes += value.byteLength;
15141
+ if (totalBytes > maxBytes) {
15142
+ await reader.cancel();
15143
+ throw new Error(tooLargeMessage);
15144
+ }
15145
+ chunks.push(Buffer.from(value));
15146
+ }
15147
+ } finally {
15148
+ reader.releaseLock();
15149
+ }
15150
+ return Buffer.concat(chunks, totalBytes);
15151
+ }
15152
+ function parseWecomMessage(xml) {
15153
+ const messageType = xmlValue(xml, "MsgType");
15154
+ const userId = xmlValue(xml, "FromUserName");
15155
+ const messageId = xmlValue(xml, "MsgId");
15156
+ const agentId = xmlValue(xml, "AgentID");
15157
+ if (!messageType || !userId || !messageId || !agentId) return null;
15158
+ return {
15159
+ messageType,
15160
+ userId,
15161
+ messageId,
15162
+ agentId,
15163
+ text: xmlValue(xml, "Content").trim(),
15164
+ mediaId: xmlValue(xml, "MediaId") || void 0,
15165
+ picUrl: xmlValue(xml, "PicUrl") || void 0
15166
+ };
15167
+ }
15168
+ function xmlValue(xml, tag) {
15169
+ const match = new RegExp(`<${tag}>(?:<!\\[CDATA\\[([\\s\\S]*?)\\]\\]>|([\\s\\S]*?))<\\/${tag}>`, "i").exec(xml);
15170
+ return decodeXmlEntities((match?.[1] ?? match?.[2] ?? "").trim());
15171
+ }
15172
+ function decodeXmlEntities(value) {
15173
+ return value.replace(/&lt;/g, "<").replace(/&gt;/g, ">").replace(/&quot;/g, '"').replace(/&apos;/g, "'").replace(/&amp;/g, "&");
15174
+ }
15175
+ function parseList2(value) {
15176
+ return Array.from(new Set((value || "").split(",").map((item) => item.trim()).filter(Boolean)));
15177
+ }
15178
+ function normalizeList(value) {
15179
+ return Array.from(new Set((Array.isArray(value) ? value : []).filter((item) => typeof item === "string").map((item) => item.trim()).filter(Boolean)));
15180
+ }
15181
+ function stringValue3(value) {
15182
+ return typeof value === "string" ? value : value === void 0 || value === null ? "" : String(value);
15183
+ }
15184
+ function gatewayError(message, statusCode) {
15185
+ const error = new Error(message);
15186
+ error.statusCode = statusCode;
15187
+ return error;
15188
+ }
15189
+ async function parseWecomResponse(response) {
15190
+ const text = await response.text();
15191
+ let data = {};
15192
+ try {
15193
+ data = JSON.parse(text);
15194
+ } catch {
15195
+ }
15196
+ if (!response.ok) throw new Error(`WeCom HTTP ${response.status}: ${text.slice(0, 200)}`);
15197
+ return data;
15198
+ }
15199
+ function extractLastAssistantText3(messages) {
15200
+ for (let index = messages.length - 1; index >= 0; index -= 1) {
15201
+ const message = messages[index];
15202
+ if (message?.role !== "assistant") continue;
15203
+ if (typeof message.content === "string" && message.content.trim()) return message.content.trim();
15204
+ if (!Array.isArray(message.content)) continue;
15205
+ const text = message.content.filter((part) => part?.type === "text" && typeof part.text === "string").map((part) => part.text).join("").trim();
15206
+ if (text) return text;
15207
+ }
15208
+ return void 0;
15209
+ }
15210
+ function formatWecomMarkdown(text) {
15211
+ return text.replace(/^([ \t]*)[-+*][ \t]+/gm, (_match, indentation) => `${indentation}${indentation ? "\u25E6" : "\u2022"} `);
15212
+ }
15213
+ function splitUtf8Text(text, maxBytes) {
15214
+ if (Buffer.byteLength(text, "utf8") <= maxBytes) return [text];
15215
+ const chunks = [];
15216
+ let current = "";
15217
+ let currentBytes = 0;
15218
+ for (const character of text) {
15219
+ const characterBytes = Buffer.byteLength(character, "utf8");
15220
+ if (current && currentBytes + characterBytes > maxBytes) {
15221
+ chunks.push(current);
15222
+ current = "";
15223
+ currentBytes = 0;
15224
+ }
15225
+ current += character;
15226
+ currentBytes += characterBytes;
15227
+ }
15228
+ if (current) chunks.push(current);
15229
+ return chunks;
15230
+ }
15231
+
14080
15232
  // src/services/review-source-store.ts
14081
15233
  import * as os8 from "os";
14082
15234
  import * as path11 from "path";
@@ -14124,7 +15276,7 @@ var ClaudeCodeReviewSourceAdapter = class {
14124
15276
  try {
14125
15277
  const records = this.readRecords(filePath);
14126
15278
  if (!records.length) return [];
14127
- const stat8 = fs5.statSync(filePath);
15279
+ const stat9 = fs5.statSync(filePath);
14128
15280
  const sessionRecord = records.find((record) => record.sessionId || record.session_id);
14129
15281
  const sessionId = sessionRecord?.sessionId || sessionRecord?.session_id || path7.basename(filePath, ".jsonl");
14130
15282
  const cwd = records.find((record) => record.cwd)?.cwd;
@@ -14140,8 +15292,8 @@ var ClaudeCodeReviewSourceAdapter = class {
14140
15292
  name: title,
14141
15293
  path: filePath,
14142
15294
  cwd,
14143
- created: new Date(timestamps.length ? Math.min(...timestamps) : stat8.birthtimeMs).toISOString(),
14144
- modified: new Date(timestamps.length ? Math.max(...timestamps) : stat8.mtimeMs).toISOString(),
15295
+ created: new Date(timestamps.length ? Math.min(...timestamps) : stat9.birthtimeMs).toISOString(),
15296
+ modified: new Date(timestamps.length ? Math.max(...timestamps) : stat9.mtimeMs).toISOString(),
14145
15297
  messageCount: messages.length,
14146
15298
  firstMessage: firstUserMessage ? this.messageText(firstUserMessage.content) : void 0
14147
15299
  }
@@ -14287,7 +15439,7 @@ var CodexReviewSourceAdapter = class {
14287
15439
  try {
14288
15440
  const records = this.readRecords(filePath);
14289
15441
  if (!records.length) return [];
14290
- const stat8 = fs6.statSync(filePath);
15442
+ const stat9 = fs6.statSync(filePath);
14291
15443
  const metadata = records.find((record) => record.type === "session_meta")?.payload;
14292
15444
  const messages = this.recordsToMessages(records);
14293
15445
  const timestamps = records.map((record) => this.timestamp(record.timestamp)).filter((value) => value !== void 0);
@@ -14299,8 +15451,8 @@ var CodexReviewSourceAdapter = class {
14299
15451
  sourceId: "",
14300
15452
  path: filePath,
14301
15453
  cwd: this.stringValue(metadata?.cwd),
14302
- created: new Date(timestamps.length ? Math.min(...timestamps) : stat8.birthtimeMs).toISOString(),
14303
- modified: new Date(timestamps.length ? Math.max(...timestamps) : stat8.mtimeMs).toISOString(),
15454
+ created: new Date(timestamps.length ? Math.min(...timestamps) : stat9.birthtimeMs).toISOString(),
15455
+ modified: new Date(timestamps.length ? Math.max(...timestamps) : stat9.mtimeMs).toISOString(),
14304
15456
  messageCount: messages.length,
14305
15457
  firstMessage: firstUserMessage ? this.messageText(firstUserMessage.content) : void 0
14306
15458
  }
@@ -14866,7 +16018,7 @@ function createDefaultConfig() {
14866
16018
  if (process.env.NODE_ENV === "test") return;
14867
16019
  if (process.env.PI_CLOUD_AUTH_USERNAME || process.env.PI_CLOUD_AUTH_PASSWORD || process.env.PI_CLOUD_AUTH_PASSWORD_HASH) return;
14868
16020
  const username = "admin";
14869
- const password = randomBytes3(6).toString("base64url");
16021
+ const password = randomBytes4(6).toString("base64url");
14870
16022
  const samplePath = path12.resolve(__dirname, "../../.env.example");
14871
16023
  const content = renderDefaultConfig(readFileSync4(samplePath, "utf8"), username, password);
14872
16024
  mkdirSync2(userConfigDir, { recursive: true, mode: 448 });
@@ -14887,6 +16039,7 @@ var CONTENT_SECURITY_POLICY = [
14887
16039
  "object-src 'none'",
14888
16040
  "frame-ancestors 'none'",
14889
16041
  "img-src 'self' data: blob:",
16042
+ "media-src 'self' blob:",
14890
16043
  "font-src 'self' data:",
14891
16044
  "style-src 'self' 'unsafe-inline'",
14892
16045
  "script-src 'self'",
@@ -14909,6 +16062,7 @@ function isPublicRoute(req) {
14909
16062
  if (url === "/api/health") return true;
14910
16063
  if (url.startsWith("/api/auth")) return true;
14911
16064
  if (url === "/api/gateways/feishu/events") return true;
16065
+ if (url === "/api/gateways/wecom/callback") return true;
14912
16066
  if (req.method === "GET" && !url.startsWith("/api/")) return true;
14913
16067
  return false;
14914
16068
  }
@@ -14989,6 +16143,7 @@ async function buildApp() {
14989
16143
  const githubSettings = new GithubSettingsStore(db);
14990
16144
  const feishuGateway = new FeishuGatewayService(db, gatewaySettings, piSessionService);
14991
16145
  const weixinGateway = new WeixinGatewayService(db, gatewaySettings, piSessionService);
16146
+ const wecomGateway = new WecomGatewayService(db, gatewaySettings, piSessionService);
14992
16147
  weixinGateway.start();
14993
16148
  const repositoryCloner = new RepositoryCloner({
14994
16149
  spawnGit: (args, options) => spawn3("git", args, options),
@@ -15040,7 +16195,7 @@ async function buildApp() {
15040
16195
  reply.header("X-Content-Type-Options", "nosniff");
15041
16196
  reply.header("X-Frame-Options", "DENY");
15042
16197
  reply.header("Referrer-Policy", "no-referrer");
15043
- reply.header("Permissions-Policy", "camera=(), microphone=(), geolocation=()");
16198
+ reply.header("Permissions-Policy", "camera=(), microphone=(self), geolocation=()");
15044
16199
  if (authConfig.cookieSecure) {
15045
16200
  reply.header("Strict-Transport-Security", "max-age=31536000");
15046
16201
  }
@@ -15089,6 +16244,7 @@ async function buildApp() {
15089
16244
  await app.register(taskRoutes, { prefix: "/api/tasks", store: projectTaskStore, starter: projectTaskStarter, activityStore: sessionActivityStore });
15090
16245
  await app.register(gatewayRoutes, { prefix: "/api/gateways", settings: gatewaySettings, weixin: weixinGateway });
15091
16246
  await app.register(feishuGatewayRoutes, { prefix: "/api/gateways/feishu", service: feishuGateway });
16247
+ await app.register(wecomGatewayRoutes, { prefix: "/api/gateways/wecom", service: wecomGateway });
15092
16248
  const gitHostingRouteOptions = {
15093
16249
  settings: giteaSettings,
15094
16250
  githubSettings,
@@ -15108,6 +16264,7 @@ async function buildApp() {
15108
16264
  });
15109
16265
  await app.register(slashCommandRoutes, { prefix: "/api/slash-commands" });
15110
16266
  await app.register(changelogRoutes, { prefix: "/api/changelog" });
16267
+ await app.register(speechRoutes, { prefix: "/api/speech" });
15111
16268
  await app.register(reviewSourceRoutes, { prefix: "/api/review-sources", reviewSourceService, pinStore: sessionPinStore });
15112
16269
  await app.register(chatWebSocket);
15113
16270
  await app.register(terminalWebSocket);