pi-cloud 2.0.0 → 2.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (288) hide show
  1. package/.env.example +29 -0
  2. package/CHANGELOG.md +48 -2
  3. package/README.md +3 -3
  4. package/README.zh-CN.md +3 -3
  5. package/bin/pi-cloud.mjs +1 -1
  6. package/client/dist/.vite/manifest.json +333 -309
  7. package/client/dist/assets/{EditorPanel-qAJ9i_Hs.css → EditorPanel-CVtykOZb.css} +1 -1
  8. package/client/dist/assets/EditorPanel-CmUoE78w.js +968 -0
  9. package/client/dist/assets/{GitHistoryView-i__uu43I.js → GitHistoryView-v-2M8_wB.js} +1 -1
  10. package/client/dist/assets/{MemoryCenter-DOvz1FIy.js → MemoryCenter-C7FlmiQa.js} +1 -1
  11. package/client/dist/assets/{PhCaretLeft.vue-C3HxQ-jr.js → PhCaretLeft.vue-mR6H4iil.js} +1 -1
  12. package/client/dist/assets/PhFolderOpen.vue-CBZCIEKc.js +1 -0
  13. package/client/dist/assets/SettingsDialog-BaIuifOT.css +1 -0
  14. package/client/dist/assets/SettingsDialog-CmkVcpN6.js +5 -0
  15. package/client/dist/assets/TaskInboxPanel-CZiNlRVB.js +1 -0
  16. package/client/dist/assets/{TaskQueuePanel-Bnvgy4cm.css → TaskInboxPanel-D-F8nXji.css} +1 -1
  17. package/client/dist/assets/{TerminalPanel-BQhGwCE5.js → TerminalPanel-BKnITvmW.js} +1 -1
  18. package/client/dist/assets/{abnfDiagram-VCTEODGH-DyGBNBbN.js → abnfDiagram-VCTEODGH-C0onveOK.js} +1 -1
  19. package/client/dist/assets/{arc-B_21hOEg.js → arc-BtnkAKcM.js} +1 -1
  20. package/client/dist/assets/{architectureDiagram-5GKGNRK7-CjyAJ-2z.js → architectureDiagram-5GKGNRK7-BWzkt3mg.js} +1 -1
  21. package/client/dist/assets/{blockDiagram-NRAW4CY4-CmrEp6Fe.js → blockDiagram-NRAW4CY4-B5U3jj5J.js} +1 -1
  22. package/client/dist/assets/{c4Diagram-UCG6FXSJ-DfT2xU1s.js → c4Diagram-UCG6FXSJ-BijI8bRn.js} +1 -1
  23. package/client/dist/assets/channel-Fc9PxOLB.js +1 -0
  24. package/client/dist/assets/{chunk-2Q5K7J3B-BVEJoy95.js → chunk-2Q5K7J3B-BEieUt6C.js} +1 -1
  25. package/client/dist/assets/{chunk-5VM5RSS4-AYOc2H0F.js → chunk-5VM5RSS4-DK306biR.js} +1 -1
  26. package/client/dist/assets/{chunk-F27PBJKO-CAdIcM4C.js → chunk-F27PBJKO-jkwA9GYW.js} +1 -1
  27. package/client/dist/assets/{chunk-G27WJ6UU-Bk4Qjg6Y.js → chunk-G27WJ6UU-DnVXeFY7.js} +1 -1
  28. package/client/dist/assets/{chunk-JWPE2WC7-u-wi2HbJ.js → chunk-JWPE2WC7-DpOSXvW8.js} +1 -1
  29. package/client/dist/assets/{chunk-LCL6LL3I-BgicKCMi.js → chunk-LCL6LL3I-BD7QKy4w.js} +1 -1
  30. package/client/dist/assets/{chunk-POPQ4Y6H-DhJ3_oti.js → chunk-POPQ4Y6H-BqLcddFA.js} +1 -1
  31. package/client/dist/assets/{chunk-SVP7TREG-B-FKnWYy.js → chunk-SVP7TREG-C9GhrbFg.js} +1 -1
  32. package/client/dist/assets/{chunk-XXDRQBXY-Dnsv7iwr.js → chunk-XXDRQBXY-jQoIqp5a.js} +1 -1
  33. package/client/dist/assets/classDiagram-DTDB5LWJ-D6WMV2bo.js +1 -0
  34. package/client/dist/assets/classDiagram-v2-JRS7N3AN-D6WMV2bo.js +1 -0
  35. package/client/dist/assets/{cose-bilkent-JH36ORCC-DL2lIpOf.js → cose-bilkent-JH36ORCC-DNt10hsx.js} +1 -1
  36. package/client/dist/assets/cssMode-D5VnGCRL.js +1 -0
  37. package/client/dist/assets/{cynefin-OW5HDTMX-tZ1QaCp9.js → cynefin-OW5HDTMX-wI0wNPcp.js} +1 -1
  38. package/client/dist/assets/{cynefinDiagram-5FMLGOSQ-GLzVU_pJ.js → cynefinDiagram-5FMLGOSQ-DwksvA8r.js} +1 -1
  39. package/client/dist/assets/{dagre-3AP2YEHR-B8V1RvVr.js → dagre-3AP2YEHR-ZY-MhFWR.js} +1 -1
  40. package/client/dist/assets/{diagram-S7CK7UJ4-MTofUt2F.js → diagram-S7CK7UJ4-BKaJVqZu.js} +1 -1
  41. package/client/dist/assets/{diagram-UQ7AKVKN-Dw6Onbe3.js → diagram-UQ7AKVKN-DAaE-rA1.js} +1 -1
  42. package/client/dist/assets/{diagram-VSXAHHWV-C4WupsI1.js → diagram-VSXAHHWV-CJJ9xyap.js} +1 -1
  43. package/client/dist/assets/{diagram-VX7I27RA-CF78AvRx.js → diagram-VX7I27RA-4Er142Er.js} +1 -1
  44. package/client/dist/assets/{diagram-Z3DM3KII-xT7zx4S2.js → diagram-Z3DM3KII-eEe26_w-.js} +1 -1
  45. package/client/dist/assets/{ebnfDiagram-PWID7BFC-zkR-_zOG.js → ebnfDiagram-PWID7BFC-Dshy_lHs.js} +1 -1
  46. package/client/dist/assets/{erDiagram-SSCWMZ5O-BzU9E1j8.js → erDiagram-SSCWMZ5O-k7LwDRbW.js} +1 -1
  47. package/client/dist/assets/{flowDiagram-A5DVABFB-BM-160SB.js → flowDiagram-A5DVABFB-7_X479bX.js} +1 -1
  48. package/client/dist/assets/{freemarker2-B4PXq-2i.js → freemarker2-B7Gu__z_.js} +2 -2
  49. package/client/dist/assets/{ganttDiagram-EL5Y4UJY-LVhMiHMo.js → ganttDiagram-EL5Y4UJY-5WmBzvON.js} +1 -1
  50. package/client/dist/assets/{gitGraphDiagram-WWUBYQGX-CYGEpNO-.js → gitGraphDiagram-WWUBYQGX-CGTy36Lq.js} +1 -1
  51. package/client/dist/assets/handlebars-DRBRCJNd.js +1 -0
  52. package/client/dist/assets/html-6prUe0kc.js +1 -0
  53. package/client/dist/assets/htmlMode-DWoHKeAk.js +1 -0
  54. package/client/dist/assets/index-D8pylZiM.css +1 -0
  55. package/client/dist/assets/{index-PSBDlhaw.js → index-GTyUzvDG.js} +1 -1
  56. package/client/dist/assets/index-nLzofEMh.js +407 -0
  57. package/client/dist/assets/{infoDiagram-RXCK75RN-Ctmoz63S.js → infoDiagram-RXCK75RN-DZVb1P4d.js} +1 -1
  58. package/client/dist/assets/{ishikawaDiagram-5VMMS53U-DiWG9VHU.js → ishikawaDiagram-5VMMS53U-CMgYHU70.js} +1 -1
  59. package/client/dist/assets/javascript-Celvl7cV.js +1 -0
  60. package/client/dist/assets/{journeyDiagram-EYS64GPL-CImdI3K_.js → journeyDiagram-EYS64GPL-BnqHXNJS.js} +1 -1
  61. package/client/dist/assets/{jsonMode-l6_K9bFf.js → jsonMode-DLC0POfi.js} +2 -2
  62. package/client/dist/assets/{kanban-definition-3QL26DDD-CSSranPL.js → kanban-definition-3QL26DDD-BeStEVi-.js} +1 -1
  63. package/client/dist/assets/{layout-CE5PddK3.js → layout-B8XEB-tZ.js} +1 -1
  64. package/client/dist/assets/{linear-CkzMiZL-.js → linear-C1qdgmvA.js} +1 -1
  65. package/client/dist/assets/liquid-CiU5o5FX.js +1 -0
  66. package/client/dist/assets/{lspLanguageFeatures-ZfiCKJ_J.js → lspLanguageFeatures-BXNRwtlE.js} +1 -1
  67. package/client/dist/assets/mdx-CfTUs1eY.js +1 -0
  68. package/client/dist/assets/{mermaid.core-BlcsAN4s.js → mermaid.core-CFpSBIjf.js} +5 -5
  69. package/client/dist/assets/{mindmap-definition-FBJOCRG2-D01B1gH8.js → mindmap-definition-FBJOCRG2-CUNP7NW4.js} +1 -1
  70. package/client/dist/assets/{pegDiagram-XKGWAZYB-DhdSTBt5.js → pegDiagram-XKGWAZYB-bRD6LI7t.js} +1 -1
  71. package/client/dist/assets/{pieDiagram-E7YTZNPT-hVSzmo7b.js → pieDiagram-E7YTZNPT-CUE2D9Mz.js} +1 -1
  72. package/client/dist/assets/python-rOgKTqjK.js +1 -0
  73. package/client/dist/assets/{quadrantDiagram-AXDQQJYC-CVexQ_Dn.js → quadrantDiagram-AXDQQJYC-zDDE2XzB.js} +1 -1
  74. package/client/dist/assets/{railroadDiagram-O6MQD6OU-BPCMbhAx.js → railroadDiagram-O6MQD6OU-3TRS2gLn.js} +1 -1
  75. package/client/dist/assets/razor-DhEzBZvo.js +1 -0
  76. package/client/dist/assets/{requirementDiagram-EFPCY7ZU-EntABUqc.js → requirementDiagram-EFPCY7ZU-Cgm0MiBG.js} +1 -1
  77. package/client/dist/assets/{sankeyDiagram-P5KCCOFB-D68oih_M.js → sankeyDiagram-P5KCCOFB-DzQfpaqZ.js} +1 -1
  78. package/client/dist/assets/{sequenceDiagram-WJ2MYXX4-DWzaT5vI.js → sequenceDiagram-WJ2MYXX4-D_eKGGY5.js} +1 -1
  79. package/client/dist/assets/{sizeCapture-X5ZJPWSS-HbYBaSbU.js → sizeCapture-X5ZJPWSS-BVFpPdEZ.js} +1 -1
  80. package/client/dist/assets/{stateDiagram-HBIQ2CUA-BteRSnYL.js → stateDiagram-HBIQ2CUA-Bvp5OM6x.js} +1 -1
  81. package/client/dist/assets/stateDiagram-v2-4QOOHH4V-CM3P74Kw.js +1 -0
  82. package/client/dist/assets/{swimlanes-XN3QIQJK-BAvVt5mV.js → swimlanes-XN3QIQJK-B497DW3A.js} +1 -1
  83. package/client/dist/assets/swimlanesDiagram-VK2B7HYN-DAMUXB40.js +8 -0
  84. package/client/dist/assets/{timeline-definition-24CTP7MA-D6yKUiYl.js → timeline-definition-24CTP7MA-BHAeybVL.js} +1 -1
  85. package/client/dist/assets/{tsMode-DrakxHLR.js → tsMode-BKQ7PNBK.js} +2 -2
  86. package/client/dist/assets/typescript-0T3kV3Fw.js +1 -0
  87. package/client/dist/assets/{useTerminal-Dhk5K03J.js → useTerminal-D0Eg4ohV.js} +1 -1
  88. package/client/dist/assets/{vennDiagram-4TSXK5OY-CGU61Pyq.js → vennDiagram-4TSXK5OY-C3aoPRLD.js} +1 -1
  89. package/client/dist/assets/{wardleyDiagram-VM6X3IG4-BrTe4Lxv.js → wardleyDiagram-VM6X3IG4-D45EDW-W.js} +1 -1
  90. package/client/dist/assets/{workers-CqVatS52.js → workers-nJulRuKZ.js} +1 -1
  91. package/client/dist/assets/xml-Dm41fNV2.js +1 -0
  92. package/client/dist/assets/{xychartDiagram-S5SC5T6Z-CnMmHAmD.js → xychartDiagram-S5SC5T6Z-CCfr24LB.js} +1 -1
  93. package/client/dist/assets/yaml-BHs6-_Vy.js +1 -0
  94. package/client/dist/index.html +2 -2
  95. package/client/dist/pdfjs/cmaps/78-EUC-H.bcmap +0 -0
  96. package/client/dist/pdfjs/cmaps/78-EUC-V.bcmap +0 -0
  97. package/client/dist/pdfjs/cmaps/78-H.bcmap +0 -0
  98. package/client/dist/pdfjs/cmaps/78-RKSJ-H.bcmap +0 -0
  99. package/client/dist/pdfjs/cmaps/78-RKSJ-V.bcmap +0 -0
  100. package/client/dist/pdfjs/cmaps/78-V.bcmap +0 -0
  101. package/client/dist/pdfjs/cmaps/78ms-RKSJ-H.bcmap +0 -0
  102. package/client/dist/pdfjs/cmaps/78ms-RKSJ-V.bcmap +0 -0
  103. package/client/dist/pdfjs/cmaps/83pv-RKSJ-H.bcmap +0 -0
  104. package/client/dist/pdfjs/cmaps/90ms-RKSJ-H.bcmap +0 -0
  105. package/client/dist/pdfjs/cmaps/90ms-RKSJ-V.bcmap +0 -0
  106. package/client/dist/pdfjs/cmaps/90msp-RKSJ-H.bcmap +0 -0
  107. package/client/dist/pdfjs/cmaps/90msp-RKSJ-V.bcmap +0 -0
  108. package/client/dist/pdfjs/cmaps/90pv-RKSJ-H.bcmap +0 -0
  109. package/client/dist/pdfjs/cmaps/90pv-RKSJ-V.bcmap +0 -0
  110. package/client/dist/pdfjs/cmaps/Add-H.bcmap +0 -0
  111. package/client/dist/pdfjs/cmaps/Add-RKSJ-H.bcmap +0 -0
  112. package/client/dist/pdfjs/cmaps/Add-RKSJ-V.bcmap +0 -0
  113. package/client/dist/pdfjs/cmaps/Add-V.bcmap +0 -0
  114. package/client/dist/pdfjs/cmaps/Adobe-CNS1-0.bcmap +0 -0
  115. package/client/dist/pdfjs/cmaps/Adobe-CNS1-1.bcmap +0 -0
  116. package/client/dist/pdfjs/cmaps/Adobe-CNS1-2.bcmap +0 -0
  117. package/client/dist/pdfjs/cmaps/Adobe-CNS1-3.bcmap +0 -0
  118. package/client/dist/pdfjs/cmaps/Adobe-CNS1-4.bcmap +0 -0
  119. package/client/dist/pdfjs/cmaps/Adobe-CNS1-5.bcmap +0 -0
  120. package/client/dist/pdfjs/cmaps/Adobe-CNS1-6.bcmap +0 -0
  121. package/client/dist/pdfjs/cmaps/Adobe-CNS1-UCS2.bcmap +0 -0
  122. package/client/dist/pdfjs/cmaps/Adobe-GB1-0.bcmap +0 -0
  123. package/client/dist/pdfjs/cmaps/Adobe-GB1-1.bcmap +0 -0
  124. package/client/dist/pdfjs/cmaps/Adobe-GB1-2.bcmap +0 -0
  125. package/client/dist/pdfjs/cmaps/Adobe-GB1-3.bcmap +0 -0
  126. package/client/dist/pdfjs/cmaps/Adobe-GB1-4.bcmap +0 -0
  127. package/client/dist/pdfjs/cmaps/Adobe-GB1-5.bcmap +0 -0
  128. package/client/dist/pdfjs/cmaps/Adobe-GB1-UCS2.bcmap +0 -0
  129. package/client/dist/pdfjs/cmaps/Adobe-Japan1-0.bcmap +0 -0
  130. package/client/dist/pdfjs/cmaps/Adobe-Japan1-1.bcmap +0 -0
  131. package/client/dist/pdfjs/cmaps/Adobe-Japan1-2.bcmap +0 -0
  132. package/client/dist/pdfjs/cmaps/Adobe-Japan1-3.bcmap +0 -0
  133. package/client/dist/pdfjs/cmaps/Adobe-Japan1-4.bcmap +0 -0
  134. package/client/dist/pdfjs/cmaps/Adobe-Japan1-5.bcmap +0 -0
  135. package/client/dist/pdfjs/cmaps/Adobe-Japan1-6.bcmap +0 -0
  136. package/client/dist/pdfjs/cmaps/Adobe-Japan1-UCS2.bcmap +0 -0
  137. package/client/dist/pdfjs/cmaps/Adobe-Korea1-0.bcmap +0 -0
  138. package/client/dist/pdfjs/cmaps/Adobe-Korea1-1.bcmap +0 -0
  139. package/client/dist/pdfjs/cmaps/Adobe-Korea1-2.bcmap +0 -0
  140. package/client/dist/pdfjs/cmaps/Adobe-Korea1-UCS2.bcmap +0 -0
  141. package/client/dist/pdfjs/cmaps/B5-H.bcmap +0 -0
  142. package/client/dist/pdfjs/cmaps/B5-V.bcmap +0 -0
  143. package/client/dist/pdfjs/cmaps/B5pc-H.bcmap +0 -0
  144. package/client/dist/pdfjs/cmaps/B5pc-V.bcmap +0 -0
  145. package/client/dist/pdfjs/cmaps/CNS-EUC-H.bcmap +0 -0
  146. package/client/dist/pdfjs/cmaps/CNS-EUC-V.bcmap +0 -0
  147. package/client/dist/pdfjs/cmaps/CNS1-H.bcmap +0 -0
  148. package/client/dist/pdfjs/cmaps/CNS1-V.bcmap +0 -0
  149. package/client/dist/pdfjs/cmaps/CNS2-H.bcmap +0 -0
  150. package/client/dist/pdfjs/cmaps/CNS2-V.bcmap +3 -0
  151. package/client/dist/pdfjs/cmaps/ETHK-B5-H.bcmap +0 -0
  152. package/client/dist/pdfjs/cmaps/ETHK-B5-V.bcmap +0 -0
  153. package/client/dist/pdfjs/cmaps/ETen-B5-H.bcmap +0 -0
  154. package/client/dist/pdfjs/cmaps/ETen-B5-V.bcmap +0 -0
  155. package/client/dist/pdfjs/cmaps/ETenms-B5-H.bcmap +3 -0
  156. package/client/dist/pdfjs/cmaps/ETenms-B5-V.bcmap +0 -0
  157. package/client/dist/pdfjs/cmaps/EUC-H.bcmap +0 -0
  158. package/client/dist/pdfjs/cmaps/EUC-V.bcmap +0 -0
  159. package/client/dist/pdfjs/cmaps/Ext-H.bcmap +0 -0
  160. package/client/dist/pdfjs/cmaps/Ext-RKSJ-H.bcmap +0 -0
  161. package/client/dist/pdfjs/cmaps/Ext-RKSJ-V.bcmap +0 -0
  162. package/client/dist/pdfjs/cmaps/Ext-V.bcmap +0 -0
  163. package/client/dist/pdfjs/cmaps/GB-EUC-H.bcmap +0 -0
  164. package/client/dist/pdfjs/cmaps/GB-EUC-V.bcmap +0 -0
  165. package/client/dist/pdfjs/cmaps/GB-H.bcmap +4 -0
  166. package/client/dist/pdfjs/cmaps/GB-V.bcmap +0 -0
  167. package/client/dist/pdfjs/cmaps/GBK-EUC-H.bcmap +0 -0
  168. package/client/dist/pdfjs/cmaps/GBK-EUC-V.bcmap +0 -0
  169. package/client/dist/pdfjs/cmaps/GBK2K-H.bcmap +0 -0
  170. package/client/dist/pdfjs/cmaps/GBK2K-V.bcmap +0 -0
  171. package/client/dist/pdfjs/cmaps/GBKp-EUC-H.bcmap +0 -0
  172. package/client/dist/pdfjs/cmaps/GBKp-EUC-V.bcmap +0 -0
  173. package/client/dist/pdfjs/cmaps/GBT-EUC-H.bcmap +0 -0
  174. package/client/dist/pdfjs/cmaps/GBT-EUC-V.bcmap +0 -0
  175. package/client/dist/pdfjs/cmaps/GBT-H.bcmap +0 -0
  176. package/client/dist/pdfjs/cmaps/GBT-V.bcmap +0 -0
  177. package/client/dist/pdfjs/cmaps/GBTpc-EUC-H.bcmap +0 -0
  178. package/client/dist/pdfjs/cmaps/GBTpc-EUC-V.bcmap +0 -0
  179. package/client/dist/pdfjs/cmaps/GBpc-EUC-H.bcmap +0 -0
  180. package/client/dist/pdfjs/cmaps/GBpc-EUC-V.bcmap +0 -0
  181. package/client/dist/pdfjs/cmaps/H.bcmap +0 -0
  182. package/client/dist/pdfjs/cmaps/HKdla-B5-H.bcmap +0 -0
  183. package/client/dist/pdfjs/cmaps/HKdla-B5-V.bcmap +0 -0
  184. package/client/dist/pdfjs/cmaps/HKdlb-B5-H.bcmap +0 -0
  185. package/client/dist/pdfjs/cmaps/HKdlb-B5-V.bcmap +0 -0
  186. package/client/dist/pdfjs/cmaps/HKgccs-B5-H.bcmap +0 -0
  187. package/client/dist/pdfjs/cmaps/HKgccs-B5-V.bcmap +0 -0
  188. package/client/dist/pdfjs/cmaps/HKm314-B5-H.bcmap +0 -0
  189. package/client/dist/pdfjs/cmaps/HKm314-B5-V.bcmap +0 -0
  190. package/client/dist/pdfjs/cmaps/HKm471-B5-H.bcmap +0 -0
  191. package/client/dist/pdfjs/cmaps/HKm471-B5-V.bcmap +0 -0
  192. package/client/dist/pdfjs/cmaps/HKscs-B5-H.bcmap +0 -0
  193. package/client/dist/pdfjs/cmaps/HKscs-B5-V.bcmap +0 -0
  194. package/client/dist/pdfjs/cmaps/Hankaku.bcmap +0 -0
  195. package/client/dist/pdfjs/cmaps/Hiragana.bcmap +0 -0
  196. package/client/dist/pdfjs/cmaps/KSC-EUC-H.bcmap +0 -0
  197. package/client/dist/pdfjs/cmaps/KSC-EUC-V.bcmap +0 -0
  198. package/client/dist/pdfjs/cmaps/KSC-H.bcmap +0 -0
  199. package/client/dist/pdfjs/cmaps/KSC-Johab-H.bcmap +0 -0
  200. package/client/dist/pdfjs/cmaps/KSC-Johab-V.bcmap +0 -0
  201. package/client/dist/pdfjs/cmaps/KSC-V.bcmap +0 -0
  202. package/client/dist/pdfjs/cmaps/KSCms-UHC-H.bcmap +0 -0
  203. package/client/dist/pdfjs/cmaps/KSCms-UHC-HW-H.bcmap +0 -0
  204. package/client/dist/pdfjs/cmaps/KSCms-UHC-HW-V.bcmap +0 -0
  205. package/client/dist/pdfjs/cmaps/KSCms-UHC-V.bcmap +0 -0
  206. package/client/dist/pdfjs/cmaps/KSCpc-EUC-H.bcmap +0 -0
  207. package/client/dist/pdfjs/cmaps/KSCpc-EUC-V.bcmap +0 -0
  208. package/client/dist/pdfjs/cmaps/Katakana.bcmap +0 -0
  209. package/client/dist/pdfjs/cmaps/LICENSE +36 -0
  210. package/client/dist/pdfjs/cmaps/NWP-H.bcmap +0 -0
  211. package/client/dist/pdfjs/cmaps/NWP-V.bcmap +0 -0
  212. package/client/dist/pdfjs/cmaps/RKSJ-H.bcmap +0 -0
  213. package/client/dist/pdfjs/cmaps/RKSJ-V.bcmap +0 -0
  214. package/client/dist/pdfjs/cmaps/Roman.bcmap +0 -0
  215. package/client/dist/pdfjs/cmaps/UniCNS-UCS2-H.bcmap +0 -0
  216. package/client/dist/pdfjs/cmaps/UniCNS-UCS2-V.bcmap +0 -0
  217. package/client/dist/pdfjs/cmaps/UniCNS-UTF16-H.bcmap +0 -0
  218. package/client/dist/pdfjs/cmaps/UniCNS-UTF16-V.bcmap +0 -0
  219. package/client/dist/pdfjs/cmaps/UniCNS-UTF32-H.bcmap +0 -0
  220. package/client/dist/pdfjs/cmaps/UniCNS-UTF32-V.bcmap +0 -0
  221. package/client/dist/pdfjs/cmaps/UniCNS-UTF8-H.bcmap +0 -0
  222. package/client/dist/pdfjs/cmaps/UniCNS-UTF8-V.bcmap +0 -0
  223. package/client/dist/pdfjs/cmaps/UniGB-UCS2-H.bcmap +0 -0
  224. package/client/dist/pdfjs/cmaps/UniGB-UCS2-V.bcmap +0 -0
  225. package/client/dist/pdfjs/cmaps/UniGB-UTF16-H.bcmap +0 -0
  226. package/client/dist/pdfjs/cmaps/UniGB-UTF16-V.bcmap +0 -0
  227. package/client/dist/pdfjs/cmaps/UniGB-UTF32-H.bcmap +0 -0
  228. package/client/dist/pdfjs/cmaps/UniGB-UTF32-V.bcmap +0 -0
  229. package/client/dist/pdfjs/cmaps/UniGB-UTF8-H.bcmap +0 -0
  230. package/client/dist/pdfjs/cmaps/UniGB-UTF8-V.bcmap +0 -0
  231. package/client/dist/pdfjs/cmaps/UniJIS-UCS2-H.bcmap +0 -0
  232. package/client/dist/pdfjs/cmaps/UniJIS-UCS2-HW-H.bcmap +0 -0
  233. package/client/dist/pdfjs/cmaps/UniJIS-UCS2-HW-V.bcmap +0 -0
  234. package/client/dist/pdfjs/cmaps/UniJIS-UCS2-V.bcmap +0 -0
  235. package/client/dist/pdfjs/cmaps/UniJIS-UTF16-H.bcmap +0 -0
  236. package/client/dist/pdfjs/cmaps/UniJIS-UTF16-V.bcmap +0 -0
  237. package/client/dist/pdfjs/cmaps/UniJIS-UTF32-H.bcmap +0 -0
  238. package/client/dist/pdfjs/cmaps/UniJIS-UTF32-V.bcmap +0 -0
  239. package/client/dist/pdfjs/cmaps/UniJIS-UTF8-H.bcmap +0 -0
  240. package/client/dist/pdfjs/cmaps/UniJIS-UTF8-V.bcmap +0 -0
  241. package/client/dist/pdfjs/cmaps/UniJIS2004-UTF16-H.bcmap +0 -0
  242. package/client/dist/pdfjs/cmaps/UniJIS2004-UTF16-V.bcmap +0 -0
  243. package/client/dist/pdfjs/cmaps/UniJIS2004-UTF32-H.bcmap +0 -0
  244. package/client/dist/pdfjs/cmaps/UniJIS2004-UTF32-V.bcmap +0 -0
  245. package/client/dist/pdfjs/cmaps/UniJIS2004-UTF8-H.bcmap +0 -0
  246. package/client/dist/pdfjs/cmaps/UniJIS2004-UTF8-V.bcmap +0 -0
  247. package/client/dist/pdfjs/cmaps/UniJISPro-UCS2-HW-V.bcmap +0 -0
  248. package/client/dist/pdfjs/cmaps/UniJISPro-UCS2-V.bcmap +0 -0
  249. package/client/dist/pdfjs/cmaps/UniJISPro-UTF8-V.bcmap +0 -0
  250. package/client/dist/pdfjs/cmaps/UniJISX0213-UTF32-H.bcmap +0 -0
  251. package/client/dist/pdfjs/cmaps/UniJISX0213-UTF32-V.bcmap +0 -0
  252. package/client/dist/pdfjs/cmaps/UniJISX02132004-UTF32-H.bcmap +0 -0
  253. package/client/dist/pdfjs/cmaps/UniJISX02132004-UTF32-V.bcmap +0 -0
  254. package/client/dist/pdfjs/cmaps/UniKS-UCS2-H.bcmap +0 -0
  255. package/client/dist/pdfjs/cmaps/UniKS-UCS2-V.bcmap +0 -0
  256. package/client/dist/pdfjs/cmaps/UniKS-UTF16-H.bcmap +0 -0
  257. package/client/dist/pdfjs/cmaps/UniKS-UTF16-V.bcmap +0 -0
  258. package/client/dist/pdfjs/cmaps/UniKS-UTF32-H.bcmap +0 -0
  259. package/client/dist/pdfjs/cmaps/UniKS-UTF32-V.bcmap +0 -0
  260. package/client/dist/pdfjs/cmaps/UniKS-UTF8-H.bcmap +0 -0
  261. package/client/dist/pdfjs/cmaps/UniKS-UTF8-V.bcmap +0 -0
  262. package/client/dist/pdfjs/cmaps/V.bcmap +0 -0
  263. package/client/dist/pdfjs/cmaps/WP-Symbol.bcmap +0 -0
  264. package/package.json +4 -3
  265. package/server/dist/index.js +2360 -472
  266. package/client/dist/assets/EditorPanel-C4j_LnbI.js +0 -968
  267. package/client/dist/assets/SettingsDialog-BTyTnZYM.css +0 -1
  268. package/client/dist/assets/SettingsDialog-VlpollL1.js +0 -2
  269. package/client/dist/assets/TaskQueuePanel-DPnDhZFb.js +0 -1
  270. package/client/dist/assets/channel-CROBmx9n.js +0 -1
  271. package/client/dist/assets/classDiagram-DTDB5LWJ-D2KSupgp.js +0 -1
  272. package/client/dist/assets/classDiagram-v2-JRS7N3AN-D2KSupgp.js +0 -1
  273. package/client/dist/assets/cssMode-BcZYukVu.js +0 -1
  274. package/client/dist/assets/handlebars-DleyW4LO.js +0 -1
  275. package/client/dist/assets/html-BJ95-ybC.js +0 -1
  276. package/client/dist/assets/htmlMode-Cqq8N0XF.js +0 -1
  277. package/client/dist/assets/index-C1R3qRwd.js +0 -407
  278. package/client/dist/assets/index-D1Z3F2UU.css +0 -1
  279. package/client/dist/assets/javascript-BIGigzp1.js +0 -1
  280. package/client/dist/assets/liquid-C8hVjUZv.js +0 -1
  281. package/client/dist/assets/mdx-DWmYYep4.js +0 -1
  282. package/client/dist/assets/python-AbIuqkub.js +0 -1
  283. package/client/dist/assets/razor-PLr1HLk0.js +0 -1
  284. package/client/dist/assets/stateDiagram-v2-4QOOHH4V-CN8Njcij.js +0 -1
  285. package/client/dist/assets/swimlanesDiagram-VK2B7HYN-p55moRoN.js +0 -8
  286. package/client/dist/assets/typescript-DcMOCMAD.js +0 -1
  287. package/client/dist/assets/xml-xUSiNagm.js +0 -1
  288. package/client/dist/assets/yaml-CSavWraE.js +0 -1
@@ -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";
@@ -14,7 +14,7 @@ import { fileURLToPath as fileURLToPath2 } from "url";
14
14
 
15
15
  // src/routes/sessions.ts
16
16
  import * as os3 from "os";
17
- import { dirname as dirname2 } from "path";
17
+ import { dirname as dirname2, isAbsolute as isAbsolute2 } from "path";
18
18
 
19
19
  // src/services/project-mover.ts
20
20
  import { access as access2, rename, stat } from "fs/promises";
@@ -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 {
@@ -123,12 +140,12 @@ var ProjectMover = class {
123
140
  }
124
141
  return trimmed;
125
142
  }
126
- async assertDirectory(path13, message) {
143
+ async assertDirectory(path13, message2) {
127
144
  try {
128
145
  const stats = await stat(path13);
129
- if (!stats.isDirectory()) throw new Error(message);
146
+ if (!stats.isDirectory()) throw new Error(message2);
130
147
  } catch (error) {
131
- if (isMissingPathError(error)) throw new Error(message);
148
+ if (isMissingPathError(error)) throw new Error(message2);
132
149
  throw error;
133
150
  }
134
151
  }
@@ -758,10 +775,12 @@ async function sessionRoutes(app, options = {}) {
758
775
  if (!profileId) return reply.status(400).send({ error: "profileId is required" });
759
776
  const owner = { type: "profile", id: profileId };
760
777
  const idsByGroup = options.pinStore.listSessionIdsByGroup(owner);
778
+ const filesByGroup = options.pinStore.listFilePathsByGroup(profileId);
761
779
  return {
762
780
  groups: options.pinStore.listGroups(owner).map((group) => ({
763
781
  ...group,
764
- sessionIds: idsByGroup.get(group.id) || []
782
+ sessionIds: idsByGroup.get(group.id) || [],
783
+ filePaths: filesByGroup.get(group.id) || []
765
784
  }))
766
785
  };
767
786
  });
@@ -775,6 +794,26 @@ async function sessionRoutes(app, options = {}) {
775
794
  return reply.status(400).send({ error: error instanceof Error ? error.message : "Failed to create pin group" });
776
795
  }
777
796
  });
797
+ app.put("/files/pin", async (req, reply) => {
798
+ if (!options.pinStore) return reply.status(503).send({ error: "Session pins are not configured" });
799
+ const { filePath, groupId, profileId } = req.body;
800
+ if (!filePath || !isAbsolute2(filePath) || !groupId || !profileId) {
801
+ return reply.status(400).send({ error: "absolute filePath, groupId, and profileId are required" });
802
+ }
803
+ try {
804
+ options.pinStore.pinFile(profileId, filePath, groupId);
805
+ return { success: true };
806
+ } catch (error) {
807
+ return reply.status(404).send({ error: error instanceof Error ? error.message : "Pin group not found" });
808
+ }
809
+ });
810
+ app.delete("/files/pin", async (req, reply) => {
811
+ if (!options.pinStore) return reply.status(503).send({ error: "Session pins are not configured" });
812
+ const { filePath, profileId } = req.query;
813
+ if (!filePath || !profileId) return reply.status(400).send({ error: "filePath and profileId are required" });
814
+ options.pinStore.unpinFile(profileId, filePath);
815
+ return { success: true };
816
+ });
778
817
  app.put("/:id/pin", async (req, reply) => {
779
818
  if (!options.pinStore) return reply.status(503).send({ error: "Session pins are not configured" });
780
819
  const { id } = req.params;
@@ -806,9 +845,11 @@ async function sessionRoutes(app, options = {}) {
806
845
  }, worktreeMetadata)]));
807
846
  const owner = { type: "profile", id: profileId };
808
847
  const idsByGroup = options.pinStore.listSessionIdsByGroup(owner);
848
+ const filesByGroup = options.pinStore.listFilePathsByGroup(profileId);
809
849
  return {
810
850
  groups: options.pinStore.listGroups(owner).map((group) => ({
811
851
  ...group,
852
+ filePaths: filesByGroup.get(group.id) || [],
812
853
  sessions: (idsByGroup.get(group.id) || []).map((id) => sessionsById.get(id)).filter((session) => Boolean(session)).map(toSessionListItem)
813
854
  }))
814
855
  };
@@ -1037,9 +1078,9 @@ async function sessionRoutes(app, options = {}) {
1037
1078
  skippedSessionFiles: sessionResult.skipped
1038
1079
  };
1039
1080
  } catch (error) {
1040
- const message = error instanceof Error ? error.message : "Failed to move project";
1041
- const status = message.includes("already exists") || message.includes("streaming") ? 409 : 400;
1042
- return reply.status(status).send({ error: message });
1081
+ const message2 = error instanceof Error ? error.message : "Failed to move project";
1082
+ const status = message2.includes("already exists") || message2.includes("streaming") ? 409 : 400;
1083
+ return reply.status(status).send({ error: message2 });
1043
1084
  }
1044
1085
  });
1045
1086
  app.post("/relocate-project", async (req, reply) => {
@@ -1067,8 +1108,8 @@ async function sessionRoutes(app, options = {}) {
1067
1108
  sessionService.invalidateSessionListCache();
1068
1109
  return { success: true, moved: result.moved, skipped: result.skipped };
1069
1110
  } catch (error) {
1070
- const message = error instanceof Error ? error.message : "Failed to relocate project sessions";
1071
- return reply.status(message.includes("already exists") ? 409 : 400).send({ error: message });
1111
+ const message2 = error instanceof Error ? error.message : "Failed to relocate project sessions";
1112
+ return reply.status(message2.includes("already exists") ? 409 : 400).send({ error: message2 });
1072
1113
  }
1073
1114
  });
1074
1115
  app.post("/:id/relocate", async (req, reply) => {
@@ -1104,8 +1145,8 @@ async function sessionRoutes(app, options = {}) {
1104
1145
  sessionService.invalidateSessionListCache();
1105
1146
  return { success: true, path: result.destinationPath, cwd: newCwd };
1106
1147
  } catch (error) {
1107
- const message = error instanceof Error ? error.message : "Failed to move session";
1108
- return reply.status(message.includes("already exists") ? 409 : 400).send({ error: message });
1148
+ const message2 = error instanceof Error ? error.message : "Failed to move session";
1149
+ return reply.status(message2.includes("already exists") ? 409 : 400).send({ error: message2 });
1109
1150
  }
1110
1151
  });
1111
1152
  app.post("/:id/resume", async (req) => {
@@ -1162,8 +1203,8 @@ async function sessionRoutes(app, options = {}) {
1162
1203
  try {
1163
1204
  return await sessionService.updateSessionSkillPolicy(clientId, id, mode, skills || []);
1164
1205
  } catch (error) {
1165
- const message = error instanceof Error ? error.message : "Failed to update session skills";
1166
- return reply.status(message.includes("streaming") ? 409 : 404).send({ error: message });
1206
+ const message2 = error instanceof Error ? error.message : "Failed to update session skills";
1207
+ return reply.status(message2.includes("streaming") ? 409 : 404).send({ error: message2 });
1167
1208
  }
1168
1209
  });
1169
1210
  app.post("/:id/model", async (req, reply) => {
@@ -1175,8 +1216,8 @@ async function sessionRoutes(app, options = {}) {
1175
1216
  try {
1176
1217
  return await sessionService.setSessionModel(clientId, id, provider, modelId);
1177
1218
  } catch (error) {
1178
- const message = error instanceof Error ? error.message : "Failed to change model";
1179
- return reply.status(message.includes("streaming") ? 409 : 400).send({ error: message });
1219
+ const message2 = error instanceof Error ? error.message : "Failed to change model";
1220
+ return reply.status(message2.includes("streaming") ? 409 : 400).send({ error: message2 });
1180
1221
  }
1181
1222
  });
1182
1223
  app.post("/:id/thinking-level", async (req, reply) => {
@@ -1188,8 +1229,8 @@ async function sessionRoutes(app, options = {}) {
1188
1229
  try {
1189
1230
  return await sessionService.setSessionThinkingLevel(clientId, id, level);
1190
1231
  } catch (error) {
1191
- const message = error instanceof Error ? error.message : "Failed to change thinking level";
1192
- return reply.status(message.includes("streaming") ? 409 : 400).send({ error: message });
1232
+ const message2 = error instanceof Error ? error.message : "Failed to change thinking level";
1233
+ return reply.status(message2.includes("streaming") ? 409 : 400).send({ error: message2 });
1193
1234
  }
1194
1235
  });
1195
1236
  app.get("/:id/summary", async (req, reply) => {
@@ -1313,6 +1354,7 @@ async function sessionRoutes(app, options = {}) {
1313
1354
  model: foundSession.model,
1314
1355
  thinkingLevel: foundSession.thinkingLevel,
1315
1356
  isStreaming: foundSession.isStreaming,
1357
+ streamingStartedAt: sessionService.getSessionStreamingStartedAt(foundSession.sessionId),
1316
1358
  activity: options.activityStore?.listForSession(foundSession.sessionId) || []
1317
1359
  }, worktreeMetadata);
1318
1360
  }
@@ -1389,8 +1431,8 @@ async function sessionRoutes(app, options = {}) {
1389
1431
  const result = await sessionService.renameSession(clientId, id, name);
1390
1432
  return result;
1391
1433
  } catch (error) {
1392
- const message = error instanceof Error ? error.message : "Failed to rename session";
1393
- return reply.status(message.includes("not found") ? 404 : 400).send({ error: message });
1434
+ const message2 = error instanceof Error ? error.message : "Failed to rename session";
1435
+ return reply.status(message2.includes("not found") ? 404 : 400).send({ error: message2 });
1394
1436
  }
1395
1437
  });
1396
1438
  app.delete("/:id", async (req, reply) => {
@@ -1423,8 +1465,8 @@ var MEMORY_CATEGORIES = ["rule", "preference", "decision", "fact", "pitfall"];
1423
1465
  var MEMORY_STATUSES = ["active", "pending", "archived"];
1424
1466
  var PINNED_APPLICABILITY_MODES = ["always", "matched"];
1425
1467
  var MemoryRouteError = class extends Error {
1426
- constructor(statusCode, message) {
1427
- super(message);
1468
+ constructor(statusCode, message2) {
1469
+ super(message2);
1428
1470
  this.statusCode = statusCode;
1429
1471
  }
1430
1472
  statusCode;
@@ -1544,7 +1586,10 @@ async function memoryRoutes(app) {
1544
1586
  app.post("/extractions/:id/clear", async (req, reply) => {
1545
1587
  try {
1546
1588
  const body = requestBody(req.body);
1547
- const context = await resolveContext(app, body);
1589
+ const context = await resolveContext(app, {
1590
+ clientId: body.clientId,
1591
+ projectPath: body.projectPath
1592
+ });
1548
1593
  const { id } = req.params;
1549
1594
  requireAccessibleRun(app, context, id);
1550
1595
  app.memoryRuntime.store.clearFailedRun(id);
@@ -1750,23 +1795,23 @@ function clampNumber(value, min, max, fallback) {
1750
1795
  return Math.min(max, Math.max(min, Math.trunc(parsed)));
1751
1796
  }
1752
1797
  function sendError(reply, error) {
1753
- const message = error instanceof Error ? error.message : "Memory request failed";
1754
- if (error instanceof MemoryRouteError) return reply.status(error.statusCode).send({ error: message });
1755
- if (/not found/i.test(message)) return reply.status(404).send({ error: message });
1756
- if (/modified|already exists|not pending|not failed|not completed|cannot be requeued|superseded/i.test(message)) {
1757
- return reply.status(409).send({ error: message });
1798
+ const message2 = error instanceof Error ? error.message : "Memory request failed";
1799
+ if (error instanceof MemoryRouteError) return reply.status(error.statusCode).send({ error: message2 });
1800
+ if (/not found/i.test(message2)) return reply.status(404).send({ error: message2 });
1801
+ if (/modified|already exists|not pending|not failed|not completed|cannot be requeued|superseded/i.test(message2)) {
1802
+ return reply.status(409).send({ error: message2 });
1758
1803
  }
1759
- return reply.status(400).send({ error: message });
1804
+ return reply.status(400).send({ error: message2 });
1760
1805
  }
1761
1806
 
1762
1807
  // src/routes/files.ts
1763
1808
  import { spawn } from "child_process";
1764
1809
  import { isUtf8 } from "buffer";
1765
- import { createReadStream as createReadStream2 } from "fs";
1810
+ import { constants as fsConstants, createReadStream as createReadStream2 } from "fs";
1766
1811
  import * as fs2 from "fs/promises";
1767
1812
  import * as path3 from "path";
1768
1813
  import { ZipArchive } from "archiver";
1769
- import { glob } from "glob";
1814
+ import { globIterate } from "glob";
1770
1815
 
1771
1816
  // src/utils/archive-preview.ts
1772
1817
  import { createReadStream } from "node:fs";
@@ -1882,6 +1927,13 @@ function listTarEntries(filePath, gzipped) {
1882
1927
  }
1883
1928
 
1884
1929
  // src/routes/files.ts
1930
+ var MAX_FILE_TREE_DEPTH = 10;
1931
+ var MAX_FILE_TREE_NODES = 5e3;
1932
+ var FILE_TREE_TIMEOUT_MS = 5e3;
1933
+ var MAX_TEXT_FILE_BYTES = 10 * 1024 * 1024;
1934
+ var MAX_FILE_SEARCH_RESULTS = 5e3;
1935
+ var FILE_SEARCH_TIMEOUT_MS = 5e3;
1936
+ var FILE_SEARCH_IGNORES = ["**/.git/**", "**/node_modules/**"];
1885
1937
  var imageMimeTypes = /* @__PURE__ */ new Map([
1886
1938
  [".apng", "image/apng"],
1887
1939
  [".avif", "image/avif"],
@@ -1995,7 +2047,7 @@ async function createTreeNode(dirPath, entry) {
1995
2047
  const rawTarget = await fs2.readlink(fullPath);
1996
2048
  linkTarget = path3.isAbsolute(rawTarget) ? rawTarget : path3.resolve(dirPath, rawTarget);
1997
2049
  try {
1998
- const targetStats = await fs2.stat(fullPath);
2050
+ const targetStats = await fs2.stat(await resolveAllowedExistingPath(fullPath));
1999
2051
  if (targetStats.isDirectory()) {
2000
2052
  isDirectory = true;
2001
2053
  targetType = "directory";
@@ -2020,29 +2072,41 @@ async function createTreeNode(dirPath, entry) {
2020
2072
  }
2021
2073
  return node;
2022
2074
  }
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
- }) {
2075
+ async function buildFileTree(dirPath, depth, currentDepth, options, budget) {
2029
2076
  if (currentDepth >= depth) return [];
2030
- const entries = await fs2.readdir(dirPath, { withFileTypes: true });
2031
2077
  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);
2078
+ const accessDirPath = await resolveAllowedExistingPath(dirPath);
2079
+ const directory = await fs2.opendir(accessDirPath);
2080
+ try {
2081
+ let entry = await directory.read();
2082
+ while (entry !== null) {
2083
+ if (budget.remainingNodes <= 0 || Date.now() >= budget.deadline) {
2084
+ budget.truncated = true;
2085
+ break;
2086
+ }
2087
+ if (!options.includeHidden && entry.name.startsWith(".")) {
2088
+ entry = await directory.read();
2089
+ continue;
2090
+ }
2091
+ if (options.excludeNames.has(entry.name)) {
2092
+ entry = await directory.read();
2093
+ continue;
2094
+ }
2095
+ const node = await createTreeNode(dirPath, entry);
2096
+ if (options.sort === "modified") {
2097
+ node.mtime = (await fs2.lstat(path3.join(accessDirPath, entry.name))).mtimeMs;
2098
+ }
2099
+ if (options.filterType === "all" || options.filterType === node.type) {
2100
+ budget.remainingNodes--;
2101
+ if (node.type === "directory" && !node.isSymlink) {
2102
+ node.children = await buildFileTree(node.path, depth, currentDepth + 1, options, budget);
2103
+ }
2104
+ nodes.push(node);
2105
+ }
2106
+ entry = await directory.read();
2044
2107
  }
2045
- nodes.push(node);
2108
+ } finally {
2109
+ await directory.close();
2046
2110
  }
2047
2111
  return nodes.sort((a, b) => {
2048
2112
  if (options.sort === "modified") {
@@ -2053,7 +2117,7 @@ async function buildFileTree(dirPath, depth, currentDepth = 0, options = {
2053
2117
  });
2054
2118
  }
2055
2119
  async function fileRoutes(app) {
2056
- app.get("/tree", async (req) => {
2120
+ app.get("/tree", async (req, reply) => {
2057
2121
  const {
2058
2122
  path: dirPath,
2059
2123
  depth,
@@ -2062,21 +2126,31 @@ async function fileRoutes(app) {
2062
2126
  exclude,
2063
2127
  sort
2064
2128
  } = req.query;
2129
+ const requestedDepth = depth === void 0 ? 3 : Number(depth);
2130
+ if (!Number.isInteger(requestedDepth) || requestedDepth < 1 || requestedDepth > MAX_FILE_TREE_DEPTH) {
2131
+ return reply.code(400).send({ error: `depth must be an integer between 1 and ${MAX_FILE_TREE_DEPTH}` });
2132
+ }
2065
2133
  const resolvedPath = await resolveAllowedPath(dirPath);
2066
2134
  const excludeNames = new Set(
2067
2135
  (exclude === void 0 ? "node_modules" : exclude).split(",").map((name) => name.trim()).filter(Boolean)
2068
2136
  );
2069
- const tree = await buildFileTree(resolvedPath, parseInt(depth || "3"), 0, {
2137
+ const budget = {
2138
+ remainingNodes: MAX_FILE_TREE_NODES,
2139
+ deadline: Date.now() + FILE_TREE_TIMEOUT_MS,
2140
+ truncated: false
2141
+ };
2142
+ const tree = await buildFileTree(resolvedPath, requestedDepth, 0, {
2070
2143
  includeHidden: parseBoolean(hidden, false),
2071
2144
  excludeNames,
2072
2145
  filterType: parseFilterType(type),
2073
2146
  sort: parseTreeSort(sort)
2074
- });
2147
+ }, budget);
2075
2148
  const parentPath = path3.dirname(resolvedPath);
2076
2149
  return {
2077
2150
  path: resolvedPath,
2078
2151
  parentPath: parentPath === resolvedPath ? null : parentPath,
2079
- tree
2152
+ tree,
2153
+ truncated: budget.truncated
2080
2154
  };
2081
2155
  });
2082
2156
  app.get("/download", async (req, reply) => {
@@ -2119,9 +2193,11 @@ async function fileRoutes(app) {
2119
2193
  app.get("/read", async (req, reply) => {
2120
2194
  const { path: filePath } = req.query;
2121
2195
  const resolvedPath = await resolveAllowedPath(filePath);
2196
+ let accessPath;
2122
2197
  let stats;
2123
2198
  try {
2124
- stats = await fs2.stat(resolvedPath);
2199
+ accessPath = await resolveAllowedExistingPath(resolvedPath);
2200
+ stats = await fs2.stat(accessPath);
2125
2201
  } catch (error) {
2126
2202
  if (error.code === "ENOENT") {
2127
2203
  return reply.code(404).send({ error: "File not found", path: resolvedPath });
@@ -2133,14 +2209,14 @@ async function fileRoutes(app) {
2133
2209
  try {
2134
2210
  return {
2135
2211
  path: resolvedPath,
2136
- content: await archivePreview(resolvedPath),
2212
+ content: await archivePreview(accessPath),
2137
2213
  kind: "archive",
2138
2214
  mtime: stats.mtimeMs
2139
2215
  };
2140
2216
  } catch (error) {
2141
- const message = error instanceof Error ? error.message : "Unknown archive error";
2217
+ const message2 = error instanceof Error ? error.message : "Unknown archive error";
2142
2218
  return reply.code(415).send({
2143
- error: `Archive preview failed: ${message}`,
2219
+ error: `Archive preview failed: ${message2}`,
2144
2220
  kind: "binary",
2145
2221
  path: resolvedPath,
2146
2222
  mtime: stats.mtimeMs
@@ -2165,24 +2241,52 @@ async function fileRoutes(app) {
2165
2241
  mtime: stats.mtimeMs
2166
2242
  });
2167
2243
  }
2168
- let content;
2244
+ let handle;
2169
2245
  try {
2170
- content = await fs2.readFile(resolvedPath);
2246
+ handle = await fs2.open(accessPath, fsConstants.O_RDONLY | (fsConstants.O_NOFOLLOW ?? 0));
2247
+ const openedStats = await handle.stat();
2248
+ if (openedStats.size > MAX_TEXT_FILE_BYTES) {
2249
+ return reply.code(413).send({
2250
+ error: "File is too large to open as text",
2251
+ maxBytes: MAX_TEXT_FILE_BYTES,
2252
+ path: resolvedPath
2253
+ });
2254
+ }
2255
+ const chunks = [];
2256
+ let totalBytes = 0;
2257
+ while (true) {
2258
+ const remainingBytes = MAX_TEXT_FILE_BYTES + 1 - totalBytes;
2259
+ const buffer = Buffer.allocUnsafe(Math.min(64 * 1024, remainingBytes));
2260
+ const { bytesRead } = await handle.read(buffer, 0, buffer.byteLength, null);
2261
+ if (bytesRead === 0) break;
2262
+ totalBytes += bytesRead;
2263
+ if (totalBytes > MAX_TEXT_FILE_BYTES) {
2264
+ return reply.code(413).send({
2265
+ error: "File is too large to open as text",
2266
+ maxBytes: MAX_TEXT_FILE_BYTES,
2267
+ path: resolvedPath
2268
+ });
2269
+ }
2270
+ chunks.push(buffer.subarray(0, bytesRead));
2271
+ }
2272
+ const content = Buffer.concat(chunks, totalBytes);
2273
+ if (isBinaryFile(resolvedPath, content)) {
2274
+ return reply.code(415).send({
2275
+ error: "Unsupported file type",
2276
+ kind: "binary",
2277
+ path: resolvedPath,
2278
+ mtime: openedStats.mtimeMs
2279
+ });
2280
+ }
2281
+ return { path: resolvedPath, content: content.toString("utf-8"), mtime: openedStats.mtimeMs };
2171
2282
  } catch (error) {
2172
2283
  if (error.code === "ENOENT") {
2173
2284
  return reply.code(404).send({ error: "File not found", path: resolvedPath });
2174
2285
  }
2175
2286
  throw error;
2287
+ } finally {
2288
+ await handle?.close();
2176
2289
  }
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
2290
  });
2187
2291
  app.get("/preview-asset", async (req, reply) => {
2188
2292
  const { root, path: relativePath } = req.query;
@@ -2223,14 +2327,22 @@ async function fileRoutes(app) {
2223
2327
  if (!imageMimeType && !isPdf) {
2224
2328
  return reply.code(415).send({ error: "Unsupported file type" });
2225
2329
  }
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'");
2330
+ let handle;
2331
+ try {
2332
+ const accessPath = await resolveAllowedExistingPath(resolvedPath);
2333
+ handle = await fs2.open(accessPath, fsConstants.O_RDONLY | (fsConstants.O_NOFOLLOW ?? 0));
2334
+ const stream = handle.createReadStream();
2335
+ handle = void 0;
2336
+ if (isPdf) {
2337
+ return reply.type("application/pdf").send(stream);
2338
+ }
2339
+ if (imageMimeType === "image/svg+xml") {
2340
+ reply.header("Content-Security-Policy", "sandbox; default-src 'none'");
2341
+ }
2342
+ return reply.type(imageMimeType).send(stream);
2343
+ } finally {
2344
+ await handle?.close();
2232
2345
  }
2233
- return reply.type(imageMimeType).send(content);
2234
2346
  });
2235
2347
  app.post("/write", async (req) => {
2236
2348
  const { path: filePath, content } = req.body;
@@ -2384,11 +2496,45 @@ async function fileRoutes(app) {
2384
2496
  });
2385
2497
  return { success: true };
2386
2498
  });
2387
- app.get("/search", async (req) => {
2499
+ app.get("/search", async (req, reply) => {
2388
2500
  const { pattern, path: searchPath } = req.query;
2501
+ if (!pattern?.trim()) {
2502
+ return reply.code(400).send({ error: "Search pattern is required" });
2503
+ }
2504
+ const normalizedPattern = pattern.replace(/\\/g, "/");
2505
+ const patternSegments = normalizedPattern.split("/");
2506
+ const unsafeMagic = ["{", "}", "[", "]"].some((character) => normalizedPattern.includes(character)) || /[?*+@!]\(/.test(normalizedPattern);
2507
+ const unsafeDotSegment = patternSegments.some((segment) => segment === ".." || segment.includes(".") && /^[.*?]+$/.test(segment));
2508
+ if (path3.isAbsolute(pattern) || unsafeMagic || unsafeDotSegment) {
2509
+ return reply.code(400).send({ error: "Search pattern must stay within the requested path" });
2510
+ }
2389
2511
  const resolvedSearchPath = await resolveAllowedPath(searchPath || ".");
2390
- const files = await glob(pattern, { cwd: resolvedSearchPath });
2391
- return { files };
2512
+ const files = [];
2513
+ const signal = AbortSignal.timeout(FILE_SEARCH_TIMEOUT_MS);
2514
+ try {
2515
+ for await (const file of globIterate(normalizedPattern, {
2516
+ cwd: resolvedSearchPath,
2517
+ root: resolvedSearchPath,
2518
+ ignore: FILE_SEARCH_IGNORES,
2519
+ nodir: true,
2520
+ signal
2521
+ })) {
2522
+ const relativePath = path3.relative(resolvedSearchPath, path3.resolve(resolvedSearchPath, file));
2523
+ if (relativePath.startsWith("..") || path3.isAbsolute(relativePath)) {
2524
+ return reply.code(400).send({ error: "Search pattern must stay within the requested path" });
2525
+ }
2526
+ if (files.length === MAX_FILE_SEARCH_RESULTS) {
2527
+ return { files, truncated: true };
2528
+ }
2529
+ files.push(file);
2530
+ }
2531
+ } catch (error) {
2532
+ if (signal.aborted) {
2533
+ return reply.code(408).send({ error: "File search timed out" });
2534
+ }
2535
+ throw error;
2536
+ }
2537
+ return { files, truncated: false };
2392
2538
  });
2393
2539
  }
2394
2540
 
@@ -2674,6 +2820,205 @@ async function changelogRoutes(app) {
2674
2820
  });
2675
2821
  }
2676
2822
 
2823
+ // src/services/speech-to-text.ts
2824
+ var DEFAULT_BASE_URL = "https://api.openai.com/v1";
2825
+ var DEFAULT_MODEL = "gpt-4o-mini-transcribe";
2826
+ var MAX_TRANSCRIPTION_ATTEMPTS = 3;
2827
+ var RETRY_DELAY_MS = 500;
2828
+ var MAX_AUDIO_BYTES = 25 * 1024 * 1024;
2829
+ function audioExtension(contentType) {
2830
+ if (contentType.includes("amr")) return "amr";
2831
+ if (contentType.includes("mp4")) return "m4a";
2832
+ if (contentType.includes("mpeg")) return "mp3";
2833
+ if (contentType.includes("ogg")) return "ogg";
2834
+ if (contentType.includes("wav")) return "wav";
2835
+ return "webm";
2836
+ }
2837
+ function isRetryableStatus(status) {
2838
+ return status === 429 || status >= 500;
2839
+ }
2840
+ function wait(ms) {
2841
+ return new Promise((resolve10) => setTimeout(resolve10, ms));
2842
+ }
2843
+ function isSpeechToTextConfigured() {
2844
+ return Boolean(process.env.PI_CLOUD_STT_API_KEY?.trim() || process.env.OPENAI_API_KEY?.trim());
2845
+ }
2846
+ async function transcribeAudio(audio, contentType, options = {}) {
2847
+ const apiKey = process.env.PI_CLOUD_STT_API_KEY?.trim() || process.env.OPENAI_API_KEY?.trim();
2848
+ if (!apiKey) throw new Error("Speech-to-text is not configured.");
2849
+ const baseUrl = (process.env.PI_CLOUD_STT_BASE_URL?.trim() || DEFAULT_BASE_URL).replace(/\/+$/, "");
2850
+ const model = process.env.PI_CLOUD_STT_MODEL?.trim() || DEFAULT_MODEL;
2851
+ const language = process.env.PI_CLOUD_STT_LANGUAGE?.trim();
2852
+ const fetchImpl = options.fetch ?? fetch;
2853
+ const retryDelayMs = options.retryDelayMs ?? RETRY_DELAY_MS;
2854
+ const form = new FormData();
2855
+ form.append("file", new Blob([audio], { type: contentType }), `recording.${audioExtension(contentType)}`);
2856
+ form.append("model", model);
2857
+ if (language) form.append("language", language);
2858
+ for (let attempt = 1; attempt <= MAX_TRANSCRIPTION_ATTEMPTS; attempt += 1) {
2859
+ let response;
2860
+ try {
2861
+ response = await fetchImpl(`${baseUrl}/audio/transcriptions`, {
2862
+ method: "POST",
2863
+ headers: { Authorization: `Bearer ${apiKey}` },
2864
+ body: form
2865
+ });
2866
+ } catch (error) {
2867
+ if (attempt === MAX_TRANSCRIPTION_ATTEMPTS) {
2868
+ throw new Error("Unable to reach the speech-to-text provider.", { cause: error });
2869
+ }
2870
+ await wait(retryDelayMs * attempt);
2871
+ continue;
2872
+ }
2873
+ const result = await response.json().catch(() => ({}));
2874
+ if (!response.ok) {
2875
+ if (isRetryableStatus(response.status) && attempt < MAX_TRANSCRIPTION_ATTEMPTS) {
2876
+ await wait(retryDelayMs * attempt);
2877
+ continue;
2878
+ }
2879
+ throw new Error(typeof result.error?.message === "string" ? result.error.message : "Transcription failed.");
2880
+ }
2881
+ if (typeof result.text !== "string") throw new Error("The speech-to-text provider returned an invalid response.");
2882
+ return result.text;
2883
+ }
2884
+ throw new Error("Unable to reach the speech-to-text provider.");
2885
+ }
2886
+
2887
+ // src/services/text-to-speech.ts
2888
+ var DEFAULT_MODEL2 = "mlx-community/Kokoro-82M-bf16";
2889
+ var DEFAULT_VOICE = "af_heart";
2890
+ var DEFAULT_FORMAT = "wav";
2891
+ var MAX_SYNTHESIS_ATTEMPTS = 3;
2892
+ var RETRY_DELAY_MS2 = 500;
2893
+ var MAX_TTS_TEXT_LENGTH = 1e5;
2894
+ var FORMAT_CONTENT_TYPES = {
2895
+ aac: "audio/aac",
2896
+ flac: "audio/flac",
2897
+ mp3: "audio/mpeg",
2898
+ ogg: "audio/ogg",
2899
+ opus: "audio/ogg",
2900
+ pcm: "audio/L16",
2901
+ wav: "audio/wav"
2902
+ };
2903
+ function isRetryableStatus2(status) {
2904
+ return status === 429 || status >= 500;
2905
+ }
2906
+ function wait2(ms) {
2907
+ return new Promise((resolve10) => setTimeout(resolve10, ms));
2908
+ }
2909
+ function languageCode(language) {
2910
+ if (language === "zh") return "z";
2911
+ if (language === "en") return "a";
2912
+ return language;
2913
+ }
2914
+ async function providerError(response) {
2915
+ const result = await response.json().catch(() => ({}));
2916
+ if (typeof result.error?.message === "string") return result.error.message;
2917
+ if (typeof result.detail === "string") return result.detail;
2918
+ return "Speech synthesis failed.";
2919
+ }
2920
+ function isTextToSpeechConfigured() {
2921
+ return Boolean(process.env.PI_CLOUD_TTS_BASE_URL?.trim());
2922
+ }
2923
+ async function synthesizeSpeech(text, options = {}) {
2924
+ const baseUrl = process.env.PI_CLOUD_TTS_BASE_URL?.trim().replace(/\/+$/, "");
2925
+ if (!baseUrl) throw new Error("Text-to-speech is not configured.");
2926
+ const model = process.env.PI_CLOUD_TTS_MODEL?.trim() || DEFAULT_MODEL2;
2927
+ const voice = process.env.PI_CLOUD_TTS_VOICE?.trim() || DEFAULT_VOICE;
2928
+ const language = process.env.PI_CLOUD_TTS_LANGUAGE?.trim();
2929
+ const format = process.env.PI_CLOUD_TTS_FORMAT?.trim().toLowerCase() || DEFAULT_FORMAT;
2930
+ const apiKey = process.env.PI_CLOUD_TTS_API_KEY?.trim();
2931
+ const fetchImpl = options.fetch ?? fetch;
2932
+ const retryDelayMs = options.retryDelayMs ?? RETRY_DELAY_MS2;
2933
+ const body = {
2934
+ model,
2935
+ input: text,
2936
+ voice,
2937
+ response_format: format,
2938
+ ...language ? { language } : {},
2939
+ ...language ? { lang_code: languageCode(language) } : {}
2940
+ };
2941
+ for (let attempt = 1; attempt <= MAX_SYNTHESIS_ATTEMPTS; attempt += 1) {
2942
+ let response;
2943
+ try {
2944
+ response = await fetchImpl(`${baseUrl}/audio/speech`, {
2945
+ method: "POST",
2946
+ headers: {
2947
+ "Content-Type": "application/json",
2948
+ ...apiKey ? { Authorization: `Bearer ${apiKey}` } : {}
2949
+ },
2950
+ body: JSON.stringify(body)
2951
+ });
2952
+ } catch (error) {
2953
+ if (attempt === MAX_SYNTHESIS_ATTEMPTS) {
2954
+ throw new Error("Unable to reach the text-to-speech provider.", { cause: error });
2955
+ }
2956
+ await wait2(retryDelayMs * attempt);
2957
+ continue;
2958
+ }
2959
+ if (!response.ok) {
2960
+ const message2 = await providerError(response);
2961
+ if (isRetryableStatus2(response.status) && attempt < MAX_SYNTHESIS_ATTEMPTS) {
2962
+ await wait2(retryDelayMs * attempt);
2963
+ continue;
2964
+ }
2965
+ throw new Error(message2);
2966
+ }
2967
+ const audio = Buffer.from(await response.arrayBuffer());
2968
+ if (audio.length === 0) throw new Error("The text-to-speech provider returned an empty response.");
2969
+ return {
2970
+ audio,
2971
+ contentType: response.headers.get("content-type")?.split(";")[0] || FORMAT_CONTENT_TYPES[format] || "application/octet-stream"
2972
+ };
2973
+ }
2974
+ throw new Error("Unable to reach the text-to-speech provider.");
2975
+ }
2976
+
2977
+ // src/routes/speech.ts
2978
+ async function speechRoutes(app, options = {}) {
2979
+ app.addContentTypeParser(/^audio\//, { parseAs: "buffer", bodyLimit: MAX_AUDIO_BYTES }, (_req, body, done) => {
2980
+ done(null, body);
2981
+ });
2982
+ app.get("/status", async () => ({
2983
+ available: isSpeechToTextConfigured(),
2984
+ ttsAvailable: isTextToSpeechConfigured()
2985
+ }));
2986
+ app.post("/transcribe", async (req, reply) => {
2987
+ if (!isSpeechToTextConfigured()) {
2988
+ return reply.status(503).send({ error: "Speech-to-text is not configured." });
2989
+ }
2990
+ const contentType = req.headers["content-type"]?.split(";")[0]?.trim().toLowerCase() || "";
2991
+ if (!contentType.startsWith("audio/") || !Buffer.isBuffer(req.body) || req.body.length === 0) {
2992
+ return reply.status(400).send({ error: "A non-empty audio request body is required." });
2993
+ }
2994
+ try {
2995
+ return { text: await transcribeAudio(req.body, contentType, options) };
2996
+ } catch (error) {
2997
+ req.log.error({ err: error }, "Speech-to-text request failed");
2998
+ return reply.status(502).send({ error: error instanceof Error ? error.message : "Transcription failed." });
2999
+ }
3000
+ });
3001
+ app.post("/synthesize", async (req, reply) => {
3002
+ if (!isTextToSpeechConfigured()) {
3003
+ return reply.status(503).send({ error: "Text-to-speech is not configured." });
3004
+ }
3005
+ const text = req.body?.text;
3006
+ if (typeof text !== "string" || !text.trim()) {
3007
+ return reply.status(400).send({ error: "Non-empty text is required." });
3008
+ }
3009
+ if (text.length > MAX_TTS_TEXT_LENGTH) {
3010
+ return reply.status(413).send({ error: `Text must not exceed ${MAX_TTS_TEXT_LENGTH} characters.` });
3011
+ }
3012
+ try {
3013
+ const result = await synthesizeSpeech(text, options);
3014
+ return reply.type(result.contentType).send(result.audio);
3015
+ } catch (error) {
3016
+ req.log.error({ err: error }, "Text-to-speech request failed");
3017
+ return reply.status(502).send({ error: error instanceof Error ? error.message : "Speech synthesis failed." });
3018
+ }
3019
+ });
3020
+ }
3021
+
2677
3022
  // src/routes/git.ts
2678
3023
  import { execFile } from "node:child_process";
2679
3024
  import { createHash } from "node:crypto";
@@ -2798,6 +3143,17 @@ async function runGit(cwd, args, maxBuffer3 = MAX_SLASH_COMMAND_OUTPUT_BYTES) {
2798
3143
  throw error;
2799
3144
  }
2800
3145
  }
3146
+ async function runGitNoIndex(cwd, args, maxBuffer3) {
3147
+ try {
3148
+ const { stdout } = await execFileAsync("git", ["diff", "--no-index", ...args], { cwd, maxBuffer: maxBuffer3 });
3149
+ return stdout;
3150
+ } catch (error) {
3151
+ if (isMaxBufferError(error)) throw new OversizedGitOutputError();
3152
+ const result = error;
3153
+ if (result.code === 1) return result.stdout || "";
3154
+ throw error;
3155
+ }
3156
+ }
2801
3157
  async function isGitRepository(cwd) {
2802
3158
  try {
2803
3159
  return (await runGit(cwd, ["rev-parse", "--is-inside-work-tree"])).trim() === "true";
@@ -2874,12 +3230,26 @@ function parseBranchName(name, files) {
2874
3230
  if (!files.length) throw new Error("No git changes to suggest a branch name from");
2875
3231
  return sanitizeBranchName(proposeCommitMessage(files));
2876
3232
  }
2877
- function textFromAssistantMessage(message) {
2878
- return message.content.filter((item) => item.type === "text").map((item) => item.text).join("\n").trim();
3233
+ function textFromAssistantMessage(message2) {
3234
+ return message2.content.filter((item) => item.type === "text").map((item) => item.text).join("\n").trim();
2879
3235
  }
2880
3236
  function joinGitOutput(...parts) {
2881
3237
  return parts.map((part) => part.replace(/^(?:\r?\n)+|(?:\r?\n)+$/g, "")).filter((part) => part.trim()).join("\n\n");
2882
3238
  }
3239
+ async function appendUntrackedDiff(cwd, diff, args, path13, maxBytes) {
3240
+ const pathArgs = path13 ? ["--", path13] : [];
3241
+ const untracked = await runGit(cwd, ["ls-files", "--others", "--exclude-standard", "-z", ...pathArgs]);
3242
+ const files = untracked.split("\0").filter(Boolean);
3243
+ let output = diff;
3244
+ for (const file of files) {
3245
+ const separatorBytes = output.trim() ? 2 : 0;
3246
+ const remainingBytes = maxBytes - Buffer.byteLength(output) - separatorBytes;
3247
+ if (remainingBytes <= 0) throw new OversizedGitOutputError();
3248
+ const fileDiff = await runGitNoIndex(cwd, [...args, "--", "/dev/null", file], remainingBytes);
3249
+ output = joinGitOutput(output, fileDiff);
3250
+ }
3251
+ return output;
3252
+ }
2883
3253
  async function getCombinedDiff(cwd, args, maxBytes = MAX_SLASH_COMMAND_OUTPUT_BYTES) {
2884
3254
  const unstaged = await runGit(cwd, ["diff", ...args], maxBytes);
2885
3255
  const remainingBytes = maxBytes - Buffer.byteLength(unstaged);
@@ -3023,9 +3393,9 @@ async function generateCommitMessageWithAi(sessionService, clientId, cwd, status
3023
3393
  if (response.stopReason === "error") {
3024
3394
  throw new Error(response.errorMessage || "AI commit message generation failed");
3025
3395
  }
3026
- const message = textFromAssistantMessage(response).replace(/^```(?:text)?/i, "").replace(/```$/i, "").trim().replace(/^['"]|['"]$/g, "").trim();
3027
- if (!message) throw new Error("AI did not return a commit message");
3028
- return message;
3396
+ const message2 = textFromAssistantMessage(response).replace(/^```(?:text)?/i, "").replace(/```$/i, "").trim().replace(/^['"]|['"]$/g, "").trim();
3397
+ if (!message2) throw new Error("AI did not return a commit message");
3398
+ return message2;
3029
3399
  }
3030
3400
  function parseChangedRanges(diff) {
3031
3401
  const changes = {};
@@ -3100,7 +3470,7 @@ async function gitRoutes(app, options = {}) {
3100
3470
  }) || { global: {}, project: {}, effective: DEFAULT_COMMIT_MESSAGE_PROMPTS };
3101
3471
  });
3102
3472
  app.get("/status", async (req, reply) => {
3103
- const { cwd, message, stagedOnly } = req.query;
3473
+ const { cwd, message: message2, stagedOnly } = req.query;
3104
3474
  const resolvedCwd = await resolveGitCwd(cwd);
3105
3475
  try {
3106
3476
  if (!await isGitRepository(resolvedCwd)) {
@@ -3115,7 +3485,7 @@ async function gitRoutes(app, options = {}) {
3115
3485
  cwd: resolvedCwd,
3116
3486
  isRepository: true,
3117
3487
  files,
3118
- message: message?.trim() || (files.length ? proposeCommitMessage(files) : ""),
3488
+ message: message2?.trim() || (files.length ? proposeCommitMessage(files) : ""),
3119
3489
  output
3120
3490
  };
3121
3491
  } catch (error) {
@@ -3152,8 +3522,8 @@ async function gitRoutes(app, options = {}) {
3152
3522
  commits: commits.slice(0, GIT_HISTORY_PAGE_SIZE)
3153
3523
  };
3154
3524
  } catch (error) {
3155
- const message = error instanceof Error ? error.message : "Failed to load git history";
3156
- return reply.status(400).send({ error: message });
3525
+ const message2 = error instanceof Error ? error.message : "Failed to load git history";
3526
+ return reply.status(400).send({ error: message2 });
3157
3527
  }
3158
3528
  });
3159
3529
  app.get("/branches", async (req, reply) => {
@@ -3214,8 +3584,8 @@ async function gitRoutes(app, options = {}) {
3214
3584
  return reply.status(400).send({ error: "No git changes to generate a commit message from" });
3215
3585
  }
3216
3586
  const prompts = options.commitMessagePrompts?.get(resolvedCwd).effective || DEFAULT_COMMIT_MESSAGE_PROMPTS;
3217
- const message = await generateCommitMessageWithAi(app.services.sessions, clientId, resolvedCwd, status, diff, prompts);
3218
- return { cwd: resolvedCwd, message, files };
3587
+ const message2 = await generateCommitMessageWithAi(app.services.sessions, clientId, resolvedCwd, status, diff, prompts);
3588
+ return { cwd: resolvedCwd, message: message2, files };
3219
3589
  } catch (error) {
3220
3590
  const errorMessage2 = error instanceof Error ? error.message : "Failed to generate commit message";
3221
3591
  return reply.status(400).send({ error: errorMessage2 });
@@ -3294,8 +3664,8 @@ async function gitRoutes(app, options = {}) {
3294
3664
  app.post("/commit", async (req, reply) => {
3295
3665
  const body = req.body || {};
3296
3666
  const resolvedCwd = await resolveGitCwd(body.cwd);
3297
- const message = body.message?.trim();
3298
- if (!message) {
3667
+ const message2 = body.message?.trim();
3668
+ if (!message2) {
3299
3669
  return reply.status(400).send({ error: "Commit message is required" });
3300
3670
  }
3301
3671
  try {
@@ -3306,12 +3676,12 @@ async function gitRoutes(app, options = {}) {
3306
3676
  if (!files.length) {
3307
3677
  return reply.status(400).send({ error: "No changes to commit" });
3308
3678
  }
3309
- const output = await runGit(resolvedCwd, ["commit", "-m", message]);
3679
+ const output = await runGit(resolvedCwd, ["commit", "-m", message2]);
3310
3680
  const commit = (await runGit(resolvedCwd, ["rev-parse", "HEAD"])).trim();
3311
- recordCommitActivity(options, { sessionId: body.sessionId, cwd: resolvedCwd, message, commit, files, mode: "commit" });
3681
+ recordCommitActivity(options, { sessionId: body.sessionId, cwd: resolvedCwd, message: message2, commit, files, mode: "commit" });
3312
3682
  return {
3313
3683
  cwd: resolvedCwd,
3314
- message,
3684
+ message: message2,
3315
3685
  files,
3316
3686
  commit,
3317
3687
  output
@@ -3322,7 +3692,7 @@ async function gitRoutes(app, options = {}) {
3322
3692
  }
3323
3693
  });
3324
3694
  app.get("/amend-status", async (req, reply) => {
3325
- const { cwd, message } = req.query;
3695
+ const { cwd, message: message2 } = req.query;
3326
3696
  const resolvedCwd = await resolveGitCwd(cwd);
3327
3697
  try {
3328
3698
  await ensureHasCommit(resolvedCwd);
@@ -3333,7 +3703,7 @@ async function gitRoutes(app, options = {}) {
3333
3703
  return {
3334
3704
  cwd: resolvedCwd,
3335
3705
  files: parseStatusFiles(status),
3336
- message: message?.trim() || previousMessage.trim()
3706
+ message: message2?.trim() || previousMessage.trim()
3337
3707
  };
3338
3708
  } catch (error) {
3339
3709
  const errorMessage2 = error instanceof Error ? error.message : "Failed to prepare git amend";
@@ -3343,8 +3713,8 @@ async function gitRoutes(app, options = {}) {
3343
3713
  app.post("/amend", async (req, reply) => {
3344
3714
  const body = req.body || {};
3345
3715
  const resolvedCwd = await resolveGitCwd(body.cwd);
3346
- const message = body.message?.trim();
3347
- if (!message) {
3716
+ const message2 = body.message?.trim();
3717
+ if (!message2) {
3348
3718
  return reply.status(400).send({ error: "Commit message is required" });
3349
3719
  }
3350
3720
  try {
@@ -3352,12 +3722,12 @@ async function gitRoutes(app, options = {}) {
3352
3722
  await runGit(resolvedCwd, ["add", "-A"]);
3353
3723
  const status = await runGit(resolvedCwd, ["status", "--porcelain"]);
3354
3724
  const files = parseStatusFiles(status);
3355
- const output = await runGit(resolvedCwd, ["commit", "--amend", "--allow-empty", "-m", message]);
3725
+ const output = await runGit(resolvedCwd, ["commit", "--amend", "--allow-empty", "-m", message2]);
3356
3726
  const commit = (await runGit(resolvedCwd, ["rev-parse", "HEAD"])).trim();
3357
- recordCommitActivity(options, { sessionId: body.sessionId, cwd: resolvedCwd, message, commit, files, mode: "amend" });
3727
+ recordCommitActivity(options, { sessionId: body.sessionId, cwd: resolvedCwd, message: message2, commit, files, mode: "amend" });
3358
3728
  return {
3359
3729
  cwd: resolvedCwd,
3360
- message,
3730
+ message: message2,
3361
3731
  files,
3362
3732
  commit,
3363
3733
  output
@@ -3377,27 +3747,33 @@ async function gitRoutes(app, options = {}) {
3377
3747
  changes: parseChangedRanges(diff)
3378
3748
  };
3379
3749
  } catch (error) {
3380
- const message = error instanceof Error ? error.message : "Failed to run git diff";
3381
- return reply.status(400).send({ error: message });
3750
+ const message2 = error instanceof Error ? error.message : "Failed to run git diff";
3751
+ return reply.status(400).send({ error: message2 });
3382
3752
  }
3383
3753
  });
3384
3754
  app.get("/diff", async (req, reply) => {
3385
- const { cwd, scope: rawScope, commit: rawCommit, path: path13 } = req.query;
3755
+ const { cwd, scope: rawScope, commit: rawCommit, path: path13, includeUntracked } = req.query;
3386
3756
  const resolvedCwd = await resolveGitCwd(cwd);
3387
3757
  try {
3388
3758
  const commit = rawCommit ? parseCommit(rawCommit) : void 0;
3389
3759
  if (commit) {
3390
3760
  const diff2 = await runGit(resolvedCwd, ["show", "--format=", "--patch", commit]);
3391
3761
  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 };
3762
+ const stat11 = remainingBytes2 > 0 ? await runGit(resolvedCwd, ["show", "--format=", "--stat", commit], remainingBytes2) : "";
3763
+ return { cwd: resolvedCwd, scope: `commit-${commit}`, commit, stat: stat11, diff: diff2 };
3394
3764
  }
3395
3765
  const scope = parseDiffScope(rawScope);
3396
3766
  const pathArgs = path13 ? ["--", path13] : [];
3397
- const diff = await getDiff(resolvedCwd, pathArgs, scope);
3767
+ let diff = await getDiff(resolvedCwd, pathArgs, scope);
3768
+ if (includeUntracked === "true" && scope !== "staged") {
3769
+ diff = await appendUntrackedDiff(resolvedCwd, diff, [], path13, MAX_SLASH_COMMAND_OUTPUT_BYTES);
3770
+ }
3398
3771
  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 };
3772
+ let stat10 = remainingBytes > 0 ? await getDiff(resolvedCwd, ["--stat", ...pathArgs], scope, remainingBytes) : "";
3773
+ if (includeUntracked === "true" && scope !== "staged" && remainingBytes > 0) {
3774
+ stat10 = await appendUntrackedDiff(resolvedCwd, stat10, ["--stat"], path13, remainingBytes);
3775
+ }
3776
+ return { cwd: resolvedCwd, scope, path: path13, stat: stat10, diff };
3401
3777
  } catch (error) {
3402
3778
  if (error instanceof OversizedGitOutputError) {
3403
3779
  return {
@@ -3408,8 +3784,8 @@ async function gitRoutes(app, options = {}) {
3408
3784
  message: error.message
3409
3785
  };
3410
3786
  }
3411
- const message = error instanceof Error ? error.message : "Failed to run git diff";
3412
- return reply.status(400).send({ error: message });
3787
+ const message2 = error instanceof Error ? error.message : "Failed to run git diff";
3788
+ return reply.status(400).send({ error: message2 });
3413
3789
  }
3414
3790
  });
3415
3791
  }
@@ -3587,10 +3963,10 @@ var ProjectTaskStore = class {
3587
3963
  if (!task) throw new ProjectTaskNotFoundError("Task not found");
3588
3964
  return task;
3589
3965
  }
3590
- assertChanged(id, changes, message) {
3966
+ assertChanged(id, changes, message2) {
3591
3967
  if (changes) return;
3592
3968
  if (!this.get(id)) throw new ProjectTaskNotFoundError("Task not found");
3593
- throw new ProjectTaskConflictError(message);
3969
+ throw new ProjectTaskConflictError(message2);
3594
3970
  }
3595
3971
  };
3596
3972
  function isProjectTaskStatus(value) {
@@ -3760,8 +4136,8 @@ async function taskRoutes(app, options) {
3760
4136
  }
3761
4137
  });
3762
4138
  }
3763
- function textFromAssistantMessage2(message) {
3764
- return message.content.filter((item) => item.type === "text").map((item) => item.text).join("\n").trim();
4139
+ function textFromAssistantMessage2(message2) {
4140
+ return message2.content.filter((item) => item.type === "text").map((item) => item.text).join("\n").trim();
3765
4141
  }
3766
4142
  function taskPolishPrompt(title, prompt) {
3767
4143
  return `Polish this project task so it is ready for an AI coding agent to execute.
@@ -3854,11 +4230,11 @@ function optionalString3(value, field) {
3854
4230
  return value.trim() || null;
3855
4231
  }
3856
4232
  function sendTaskError(reply, error, validationStatus = 409) {
3857
- const message = error instanceof Error ? error.message : "Task request failed";
3858
- if (error instanceof ProjectTaskNotFoundError) return reply.status(404).send({ error: message });
3859
- if (error instanceof ProjectTaskConflictError) return reply.status(409).send({ error: message });
3860
- if (error instanceof ProjectTaskValidationError) return reply.status(validationStatus).send({ error: message });
3861
- return reply.status(500).send({ error: message });
4233
+ const message2 = error instanceof Error ? error.message : "Task request failed";
4234
+ if (error instanceof ProjectTaskNotFoundError) return reply.status(404).send({ error: message2 });
4235
+ if (error instanceof ProjectTaskConflictError) return reply.status(409).send({ error: message2 });
4236
+ if (error instanceof ProjectTaskValidationError) return reply.status(validationStatus).send({ error: message2 });
4237
+ return reply.status(500).send({ error: message2 });
3862
4238
  }
3863
4239
 
3864
4240
  // src/routes/feishu-gateway.ts
@@ -3874,8 +4250,8 @@ async function feishuGatewayRoutes(app, options) {
3874
4250
  function sendFeishuGatewayError(reply, error) {
3875
4251
  const maybeStatusCode = error?.statusCode;
3876
4252
  const statusCode = typeof maybeStatusCode === "number" ? maybeStatusCode : 500;
3877
- const message = error instanceof Error ? error.message : "Feishu gateway request failed";
3878
- return reply.status(statusCode).send({ error: message });
4253
+ const message2 = error instanceof Error ? error.message : "Feishu gateway request failed";
4254
+ return reply.status(statusCode).send({ error: message2 });
3879
4255
  }
3880
4256
 
3881
4257
  // src/routes/gateways.ts
@@ -3910,8 +4286,67 @@ async function gatewayRoutes(app, options) {
3910
4286
  });
3911
4287
  }
3912
4288
  function sendGatewayError(reply, error) {
3913
- const message = error instanceof Error ? error.message : "Gateway settings request failed";
3914
- return reply.status(400).send({ error: message });
4289
+ const message2 = error instanceof Error ? error.message : "Gateway settings request failed";
4290
+ return reply.status(400).send({ error: message2 });
4291
+ }
4292
+
4293
+ // src/routes/wecom-gateway.ts
4294
+ async function wecomGatewayRoutes(app, options) {
4295
+ app.addContentTypeParser(["application/xml", "text/xml"], { parseAs: "string" }, (_req, body, done) => done(null, body));
4296
+ app.get("/callback", async (req, reply) => {
4297
+ try {
4298
+ const challenge = options.service.handleVerification(req.query);
4299
+ return reply.type("text/plain; charset=utf-8").send(challenge);
4300
+ } catch (error) {
4301
+ return sendWecomError(reply, error);
4302
+ }
4303
+ });
4304
+ app.post("/callback", async (req, reply) => {
4305
+ try {
4306
+ const acknowledgement = options.service.handleCallback(
4307
+ req.query,
4308
+ typeof req.body === "string" ? req.body : ""
4309
+ );
4310
+ return reply.type("text/plain; charset=utf-8").send(acknowledgement);
4311
+ } catch (error) {
4312
+ return sendWecomError(reply, error);
4313
+ }
4314
+ });
4315
+ app.get("/status", async () => ({ status: options.service.status() }));
4316
+ app.put("/configuration", async (req, reply) => {
4317
+ try {
4318
+ return options.service.saveConfiguration(req.body);
4319
+ } catch (error) {
4320
+ return sendWecomError(reply, error);
4321
+ }
4322
+ });
4323
+ app.delete("/configuration", async (_req, reply) => {
4324
+ try {
4325
+ options.service.disconnect();
4326
+ return { status: options.service.status() };
4327
+ } catch (error) {
4328
+ return sendWecomError(reply, error);
4329
+ }
4330
+ });
4331
+ app.post("/callback-secrets", async (_req, reply) => {
4332
+ try {
4333
+ return options.service.regenerateCallbackSecrets();
4334
+ } catch (error) {
4335
+ return sendWecomError(reply, error);
4336
+ }
4337
+ });
4338
+ app.post("/test", async (_req, reply) => {
4339
+ try {
4340
+ return { status: await options.service.testConnection() };
4341
+ } catch (error) {
4342
+ return sendWecomError(reply, error);
4343
+ }
4344
+ });
4345
+ }
4346
+ function sendWecomError(reply, error) {
4347
+ const statusCode = Number(error?.statusCode) || 400;
4348
+ const message2 = error instanceof Error ? error.message : "WeCom gateway request failed";
4349
+ return reply.status(statusCode).send({ error: message2 });
3915
4350
  }
3916
4351
 
3917
4352
  // src/routes/git-hosting.ts
@@ -3953,15 +4388,15 @@ var GiteaSettingsStore = class {
3953
4388
  return this.get();
3954
4389
  }
3955
4390
  clear() {
3956
- this.db.prepare("DELETE FROM security_settings WHERE key IN (?, ?)").run(SERVER_URL_KEY, TOKEN_KEY);
4391
+ this.db.prepare("DELETE FROM application_settings WHERE key IN (?, ?)").run(SERVER_URL_KEY, TOKEN_KEY);
3957
4392
  }
3958
4393
  value(key) {
3959
- const row = this.db.prepare("SELECT value FROM security_settings WHERE key = ?").get(key);
4394
+ const row = this.db.prepare("SELECT value FROM application_settings WHERE key = ?").get(key);
3960
4395
  return row?.value;
3961
4396
  }
3962
4397
  set(key, value) {
3963
4398
  this.db.prepare(`
3964
- INSERT INTO security_settings (key, value, updated_at)
4399
+ INSERT INTO application_settings (key, value, updated_at)
3965
4400
  VALUES (?, ?, ?)
3966
4401
  ON CONFLICT(key) DO UPDATE SET value = excluded.value, updated_at = excluded.updated_at
3967
4402
  `).run(key, value, (/* @__PURE__ */ new Date()).toISOString());
@@ -4016,7 +4451,7 @@ var GithubSettingsStore = class {
4016
4451
  const value = proxyUrl.trim();
4017
4452
  if (value) new URL(value);
4018
4453
  if (value) this.set(PROXY_URL_KEY, value);
4019
- else this.db.prepare("DELETE FROM security_settings WHERE key = ?").run(PROXY_URL_KEY);
4454
+ else this.db.prepare("DELETE FROM application_settings WHERE key = ?").run(PROXY_URL_KEY);
4020
4455
  return this.get();
4021
4456
  }
4022
4457
  proxyEnv() {
@@ -4024,15 +4459,15 @@ var GithubSettingsStore = class {
4024
4459
  return proxyUrl ? githubProxyEnvFromUrl(proxyUrl) : {};
4025
4460
  }
4026
4461
  clear() {
4027
- this.db.prepare("DELETE FROM security_settings WHERE key IN (?, ?)").run(SERVER_URL_KEY2, TOKEN_KEY2);
4462
+ this.db.prepare("DELETE FROM application_settings WHERE key IN (?, ?)").run(SERVER_URL_KEY2, TOKEN_KEY2);
4028
4463
  }
4029
4464
  value(key) {
4030
- const row = this.db.prepare("SELECT value FROM security_settings WHERE key = ?").get(key);
4465
+ const row = this.db.prepare("SELECT value FROM application_settings WHERE key = ?").get(key);
4031
4466
  return row?.value;
4032
4467
  }
4033
4468
  set(key, value) {
4034
4469
  this.db.prepare(`
4035
- INSERT INTO security_settings (key, value, updated_at)
4470
+ INSERT INTO application_settings (key, value, updated_at)
4036
4471
  VALUES (?, ?, ?)
4037
4472
  ON CONFLICT(key) DO UPDATE SET value = excluded.value, updated_at = excluded.updated_at
4038
4473
  `).run(key, value, (/* @__PURE__ */ new Date()).toISOString());
@@ -4055,8 +4490,8 @@ var maxDiffBuffer = 10 * 1024 * 1024;
4055
4490
  var githubProxyCheckArgs = ["-fsSL", "--connect-timeout", "5", "--max-time", "10", "https://www.google.com/generate_204"];
4056
4491
  var githubProxyCountryArgs = ["-fsSL", "--connect-timeout", "5", "--max-time", "10", "https://ipinfo.io/country"];
4057
4492
  var proxyEnvKeys = ["ALL_PROXY", "HTTP_PROXY", "HTTPS_PROXY", "NO_PROXY", "all_proxy", "http_proxy", "https_proxy", "no_proxy"];
4058
- function textFromAssistantMessage3(message) {
4059
- return message.content.filter((item) => item.type === "text").map((item) => item.text).join("\n").trim();
4493
+ function textFromAssistantMessage3(message2) {
4494
+ return message2.content.filter((item) => item.type === "text").map((item) => item.text).join("\n").trim();
4060
4495
  }
4061
4496
  async function runGit2(cwd, args) {
4062
4497
  const { stdout } = await execFileAsync2("git", args, { cwd, maxBuffer: maxDiffBuffer });
@@ -4386,8 +4821,8 @@ async function gitHostingRoutes(app, options) {
4386
4821
  }
4387
4822
  function sendError2(reply, error, status = 500) {
4388
4823
  const maybeStatus = error?.statusCode;
4389
- const errorStatus2 = typeof maybeStatus === "number" ? maybeStatus : status;
4390
- return reply.status(errorStatus2).send({ error: error instanceof Error ? error.message : "Git integration request failed" });
4824
+ const errorStatus3 = typeof maybeStatus === "number" ? maybeStatus : status;
4825
+ return reply.status(errorStatus3).send({ error: error instanceof Error ? error.message : "Git integration request failed" });
4391
4826
  }
4392
4827
 
4393
4828
  // src/routes/review-sources.ts
@@ -4538,6 +4973,72 @@ async function reviewSourceRoutes(app, options) {
4538
4973
  });
4539
4974
  }
4540
4975
 
4976
+ // src/routes/model-window-kickoffs.ts
4977
+ import { stat as stat4 } from "node:fs/promises";
4978
+ var modelWindowKickoffRoutes = async (app, options) => {
4979
+ app.get("/", async () => ({ kickoffs: options.store.list() }));
4980
+ app.post("/", async (req, reply) => {
4981
+ try {
4982
+ const input = await validateInput(options.sessions, req.body);
4983
+ return reply.status(201).send({ kickoff: options.store.create(input) });
4984
+ } catch (error) {
4985
+ return reply.status(errorStatus(error, 400)).send({ error: message(error) });
4986
+ }
4987
+ });
4988
+ app.put("/:id", async (req, reply) => {
4989
+ try {
4990
+ const input = await validateInput(options.sessions, req.body);
4991
+ return { kickoff: options.store.update(req.params.id, input) };
4992
+ } catch (error) {
4993
+ const fallback = message(error).includes("not found") ? 404 : 400;
4994
+ return reply.status(errorStatus(error, fallback)).send({ error: message(error) });
4995
+ }
4996
+ });
4997
+ app.delete("/:id", async (req, reply) => {
4998
+ try {
4999
+ options.store.delete(req.params.id);
5000
+ return reply.status(204).send();
5001
+ } catch (error) {
5002
+ return reply.status(404).send({ error: message(error) });
5003
+ }
5004
+ });
5005
+ app.get("/notification-channels", async () => ({ channels: options.notifications.list() }));
5006
+ app.post("/notification-channels/wecom", async (req, reply) => {
5007
+ try {
5008
+ return { channel: options.notifications.saveWecom(req.body) };
5009
+ } catch (error) {
5010
+ return reply.status(400).send({ error: message(error) });
5011
+ }
5012
+ });
5013
+ app.delete("/notification-channels/:id", async (req, reply) => {
5014
+ try {
5015
+ options.notifications.delete(req.params.id);
5016
+ return reply.status(204).send();
5017
+ } catch (error) {
5018
+ return reply.status(404).send({ error: message(error) });
5019
+ }
5020
+ });
5021
+ };
5022
+ async function validateInput(sessions, input) {
5023
+ if (!input?.profileId || !input.provider || !input.modelId) throw new Error("Profile, provider, and model are required");
5024
+ const models = await sessions.listAgentProfileModels(input.profileId);
5025
+ if (!models.some((model) => model.provider === input.provider && model.id === input.modelId)) {
5026
+ throw new Error(`Unknown model: ${input.provider}/${input.modelId}`);
5027
+ }
5028
+ const projectPath = await resolveAllowedExistingPath(input.projectPath || "~");
5029
+ if (!(await stat4(projectPath)).isDirectory()) throw new Error("Project path must be a directory");
5030
+ return { ...input, projectPath };
5031
+ }
5032
+ function errorStatus(error, fallback) {
5033
+ return typeof error === "object" && error && "statusCode" in error && typeof error.statusCode === "number" ? error.statusCode : fallback;
5034
+ }
5035
+ function message(error) {
5036
+ return error instanceof Error ? error.message : String(error);
5037
+ }
5038
+
5039
+ // src/routes/auth.ts
5040
+ import rateLimit from "@fastify/rate-limit";
5041
+
4541
5042
  // src/auth/request.ts
4542
5043
  function getRequestContext(req, trustProxy) {
4543
5044
  const forwardedFor = req.headers["x-forwarded-for"];
@@ -4685,17 +5186,70 @@ function mapRow2(row) {
4685
5186
  };
4686
5187
  }
4687
5188
 
5189
+ // src/services/user-prompt-store.ts
5190
+ import { randomUUID as randomUUID3 } from "crypto";
5191
+ var UserPromptStore = class {
5192
+ constructor(db) {
5193
+ this.db = db;
5194
+ }
5195
+ db;
5196
+ list() {
5197
+ return this.db.prepare(`
5198
+ SELECT id, name, content, created_at, updated_at
5199
+ FROM user_prompts
5200
+ ORDER BY lower(name), id
5201
+ `).all().map(mapRow3);
5202
+ }
5203
+ create(input) {
5204
+ const id = randomUUID3();
5205
+ const now = (/* @__PURE__ */ new Date()).toISOString();
5206
+ this.db.prepare(`
5207
+ INSERT INTO user_prompts (id, name, content, created_at, updated_at)
5208
+ VALUES (?, ?, ?, ?, ?)
5209
+ `).run(id, input.name.trim(), input.content.trim(), now, now);
5210
+ return this.get(id);
5211
+ }
5212
+ update(id, input) {
5213
+ const result = this.db.prepare(`
5214
+ UPDATE user_prompts SET name = ?, content = ?, updated_at = ?
5215
+ WHERE id = ?
5216
+ `).run(input.name.trim(), input.content.trim(), (/* @__PURE__ */ new Date()).toISOString(), id);
5217
+ if (!result.changes) throw new Error(`User prompt not found: ${id}`);
5218
+ return this.get(id);
5219
+ }
5220
+ delete(id) {
5221
+ this.db.prepare("DELETE FROM user_prompts WHERE id = ?").run(id);
5222
+ }
5223
+ get(id) {
5224
+ const row = this.db.prepare(`
5225
+ SELECT id, name, content, created_at, updated_at
5226
+ FROM user_prompts WHERE id = ?
5227
+ `).get(id);
5228
+ return row ? mapRow3(row) : null;
5229
+ }
5230
+ };
5231
+ function mapRow3(row) {
5232
+ return {
5233
+ id: row.id,
5234
+ name: row.name,
5235
+ content: row.content,
5236
+ createdAt: row.created_at,
5237
+ updatedAt: row.updated_at
5238
+ };
5239
+ }
5240
+
4688
5241
  // src/routes/auth.ts
4689
5242
  async function authRoutes(app, options) {
4690
5243
  const { config, sessions, audit, totp, rateLimiter, db } = options;
4691
5244
  const presetStore = new SkillPresetStore(db);
5245
+ const userPromptStore = new UserPromptStore(db);
4692
5246
  function getPreferenceValue(key) {
4693
- const row = db.prepare("SELECT value FROM security_settings WHERE key = ?").get(key);
5247
+ const row = db.prepare("SELECT value FROM application_settings WHERE key = ?").get(key);
4694
5248
  return row?.value;
4695
5249
  }
4696
5250
  function setPreferenceValue(key, value) {
4697
5251
  db.prepare(`
4698
- INSERT INTO security_settings (key, value, updated_at)
5252
+ INSERT INTO application_settings (key, value, updated_at)
4699
5253
  VALUES (?, ?, ?)
4700
5254
  ON CONFLICT(key) DO UPDATE SET value = excluded.value, updated_at = excluded.updated_at
4701
5255
  `).run(key, value, (/* @__PURE__ */ new Date()).toISOString());
@@ -4742,7 +5296,8 @@ async function authRoutes(app, options) {
4742
5296
  autoExtractMemory: getPreferenceValue("memory.autoExtract") === "true",
4743
5297
  theme: getTheme(),
4744
5298
  language: getLanguage(),
4745
- soundNotification: getSoundNotification()
5299
+ soundNotification: getSoundNotification(),
5300
+ autoSpeakAssistant: getPreferenceValue("ui.autoSpeakAssistant") === "true"
4746
5301
  };
4747
5302
  }
4748
5303
  function requireAuth(req, reply) {
@@ -4768,6 +5323,13 @@ async function authRoutes(app, options) {
4768
5323
  }
4769
5324
  return { name, mode, skills };
4770
5325
  }
5326
+ function parseUserPromptBody(body) {
5327
+ const name = typeof body.name === "string" ? body.name.trim() : "";
5328
+ const content = typeof body.content === "string" ? body.content.trim() : "";
5329
+ if (!name) throw new Error("name is required");
5330
+ if (!content) throw new Error("content is required");
5331
+ return { name, content };
5332
+ }
4771
5333
  function isUniqueConstraintError(error) {
4772
5334
  return error instanceof Error && /unique/i.test(error.message);
4773
5335
  }
@@ -4777,6 +5339,12 @@ async function authRoutes(app, options) {
4777
5339
  }
4778
5340
  return reply.status(400).send({ error: error instanceof Error ? error.message : "Invalid preset payload" });
4779
5341
  }
5342
+ function sendUserPromptError(reply, error) {
5343
+ if (isUniqueConstraintError(error)) {
5344
+ return reply.status(409).send({ error: "A prompt with that name already exists" });
5345
+ }
5346
+ return reply.status(400).send({ error: error instanceof Error ? error.message : "Invalid prompt payload" });
5347
+ }
4780
5348
  app.get("/me", async (req, reply) => {
4781
5349
  const session = renewSessionFromRequest(req, reply, config, sessions);
4782
5350
  return {
@@ -4855,8 +5423,9 @@ async function authRoutes(app, options) {
4855
5423
  const hasTheme = hasPreference(body, "theme");
4856
5424
  const hasLanguage = hasPreference(body, "language");
4857
5425
  const hasSoundNotification = hasPreference(body, "soundNotification");
5426
+ const hasAutoSpeakAssistant = hasPreference(body, "autoSpeakAssistant");
4858
5427
  const hasGitCloneParentPath = hasPreference(body, "gitCloneParentPath");
4859
- if (!hasShowHintInfo && !hasShowCodeBlockLanguageHeaders && !hasStreamingMessageBehavior && !hasEditorAutoRefresh && !hasConfirmSessionDelete && !hasNewSessionShortcut && !hasFullscreenShortcut && !hasShowGoToTopButton && !hasShowChatViewOptionsButton && !hasAutoExtractMemory && !hasTheme && !hasLanguage && !hasSoundNotification && !hasGitCloneParentPath) {
5428
+ if (!hasShowHintInfo && !hasShowCodeBlockLanguageHeaders && !hasStreamingMessageBehavior && !hasEditorAutoRefresh && !hasConfirmSessionDelete && !hasNewSessionShortcut && !hasFullscreenShortcut && !hasShowGoToTopButton && !hasShowChatViewOptionsButton && !hasAutoExtractMemory && !hasTheme && !hasLanguage && !hasSoundNotification && !hasAutoSpeakAssistant && !hasGitCloneParentPath) {
4860
5429
  return reply.status(400).send({ error: "At least one preference must be provided" });
4861
5430
  }
4862
5431
  if (hasShowHintInfo && typeof body.showHintInfo !== "boolean") {
@@ -4898,6 +5467,9 @@ async function authRoutes(app, options) {
4898
5467
  if (hasSoundNotification && body.soundNotification !== "off" && body.soundNotification !== "beep" && body.soundNotification !== "chime" && body.soundNotification !== "ding") {
4899
5468
  return reply.status(400).send({ error: "soundNotification must be off, beep, chime, or ding" });
4900
5469
  }
5470
+ if (hasAutoSpeakAssistant && typeof body.autoSpeakAssistant !== "boolean") {
5471
+ return reply.status(400).send({ error: "autoSpeakAssistant must be a boolean" });
5472
+ }
4901
5473
  if (hasGitCloneParentPath && (typeof body.gitCloneParentPath !== "string" || !body.gitCloneParentPath.trim())) {
4902
5474
  return reply.status(400).send({ error: "gitCloneParentPath must be a non-empty string" });
4903
5475
  }
@@ -4914,6 +5486,7 @@ async function authRoutes(app, options) {
4914
5486
  if (hasTheme) setPreferenceValue("ui.theme", String(body.theme));
4915
5487
  if (hasLanguage) setPreferenceValue("ui.language", String(body.language));
4916
5488
  if (hasSoundNotification) setPreferenceValue("ui.soundNotification", String(body.soundNotification));
5489
+ if (hasAutoSpeakAssistant) setPreferenceValue("ui.autoSpeakAssistant", String(body.autoSpeakAssistant));
4917
5490
  if (hasGitCloneParentPath) setPreferenceValue("ui.gitCloneParentPath", String(body.gitCloneParentPath).trim());
4918
5491
  return getPreferences();
4919
5492
  });
@@ -4954,6 +5527,47 @@ async function authRoutes(app, options) {
4954
5527
  presetStore.delete(req.params.id, session.username);
4955
5528
  return { success: true };
4956
5529
  });
5530
+ await app.register(async (userPromptRoutes) => {
5531
+ await userPromptRoutes.register(rateLimit, {
5532
+ max: 100,
5533
+ timeWindow: "1 minute",
5534
+ keyGenerator: (req) => getRequestContext(req, config.trustProxy).ip
5535
+ });
5536
+ userPromptRoutes.get("/user-prompts", async (req, reply) => {
5537
+ const session = requireAuth(req, reply);
5538
+ if (!session) return;
5539
+ return { prompts: userPromptStore.list() };
5540
+ });
5541
+ userPromptRoutes.post("/user-prompts", async (req, reply) => {
5542
+ const session = requireAuth(req, reply);
5543
+ if (!session) return;
5544
+ try {
5545
+ return { prompt: userPromptStore.create(parseUserPromptBody(req.body || {})) };
5546
+ } catch (error) {
5547
+ return sendUserPromptError(reply, error);
5548
+ }
5549
+ });
5550
+ userPromptRoutes.patch("/user-prompts/:id", async (req, reply) => {
5551
+ const session = requireAuth(req, reply);
5552
+ if (!session) return;
5553
+ try {
5554
+ return {
5555
+ prompt: userPromptStore.update(
5556
+ req.params.id,
5557
+ parseUserPromptBody(req.body || {})
5558
+ )
5559
+ };
5560
+ } catch (error) {
5561
+ return sendUserPromptError(reply, error);
5562
+ }
5563
+ });
5564
+ userPromptRoutes.delete("/user-prompts/:id", async (req, reply) => {
5565
+ const session = requireAuth(req, reply);
5566
+ if (!session) return;
5567
+ userPromptStore.delete(req.params.id);
5568
+ return { success: true };
5569
+ });
5570
+ });
4957
5571
  app.post("/2fa/setup", async (req, reply) => {
4958
5572
  const session = requireAuth(req, reply);
4959
5573
  if (!session) return;
@@ -5037,8 +5651,8 @@ function isAllowedRequestOrigin(req) {
5037
5651
  var IMAGE_MIME_TYPES = /* @__PURE__ */ new Set(["image/png", "image/jpeg", "image/webp", "image/gif"]);
5038
5652
  var MAX_IMAGE_COUNT = 4;
5039
5653
  var MAX_IMAGE_BYTES = 10 * 1024 * 1024;
5040
- function invalidImage(message = "The image could not be read. Try selecting it again.") {
5041
- return { ok: false, code: "image_malformed", message };
5654
+ function invalidImage(message2 = "The image could not be read. Try selecting it again.") {
5655
+ return { ok: false, code: "image_malformed", message: message2 };
5042
5656
  }
5043
5657
  function sniffImageMimeType(bytes) {
5044
5658
  if (bytes.subarray(0, 3).equals(Buffer.from([255, 216, 255]))) return "image/jpeg";
@@ -5129,10 +5743,10 @@ function extensionForMimeType(mimeType) {
5129
5743
  }
5130
5744
 
5131
5745
  // src/ws/safe-send.ts
5132
- function sendJson(socket, message) {
5746
+ function sendJson(socket, message2) {
5133
5747
  if (socket.readyState !== socket.OPEN) return;
5134
5748
  try {
5135
- socket.send(JSON.stringify(message));
5749
+ socket.send(JSON.stringify(message2));
5136
5750
  } catch {
5137
5751
  }
5138
5752
  }
@@ -5205,6 +5819,7 @@ function formatCompactResult(result, status) {
5205
5819
  }
5206
5820
  async function chatWebSocket(app) {
5207
5821
  const sessionService = app.services.sessions;
5822
+ const clients = /* @__PURE__ */ new Set();
5208
5823
  app.get("/ws/chat", { websocket: true }, (socket, req) => {
5209
5824
  const auth = app.authServices;
5210
5825
  if (!isAllowedRequestOrigin(req)) {
@@ -5232,7 +5847,17 @@ async function chatWebSocket(app) {
5232
5847
  return;
5233
5848
  }
5234
5849
  const safeClientId = clientId;
5850
+ const connectedClient = { socket, watchedSessionId: void 0 };
5851
+ clients.add(connectedClient);
5235
5852
  console.log(`Client connected: ${safeClientId}`);
5853
+ sessionService.cancelCleanup(safeClientId);
5854
+ function broadcastToSession(sessionId, message2) {
5855
+ for (const client of clients) {
5856
+ if (client === connectedClient || client.watchedSessionId === sessionId) {
5857
+ sendJson(client.socket, message2);
5858
+ }
5859
+ }
5860
+ }
5236
5861
  function sendPromptPreflight(sessionId, requestId, accepted, error, imagePaths) {
5237
5862
  sendJson(socket, {
5238
5863
  type: "prompt_preflight",
@@ -5296,10 +5921,24 @@ async function chatWebSocket(app) {
5296
5921
  }
5297
5922
  socket.on("message", async (rawData) => {
5298
5923
  try {
5299
- const message = JSON.parse(rawData.toString());
5300
- switch (message.type) {
5924
+ const message2 = JSON.parse(rawData.toString());
5925
+ switch (message2.type) {
5926
+ case "watch": {
5927
+ connectedClient.watchedSessionId = message2.payload?.sessionId;
5928
+ const targetSessionId = connectedClient.watchedSessionId;
5929
+ if (targetSessionId) {
5930
+ const isStreaming = sessionService.isSessionStreaming(targetSessionId);
5931
+ sendJson(socket, {
5932
+ type: "streaming_state",
5933
+ sessionId: targetSessionId,
5934
+ isStreaming,
5935
+ ...isStreaming ? { streamingStartedAt: sessionService.getSessionStreamingStartedAt(targetSessionId) } : {}
5936
+ });
5937
+ }
5938
+ break;
5939
+ }
5301
5940
  case "prompt": {
5302
- const session2 = await ensureTargetSession(message.payload?.sessionId);
5941
+ const session2 = await ensureTargetSession(message2.payload?.sessionId);
5303
5942
  if (!session2) {
5304
5943
  sendJson(socket, {
5305
5944
  type: "error",
@@ -5307,14 +5946,16 @@ async function chatWebSocket(app) {
5307
5946
  });
5308
5947
  return;
5309
5948
  }
5310
- const imageResult = validateImages(message.payload?.images, session2.model);
5949
+ const imageResult = validateImages(message2.payload?.images, session2.model);
5311
5950
  if (!imageResult.ok) {
5312
- sendPromptPreflight(session2.sessionId, message.payload?.requestId, false, imageResult);
5951
+ sendPromptPreflight(session2.sessionId, message2.payload?.requestId, false, imageResult);
5313
5952
  return;
5314
5953
  }
5315
5954
  sessionService.cancelCleanup(safeClientId);
5316
5955
  const sessionId = session2.sessionId;
5317
- const compactInstructions = parseCompactCommand(message.payload?.text || "");
5956
+ connectedClient.watchedSessionId = sessionId;
5957
+ const streamingStartedAt = sessionService.markSessionStreamingStarted(sessionId);
5958
+ const compactInstructions = parseCompactCommand(message2.payload?.text || "");
5318
5959
  req.log?.info({
5319
5960
  clientId: safeClientId,
5320
5961
  sessionId,
@@ -5338,7 +5979,7 @@ async function chatWebSocket(app) {
5338
5979
  if (recent.every((t) => t.name === recent[0].name && t.input === recent[0].input)) {
5339
5980
  loopDetected = true;
5340
5981
  console.warn(`[loop-detected] tool="${name}" sessionId=${sessionId}`);
5341
- sendJson(socket, {
5982
+ broadcastToSession(sessionId, {
5342
5983
  type: "loop_detected",
5343
5984
  sessionId,
5344
5985
  message: `Loop detected: tool "${name}" called ${MAX_REPEATED_TOOL_CALLS} times with identical input. Stopping automatically.`,
@@ -5353,10 +5994,11 @@ async function chatWebSocket(app) {
5353
5994
  }
5354
5995
  }
5355
5996
  }
5356
- sendJson(socket, {
5997
+ broadcastToSession(sessionId, {
5357
5998
  type: "event",
5358
5999
  sessionId,
5359
- event
6000
+ event,
6001
+ streamingStartedAt
5360
6002
  });
5361
6003
  });
5362
6004
  try {
@@ -5370,9 +6012,9 @@ async function chatWebSocket(app) {
5370
6012
  result
5371
6013
  });
5372
6014
  } else {
5373
- const requestId = message.payload?.requestId;
6015
+ const requestId = message2.payload?.requestId;
5374
6016
  const preparedPrompt = await promptWithUploadedImagePaths(
5375
- message.payload?.text || "",
6017
+ message2.payload?.text || "",
5376
6018
  session2,
5377
6019
  imageResult
5378
6020
  );
@@ -5390,24 +6032,23 @@ async function chatWebSocket(app) {
5390
6032
  });
5391
6033
  } finally {
5392
6034
  unsubscribe();
5393
- if (!loopDetected) {
5394
- sendJson(socket, { type: "status", sessionId, status: "idle" });
5395
- }
6035
+ sessionService.markSessionStreamingFinished(sessionId);
6036
+ broadcastToSession(sessionId, { type: "status", sessionId, status: "idle" });
5396
6037
  }
5397
6038
  break;
5398
6039
  }
5399
6040
  case "steer":
5400
6041
  case "followUp": {
5401
- const session2 = await ensureTargetSession(message.payload?.sessionId);
6042
+ const session2 = await ensureTargetSession(message2.payload?.sessionId);
5402
6043
  if (!session2) break;
5403
- const imageResult = validateImages(message.payload?.images, session2.model);
6044
+ const imageResult = validateImages(message2.payload?.images, session2.model);
5404
6045
  if (!imageResult.ok) {
5405
- sendPromptPreflight(session2.sessionId, message.payload?.requestId, false, imageResult);
6046
+ sendPromptPreflight(session2.sessionId, message2.payload?.requestId, false, imageResult);
5406
6047
  break;
5407
6048
  }
5408
- if (!message.payload?.text && imageResult.images.length === 0) break;
5409
- const requestId = message.payload?.requestId;
5410
- const streamingBehavior = message.type === "steer" ? "steer" : "followUp";
6049
+ if (!message2.payload?.text && imageResult.images.length === 0) break;
6050
+ const requestId = message2.payload?.requestId;
6051
+ const streamingBehavior = message2.type === "steer" ? "steer" : "followUp";
5411
6052
  req.log?.info({
5412
6053
  clientId: safeClientId,
5413
6054
  sessionId: session2.sessionId,
@@ -5417,7 +6058,7 @@ async function chatWebSocket(app) {
5417
6058
  }, "[ai-chat] request");
5418
6059
  await sessionService.runForegroundWithClientProfileProxy(safeClientId, async () => {
5419
6060
  const preparedPrompt = await promptWithUploadedImagePaths(
5420
- message.payload?.text || "",
6061
+ message2.payload?.text || "",
5421
6062
  session2,
5422
6063
  imageResult
5423
6064
  );
@@ -5430,7 +6071,7 @@ async function chatWebSocket(app) {
5430
6071
  break;
5431
6072
  }
5432
6073
  case "abort": {
5433
- const session2 = await ensureTargetSession(message.payload?.sessionId);
6074
+ const session2 = await ensureTargetSession(message2.payload?.sessionId);
5434
6075
  if (session2) {
5435
6076
  const aborted = await abortSessionBestEffort(session2);
5436
6077
  if (!aborted) {
@@ -5441,7 +6082,8 @@ async function chatWebSocket(app) {
5441
6082
  message: "Request did not stop cleanly; the stuck runtime was discarded."
5442
6083
  });
5443
6084
  }
5444
- sendJson(socket, { type: "status", sessionId: session2.sessionId, status: "idle" });
6085
+ sessionService.markSessionStreamingFinished(session2.sessionId);
6086
+ broadcastToSession(session2.sessionId, { type: "status", sessionId: session2.sessionId, status: "idle" });
5445
6087
  }
5446
6088
  break;
5447
6089
  }
@@ -5455,17 +6097,10 @@ async function chatWebSocket(app) {
5455
6097
  }
5456
6098
  });
5457
6099
  socket.on("close", () => {
6100
+ clients.delete(connectedClient);
5458
6101
  console.log(`Client disconnected: ${safeClientId}`);
5459
6102
  unsubscribeMemoryUpdates();
5460
6103
  unsubscribeMemoryRecall();
5461
- const activeSession = sessionService.getSession(safeClientId);
5462
- if (activeSession?.isStreaming) {
5463
- abortSessionBestEffort(activeSession).then((aborted) => {
5464
- if (!aborted) sessionService.forceDisposeBySessionId(activeSession.sessionId);
5465
- }).catch((error) => {
5466
- console.warn("Failed to stop streaming session on disconnect:", error);
5467
- });
5468
- }
5469
6104
  sessionService.scheduleCleanup(safeClientId);
5470
6105
  });
5471
6106
  });
@@ -5533,16 +6168,16 @@ async function terminalWebSocket(app) {
5533
6168
  }));
5534
6169
  socket.on("message", (rawData) => {
5535
6170
  try {
5536
- const message = JSON.parse(rawData.toString());
5537
- switch (message.type) {
6171
+ const message2 = JSON.parse(rawData.toString());
6172
+ switch (message2.type) {
5538
6173
  case "input":
5539
- if (message.terminalId === terminal.id && typeof message.data === "string") {
5540
- terminalManager.writeTo(terminal.id, message.data);
6174
+ if (message2.terminalId === terminal.id && typeof message2.data === "string") {
6175
+ terminalManager.writeTo(terminal.id, message2.data);
5541
6176
  }
5542
6177
  break;
5543
6178
  case "resize":
5544
- if (message.terminalId === terminal.id && Number.isFinite(message.cols) && Number.isFinite(message.rows)) {
5545
- terminalManager.resize(terminal.id, message.cols, message.rows);
6179
+ if (message2.terminalId === terminal.id && Number.isFinite(message2.cols) && Number.isFinite(message2.rows)) {
6180
+ terminalManager.resize(terminal.id, message2.cols, message2.rows);
5546
6181
  }
5547
6182
  break;
5548
6183
  }
@@ -6081,11 +6716,157 @@ var projectHistorySchemaMigration = {
6081
6716
  }
6082
6717
  };
6083
6718
 
6719
+ // src/db/migrations/004-wecom-gateway-schema.ts
6720
+ var wecomGatewaySchemaMigration = {
6721
+ version: 4,
6722
+ name: "wecom-gateway-schema",
6723
+ up(db) {
6724
+ db.exec(`
6725
+ CREATE TABLE IF NOT EXISTS wecom_gateway_credentials (
6726
+ id INTEGER PRIMARY KEY CHECK (id = 1),
6727
+ corp_id TEXT NOT NULL,
6728
+ corp_secret TEXT NOT NULL,
6729
+ agent_id TEXT NOT NULL,
6730
+ callback_token TEXT NOT NULL,
6731
+ encoding_aes_key TEXT NOT NULL,
6732
+ allowed_users_json TEXT NOT NULL DEFAULT '[]',
6733
+ callback_verified_at TEXT,
6734
+ created_at TEXT NOT NULL,
6735
+ updated_at TEXT NOT NULL
6736
+ );
6737
+
6738
+ CREATE TABLE IF NOT EXISTS wecom_gateway_sessions (
6739
+ session_key TEXT PRIMARY KEY,
6740
+ client_id TEXT NOT NULL,
6741
+ pi_session_id TEXT NOT NULL,
6742
+ created_at TEXT NOT NULL,
6743
+ updated_at TEXT NOT NULL
6744
+ );
6745
+
6746
+ CREATE TABLE IF NOT EXISTS wecom_gateway_configs (
6747
+ client_id TEXT PRIMARY KEY,
6748
+ agent_profile TEXT,
6749
+ default_cwd TEXT,
6750
+ skill_mode TEXT,
6751
+ skill_preset_id TEXT,
6752
+ created_at TEXT NOT NULL,
6753
+ updated_at TEXT NOT NULL
6754
+ );
6755
+ `);
6756
+ }
6757
+ };
6758
+
6759
+ // src/db/migrations/005-model-window-kickoff-schema.ts
6760
+ var modelWindowKickoffSchemaMigration = {
6761
+ version: 5,
6762
+ name: "model-window-kickoff-schema",
6763
+ up(db) {
6764
+ db.exec(`
6765
+ CREATE TABLE IF NOT EXISTS notification_channels (
6766
+ id TEXT PRIMARY KEY,
6767
+ type TEXT NOT NULL,
6768
+ name TEXT NOT NULL,
6769
+ config_json TEXT NOT NULL,
6770
+ created_at TEXT NOT NULL,
6771
+ updated_at TEXT NOT NULL
6772
+ );
6773
+
6774
+ CREATE TABLE IF NOT EXISTS model_window_kickoffs (
6775
+ id TEXT PRIMARY KEY,
6776
+ enabled INTEGER NOT NULL DEFAULT 1,
6777
+ profile_id TEXT NOT NULL,
6778
+ provider TEXT NOT NULL,
6779
+ model_id TEXT NOT NULL,
6780
+ prompt TEXT NOT NULL,
6781
+ window_duration_minutes INTEGER NOT NULL,
6782
+ safety_buffer_seconds INTEGER NOT NULL,
6783
+ notification_channel_id TEXT REFERENCES notification_channels(id) ON DELETE SET NULL,
6784
+ next_run_at TEXT NOT NULL,
6785
+ last_attempt_at TEXT,
6786
+ last_success_at TEXT,
6787
+ last_error TEXT,
6788
+ created_at TEXT NOT NULL,
6789
+ updated_at TEXT NOT NULL
6790
+ );
6791
+
6792
+ CREATE INDEX IF NOT EXISTS model_window_kickoffs_due_idx
6793
+ ON model_window_kickoffs(enabled, next_run_at);
6794
+ `);
6795
+ }
6796
+ };
6797
+
6798
+ // src/db/migrations/006-model-window-kickoff-project-path.ts
6799
+ var modelWindowKickoffProjectPathMigration = {
6800
+ version: 6,
6801
+ name: "model-window-kickoff-project-path",
6802
+ up(db) {
6803
+ db.exec(`
6804
+ ALTER TABLE model_window_kickoffs
6805
+ ADD COLUMN project_path TEXT NOT NULL DEFAULT '~';
6806
+ `);
6807
+ }
6808
+ };
6809
+
6810
+ // src/db/migrations/007-user-prompts-schema.ts
6811
+ var userPromptsSchemaMigration = {
6812
+ version: 7,
6813
+ name: "user-prompts-schema",
6814
+ up(db) {
6815
+ db.exec(`
6816
+ CREATE TABLE user_prompts (
6817
+ id TEXT PRIMARY KEY,
6818
+ name TEXT NOT NULL UNIQUE,
6819
+ content TEXT NOT NULL,
6820
+ created_at TEXT NOT NULL,
6821
+ updated_at TEXT NOT NULL
6822
+ )
6823
+ `);
6824
+ }
6825
+ };
6826
+
6827
+ // src/db/migrations/008-application-settings-table.ts
6828
+ var applicationSettingsTableMigration = {
6829
+ version: 8,
6830
+ name: "application-settings-table",
6831
+ up(db) {
6832
+ db.exec("ALTER TABLE security_settings RENAME TO application_settings");
6833
+ }
6834
+ };
6835
+
6836
+ // src/db/migrations/009-pinned-files-schema.ts
6837
+ var pinnedFilesSchemaMigration = {
6838
+ version: 9,
6839
+ name: "pinned-files-schema",
6840
+ up(db) {
6841
+ db.exec(`
6842
+ CREATE TABLE pinned_files (
6843
+ owner_type TEXT NOT NULL CHECK (owner_type = 'profile'),
6844
+ owner_id TEXT NOT NULL,
6845
+ file_path TEXT NOT NULL,
6846
+ group_id TEXT NOT NULL,
6847
+ created_at TEXT NOT NULL,
6848
+ PRIMARY KEY (owner_type, owner_id, file_path),
6849
+ FOREIGN KEY (owner_type, owner_id, group_id)
6850
+ REFERENCES session_pin_groups(owner_type, owner_id, id) ON DELETE CASCADE
6851
+ );
6852
+
6853
+ CREATE INDEX pinned_files_group_idx
6854
+ ON pinned_files(owner_type, owner_id, group_id, created_at);
6855
+ `);
6856
+ }
6857
+ };
6858
+
6084
6859
  // src/db/migrations/index.ts
6085
6860
  var migrations = [
6086
6861
  applicationSchemaMigration,
6087
6862
  gatewaySchemaMigration,
6088
- projectHistorySchemaMigration
6863
+ projectHistorySchemaMigration,
6864
+ wecomGatewaySchemaMigration,
6865
+ modelWindowKickoffSchemaMigration,
6866
+ modelWindowKickoffProjectPathMigration,
6867
+ userPromptsSchemaMigration,
6868
+ applicationSettingsTableMigration,
6869
+ pinnedFilesSchemaMigration
6089
6870
  ];
6090
6871
  function runDatabaseMigrations(db) {
6091
6872
  db.exec(`
@@ -6333,18 +7114,18 @@ var TotpService = class {
6333
7114
  this.deleteValue("totp.secret");
6334
7115
  }
6335
7116
  getValue(key) {
6336
- const row = this.db.prepare("SELECT value FROM security_settings WHERE key = ?").get(key);
7117
+ const row = this.db.prepare("SELECT value FROM application_settings WHERE key = ?").get(key);
6337
7118
  return row?.value;
6338
7119
  }
6339
7120
  setValue(key, value) {
6340
7121
  this.db.prepare(`
6341
- INSERT INTO security_settings (key, value, updated_at)
7122
+ INSERT INTO application_settings (key, value, updated_at)
6342
7123
  VALUES (?, ?, ?)
6343
7124
  ON CONFLICT(key) DO UPDATE SET value = excluded.value, updated_at = excluded.updated_at
6344
7125
  `).run(key, value, (/* @__PURE__ */ new Date()).toISOString());
6345
7126
  }
6346
7127
  deleteValue(key) {
6347
- this.db.prepare("DELETE FROM security_settings WHERE key = ?").run(key);
7128
+ this.db.prepare("DELETE FROM application_settings WHERE key = ?").run(key);
6348
7129
  }
6349
7130
  };
6350
7131
 
@@ -6672,6 +7453,7 @@ var PiSessionService = class {
6672
7453
  clientSessions = /* @__PURE__ */ new Map();
6673
7454
  currentClientSession = /* @__PURE__ */ new Map();
6674
7455
  cleanupTimers = /* @__PURE__ */ new Map();
7456
+ streamingStartedAt = /* @__PURE__ */ new Map();
6675
7457
  clientProfiles = /* @__PURE__ */ new Map();
6676
7458
  sessionListCache = /* @__PURE__ */ new Map();
6677
7459
  userMessageCountCache = /* @__PURE__ */ new Map();
@@ -7419,7 +8201,7 @@ var PiSessionService = class {
7419
8201
  path: sessionPath || cwd,
7420
8202
  created: (/* @__PURE__ */ new Date()).toISOString(),
7421
8203
  modified: (/* @__PURE__ */ new Date()).toISOString(),
7422
- messageCount: activeSession.messages.filter((message) => message.role === "user").length,
8204
+ messageCount: activeSession.messages.filter((message2) => message2.role === "user").length,
7423
8205
  cwd,
7424
8206
  firstMessage: typeof firstUserMessage === "string" ? firstUserMessage : void 0,
7425
8207
  isStreaming: activeSession.isStreaming
@@ -7838,12 +8620,32 @@ var PiSessionService = class {
7838
8620
  isSessionStreaming(sessionId) {
7839
8621
  return this.getSessionBySessionId(sessionId)?.isStreaming === true;
7840
8622
  }
8623
+ markSessionStreamingStarted(sessionId) {
8624
+ const startedAt = this.streamingStartedAt.get(sessionId) ?? Date.now();
8625
+ this.streamingStartedAt.set(sessionId, startedAt);
8626
+ return startedAt;
8627
+ }
8628
+ getSessionStreamingStartedAt(sessionId) {
8629
+ const startedAt = this.streamingStartedAt.get(sessionId);
8630
+ if (startedAt !== void 0 || !this.isSessionStreaming(sessionId)) return startedAt;
8631
+ return this.markSessionStreamingStarted(sessionId);
8632
+ }
8633
+ markSessionStreamingFinished(sessionId) {
8634
+ this.streamingStartedAt.delete(sessionId);
8635
+ }
7841
8636
  isCwdStreaming(cwd) {
7842
8637
  return Array.from(this.sessions.values()).some((session) => session.sessionManager.getCwd() === cwd && session.isStreaming === true);
7843
8638
  }
7844
8639
  scheduleCleanup(clientId, delayMs = 5 * 60 * 1e3) {
7845
8640
  this.cancelCleanup(clientId);
7846
8641
  const timer = setTimeout(() => {
8642
+ this.cleanupTimers.delete(clientId);
8643
+ const sessionIds = this.clientSessions.get(clientId) || /* @__PURE__ */ new Set();
8644
+ const isStreaming = Array.from(sessionIds).some((sessionId) => this.sessions.get(sessionId)?.isStreaming);
8645
+ if (isStreaming) {
8646
+ this.scheduleCleanup(clientId, delayMs);
8647
+ return;
8648
+ }
7847
8649
  this.disposeSession(clientId);
7848
8650
  }, delayMs);
7849
8651
  this.cleanupTimers.set(clientId, timer);
@@ -7863,6 +8665,7 @@ var PiSessionService = class {
7863
8665
  if (!session) continue;
7864
8666
  session.dispose();
7865
8667
  this.sessions.delete(sessionId);
8668
+ this.streamingStartedAt.delete(sessionId);
7866
8669
  }
7867
8670
  this.clientSessions.delete(clientId);
7868
8671
  this.currentClientSession.delete(clientId);
@@ -7888,6 +8691,7 @@ var PiSessionService = class {
7888
8691
  session.dispose();
7889
8692
  this.sessions.delete(sessionId);
7890
8693
  }
8694
+ this.streamingStartedAt.delete(sessionId);
7891
8695
  for (const [clientId, sessionIds] of this.clientSessions) {
7892
8696
  sessionIds.delete(sessionId);
7893
8697
  if (this.currentClientSession.get(clientId) === sessionId) {
@@ -8108,7 +8912,7 @@ var PiSessionService = class {
8108
8912
  return (await this.readSessionDirHeader(sessionDir))?.cwd;
8109
8913
  }
8110
8914
  getFirstUserMessageContent(messages) {
8111
- const firstUserMessage = messages.find((message) => Boolean(message) && typeof message === "object" && message.role === "user");
8915
+ const firstUserMessage = messages.find((message2) => Boolean(message2) && typeof message2 === "object" && message2.role === "user");
8112
8916
  return typeof firstUserMessage?.content === "string" ? firstUserMessage.content : void 0;
8113
8917
  }
8114
8918
  async readSessionDirHeader(sessionDir) {
@@ -8224,7 +9028,7 @@ var SessionActivityStore = class {
8224
9028
  WHERE session_id = ?
8225
9029
  ORDER BY created_at ASC, id ASC
8226
9030
  `).all(sessionId);
8227
- return rows.map(mapRow3);
9031
+ return rows.map(mapRow4);
8228
9032
  }
8229
9033
  updatePrStatus(id, status, checkedAt = this.now()) {
8230
9034
  const row = this.db.prepare("SELECT session_id, data_json FROM session_builtin_events WHERE id = ? AND kind = 'pr_created'").get(id);
@@ -8246,7 +9050,7 @@ var SessionActivityStore = class {
8246
9050
  ORDER BY created_at DESC, id DESC
8247
9051
  `).all(...sessionIds);
8248
9052
  for (const row of rows) {
8249
- if (!latest.has(row.session_id)) latest.set(row.session_id, mapRow3(row));
9053
+ if (!latest.has(row.session_id)) latest.set(row.session_id, mapRow4(row));
8250
9054
  }
8251
9055
  return latest;
8252
9056
  }
@@ -8259,7 +9063,7 @@ var SessionActivityStore = class {
8259
9063
  return this.listForSession(sessionId).find((record) => record.id === Number(result.lastInsertRowid));
8260
9064
  }
8261
9065
  };
8262
- function mapRow3(row) {
9066
+ function mapRow4(row) {
8263
9067
  return {
8264
9068
  id: row.id,
8265
9069
  sessionId: row.session_id,
@@ -8278,7 +9082,7 @@ function parseJson(value) {
8278
9082
  }
8279
9083
 
8280
9084
  // src/services/session-pin-store.ts
8281
- import { randomUUID as randomUUID3 } from "crypto";
9085
+ import { randomUUID as randomUUID4 } from "crypto";
8282
9086
  var DEFAULT_PIN_GROUP_ID = "default";
8283
9087
  var SessionPinStore = class {
8284
9088
  constructor(db) {
@@ -8299,7 +9103,7 @@ var SessionPinStore = class {
8299
9103
  this.ensureDefaultGroup(owner);
8300
9104
  const trimmed = name.trim();
8301
9105
  if (!trimmed) throw new Error("Group name is required");
8302
- const id = randomUUID3();
9106
+ const id = randomUUID4();
8303
9107
  const createdAt = (/* @__PURE__ */ new Date()).toISOString();
8304
9108
  this.db.prepare(`
8305
9109
  INSERT INTO session_pin_groups (owner_type, owner_id, id, name, is_default, created_at)
@@ -8325,6 +9129,40 @@ var SessionPinStore = class {
8325
9129
  DELETE FROM session_pins WHERE owner_type = ? AND owner_id = ? AND session_id = ?
8326
9130
  `).run(owner.type, owner.id, sessionId);
8327
9131
  }
9132
+ pinFile(profileId, filePath, groupId) {
9133
+ const owner = { type: "profile", id: profileId };
9134
+ this.ensureDefaultGroup(owner);
9135
+ if (!this.db.prepare(`
9136
+ SELECT 1 FROM session_pin_groups WHERE owner_type = 'profile' AND owner_id = ? AND id = ?
9137
+ `).get(profileId, groupId)) {
9138
+ throw new Error("Pin group not found");
9139
+ }
9140
+ this.db.prepare(`
9141
+ INSERT INTO pinned_files (owner_type, owner_id, file_path, group_id, created_at)
9142
+ VALUES ('profile', ?, ?, ?, ?)
9143
+ ON CONFLICT(owner_type, owner_id, file_path) DO UPDATE SET group_id = excluded.group_id
9144
+ `).run(profileId, filePath, groupId, (/* @__PURE__ */ new Date()).toISOString());
9145
+ }
9146
+ unpinFile(profileId, filePath) {
9147
+ this.db.prepare(`
9148
+ DELETE FROM pinned_files WHERE owner_type = 'profile' AND owner_id = ? AND file_path = ?
9149
+ `).run(profileId, filePath);
9150
+ }
9151
+ listFilePathsByGroup(profileId) {
9152
+ const rows = this.db.prepare(`
9153
+ SELECT group_id, file_path
9154
+ FROM pinned_files
9155
+ WHERE owner_type = 'profile' AND owner_id = ?
9156
+ ORDER BY created_at, file_path
9157
+ `).all(profileId);
9158
+ const result = /* @__PURE__ */ new Map();
9159
+ for (const row of rows) {
9160
+ const paths = result.get(row.group_id) || [];
9161
+ paths.push(row.file_path);
9162
+ result.set(row.group_id, paths);
9163
+ }
9164
+ return result;
9165
+ }
8328
9166
  listSessionIdsByGroup(owner) {
8329
9167
  const rows = this.db.prepare(`
8330
9168
  SELECT group_id, session_id
@@ -8379,7 +9217,7 @@ var SkillPolicyStore = class {
8379
9217
  }
8380
9218
  get(sessionId) {
8381
9219
  const row = this.db.prepare("SELECT * FROM session_skill_policies WHERE session_id = ?").get(sessionId);
8382
- return row ? mapRow4(row) : null;
9220
+ return row ? mapRow5(row) : null;
8383
9221
  }
8384
9222
  delete(sessionId) {
8385
9223
  this.db.prepare("DELETE FROM session_skill_policies WHERE session_id = ?").run(sessionId);
@@ -8388,7 +9226,7 @@ var SkillPolicyStore = class {
8388
9226
  function normalizeSkills2(skills) {
8389
9227
  return Array.from(new Set(skills.map((skill) => skill.trim()).filter(Boolean)));
8390
9228
  }
8391
- function mapRow4(row) {
9229
+ function mapRow5(row) {
8392
9230
  return {
8393
9231
  sessionId: row.session_id,
8394
9232
  username: row.username,
@@ -8571,16 +9409,16 @@ function buildExtractionSource(branchEntries, startingLeafId, endingLeafId) {
8571
9409
  const evidence = [];
8572
9410
  for (const entry of branchEntries.slice(startIndex + 1, endIndex + 1)) {
8573
9411
  if (entry.type !== "message") continue;
8574
- const message = entry.message;
8575
- if (message.role === "user") {
8576
- for (const text of textBlocks(message.content)) {
9412
+ const message2 = entry.message;
9413
+ if (message2.role === "user") {
9414
+ for (const text of textBlocks(message2.content)) {
8577
9415
  lines.push(`[User] ${text}`);
8578
9416
  evidence.push({ role: "user", text });
8579
9417
  }
8580
9418
  continue;
8581
9419
  }
8582
- if (message.role !== "assistant" || !Array.isArray(message.content)) continue;
8583
- for (const block of message.content) {
9420
+ if (message2.role !== "assistant" || !Array.isArray(message2.content)) continue;
9421
+ for (const block of message2.content) {
8584
9422
  if (block.type === "text" && block.text.trim()) {
8585
9423
  const text = block.text.trim();
8586
9424
  lines.push(`[Assistant] ${text}`);
@@ -9020,15 +9858,15 @@ function buildExtractionSource2(branchEntries, startingLeafId, endingLeafId) {
9020
9858
  };
9021
9859
  for (const entry of branchEntries.slice(startIndex + 1, endIndex + 1)) {
9022
9860
  if (entry.type !== "message") continue;
9023
- const message = entry.message;
9024
- if (message.role === "user") {
9025
- for (const text2 of textBlocks2(message.content)) {
9861
+ const message2 = entry.message;
9862
+ if (message2.role === "user") {
9863
+ for (const text2 of textBlocks2(message2.content)) {
9026
9864
  if (!isRawNoise(text2)) append({ role: "user", text: text2, ...textMetadata(text2) });
9027
9865
  }
9028
9866
  continue;
9029
9867
  }
9030
- if (message.role !== "assistant" || !Array.isArray(message.content)) continue;
9031
- for (const block of message.content) {
9868
+ if (message2.role !== "assistant" || !Array.isArray(message2.content)) continue;
9869
+ for (const block of message2.content) {
9032
9870
  if (block.type === "text" && isConfirmedAssistantEvidence(block.text)) {
9033
9871
  append({ role: "assistant", text: block.text, ...textMetadata(block.text) });
9034
9872
  continue;
@@ -9530,8 +10368,8 @@ function buildExtractionQueries(source) {
9530
10368
  // src/memory/extractor.ts
9531
10369
  var EXTRACTION_TIMEOUT_MS = 45e3;
9532
10370
  var RetryableExtractionError = class extends Error {
9533
- constructor(kind, message) {
9534
- super(message);
10371
+ constructor(kind, message2) {
10372
+ super(message2);
9535
10373
  this.kind = kind;
9536
10374
  this.name = "RetryableExtractionError";
9537
10375
  }
@@ -9895,21 +10733,21 @@ function isUsage(value) {
9895
10733
  function isAbort(error, signal) {
9896
10734
  return signal.aborted || error instanceof Error && error.name === "AbortError";
9897
10735
  }
9898
- function providerResponseError(message) {
9899
- return new Error(message?.trim() || "Memory extraction provider failed");
10736
+ function providerResponseError(message2) {
10737
+ return new Error(message2?.trim() || "Memory extraction provider failed");
9900
10738
  }
9901
10739
  function isUnsupportedSchemaError(error) {
9902
- const status = errorStatus(error);
9903
- const message = error instanceof Error ? error.message : String(error);
9904
- return (status === 400 || status === 422 || status === void 0) && /(?:response[_ .-]?format|output[_ .-]?config|text\.format|json[_ .-]?schema|structured output|responseJsonSchema|unknown (?:field|parameter)).*(?:unsupported|unknown|invalid|not (?:allowed|supported))|(?:unsupported|unknown|invalid|not (?:allowed|supported)).*(?:response[_ .-]?format|output[_ .-]?config|text\.format|json[_ .-]?schema|structured output|responseJsonSchema)/i.test(message);
10740
+ const status = errorStatus2(error);
10741
+ const message2 = error instanceof Error ? error.message : String(error);
10742
+ return (status === 400 || status === 422 || status === void 0) && /(?:response[_ .-]?format|output[_ .-]?config|text\.format|json[_ .-]?schema|structured output|responseJsonSchema|unknown (?:field|parameter)).*(?:unsupported|unknown|invalid|not (?:allowed|supported))|(?:unsupported|unknown|invalid|not (?:allowed|supported)).*(?:response[_ .-]?format|output[_ .-]?config|text\.format|json[_ .-]?schema|structured output|responseJsonSchema)/i.test(message2);
9905
10743
  }
9906
10744
  function isRetryableTransportError(error) {
9907
- const status = errorStatus(error);
10745
+ const status = errorStatus2(error);
9908
10746
  if (status !== void 0) return status === 408 || status === 409 || status === 425 || status === 429 || status >= 500;
9909
10747
  if (!(error instanceof Error)) return false;
9910
10748
  return /(?:timeout|timed out|network|connection|socket|fetch failed|temporarily unavailable|rate limit|overloaded)/i.test(error.message);
9911
10749
  }
9912
- function errorStatus(error) {
10750
+ function errorStatus2(error) {
9913
10751
  if (!error || typeof error !== "object") return void 0;
9914
10752
  const candidate = error;
9915
10753
  const status = candidate.status ?? candidate.statusCode ?? candidate.response?.status;
@@ -10543,15 +11381,15 @@ function errorName2(error) {
10543
11381
  }
10544
11382
 
10545
11383
  // src/memory/store.ts
10546
- import { randomUUID as randomUUID4 } from "node:crypto";
11384
+ import { randomUUID as randomUUID5 } from "node:crypto";
10547
11385
  var MemoryStore = class {
10548
11386
  constructor(db) {
10549
11387
  this.db = db;
10550
11388
  }
10551
11389
  db;
10552
11390
  isAutoExtractionEnabled() {
10553
- const row = this.db.prepare("SELECT value FROM security_settings WHERE key = ?").get("memory.autoExtract");
10554
- return row?.value !== "false";
11391
+ const row = this.db.prepare("SELECT value FROM application_settings WHERE key = ?").get("memory.autoExtract");
11392
+ return row?.value === "true";
10555
11393
  }
10556
11394
  getOrCreateProject(profileId, canonicalPath) {
10557
11395
  const now = (/* @__PURE__ */ new Date()).toISOString();
@@ -10559,7 +11397,7 @@ var MemoryStore = class {
10559
11397
  INSERT INTO memory_projects (id, profile_id, canonical_path, created_at, updated_at)
10560
11398
  VALUES (?, ?, ?, ?, ?)
10561
11399
  ON CONFLICT(profile_id, canonical_path) DO NOTHING
10562
- `).run(randomUUID4(), profileId, canonicalPath, now, now);
11400
+ `).run(randomUUID5(), profileId, canonicalPath, now, now);
10563
11401
  return this.getProjectByPath(profileId, canonicalPath);
10564
11402
  }
10565
11403
  getProjectById(id) {
@@ -10617,7 +11455,7 @@ var MemoryStore = class {
10617
11455
  const evidence = input.evidence ? assertMemoryContent(input.evidence) : null;
10618
11456
  const tags = normalizeMemoryTags(input.tags);
10619
11457
  const now = (/* @__PURE__ */ new Date()).toISOString();
10620
- const id = randomUUID4();
11458
+ const id = randomUUID5();
10621
11459
  try {
10622
11460
  this.db.prepare(`
10623
11461
  INSERT INTO memories (
@@ -10921,7 +11759,7 @@ var MemoryStore = class {
10921
11759
  ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, 'queued', ?, ?, 0, ?)
10922
11760
  ON CONFLICT(source_session_id, ending_leaf_id, source_kind) DO NOTHING
10923
11761
  `).run(
10924
- randomUUID4(),
11762
+ randomUUID5(),
10925
11763
  input.profileId,
10926
11764
  input.projectId,
10927
11765
  input.sourceSessionId,
@@ -11431,8 +12269,8 @@ function createMemoryRuntime(options) {
11431
12269
 
11432
12270
  // src/services/worktree-manager.ts
11433
12271
  import { execFile as execFileCallback } from "child_process";
11434
- import { copyFile, mkdir as mkdir5, readdir as readdir4 } from "fs/promises";
11435
- import { basename as basename5, dirname as dirname8, isAbsolute as isAbsolute3, join as join13 } from "path";
12272
+ import { copyFile, mkdir as mkdir5, readdir as readdir3 } from "fs/promises";
12273
+ import { basename as basename5, dirname as dirname8, isAbsolute as isAbsolute4, join as join13 } from "path";
11436
12274
  import { promisify as promisify4 } from "util";
11437
12275
  var defaultExecFile = promisify4(execFileCallback);
11438
12276
  var WorktreeManager = class {
@@ -11460,7 +12298,7 @@ var WorktreeManager = class {
11460
12298
  const baseRepoPath = await this.getRepoRoot(projectPath).catch(() => void 0);
11461
12299
  if (!baseRepoPath) return [];
11462
12300
  const files = [];
11463
- const entries = await readdir4(baseRepoPath, { withFileTypes: true });
12301
+ const entries = await readdir3(baseRepoPath, { withFileTypes: true });
11464
12302
  for (const entry of entries) {
11465
12303
  if (!entry.isFile() || !this.isRootFileName(entry.name)) continue;
11466
12304
  if (await this.isIgnored(baseRepoPath, entry.name)) files.push(entry.name);
@@ -11554,7 +12392,7 @@ var WorktreeManager = class {
11554
12392
  }
11555
12393
  }
11556
12394
  isRootFileName(name) {
11557
- return Boolean(name.trim()) && !isAbsolute3(name) && !name.includes("/") && !name.includes("\\") && basename5(name) === name;
12395
+ return Boolean(name.trim()) && !isAbsolute4(name) && !name.includes("/") && !name.includes("\\") && basename5(name) === name;
11558
12396
  }
11559
12397
  };
11560
12398
 
@@ -11927,8 +12765,8 @@ function matchesNoProxy(host, entry) {
11927
12765
 
11928
12766
  // src/services/github-client.ts
11929
12767
  var GithubRequestError = class extends Error {
11930
- constructor(message, statusCode) {
11931
- super(message);
12768
+ constructor(message2, statusCode) {
12769
+ super(message2);
11932
12770
  this.statusCode = statusCode;
11933
12771
  this.name = "GithubRequestError";
11934
12772
  }
@@ -12027,8 +12865,8 @@ function execFileCapture(file, args, options) {
12027
12865
  function parseResponse2(text, ok, status) {
12028
12866
  const data = parseJsonObject3(text);
12029
12867
  if (!ok) {
12030
- const message = typeof data.message === "string" ? data.message : `GitHub request failed (${status})`;
12031
- throw new GithubRequestError(message, status);
12868
+ const message2 = typeof data.message === "string" ? data.message : `GitHub request failed (${status})`;
12869
+ throw new GithubRequestError(message2, status);
12032
12870
  }
12033
12871
  return data;
12034
12872
  }
@@ -12102,18 +12940,18 @@ var GatewaySettingsStore = class {
12102
12940
  else this.delete(key);
12103
12941
  }
12104
12942
  value(key) {
12105
- const row = this.db.prepare("SELECT value FROM security_settings WHERE key = ?").get(key);
12943
+ const row = this.db.prepare("SELECT value FROM application_settings WHERE key = ?").get(key);
12106
12944
  return row?.value || "";
12107
12945
  }
12108
12946
  set(key, value) {
12109
12947
  this.db.prepare(`
12110
- INSERT INTO security_settings (key, value, updated_at)
12948
+ INSERT INTO application_settings (key, value, updated_at)
12111
12949
  VALUES (?, ?, ?)
12112
12950
  ON CONFLICT(key) DO UPDATE SET value = excluded.value, updated_at = excluded.updated_at
12113
12951
  `).run(key, value, (/* @__PURE__ */ new Date()).toISOString());
12114
12952
  }
12115
12953
  delete(key) {
12116
- this.db.prepare("DELETE FROM security_settings WHERE key = ?").run(key);
12954
+ this.db.prepare("DELETE FROM application_settings WHERE key = ?").run(key);
12117
12955
  }
12118
12956
  };
12119
12957
  function normalizeCwdList(value) {
@@ -12235,8 +13073,8 @@ function toSessionOptions(task, cwd) {
12235
13073
 
12236
13074
  // src/services/repository-cloner.ts
12237
13075
  import { EventEmitter as EventEmitter2 } from "node:events";
12238
- import { randomUUID as randomUUID5 } from "node:crypto";
12239
- import { rm as rm4, stat as stat5 } from "node:fs/promises";
13076
+ import { randomUUID as randomUUID6 } from "node:crypto";
13077
+ import { rm as rm4, stat as stat6 } from "node:fs/promises";
12240
13078
  import { join as join14 } from "node:path";
12241
13079
  var GIT_PROGRESS_PATTERNS = [
12242
13080
  { pattern: /^Receiving objects:\s+(\d+)%/, status: "Receiving objects\u2026" },
@@ -12256,7 +13094,7 @@ var RepositoryCloner = class {
12256
13094
  if (!destinationPath) throw new Error("Destination path is required");
12257
13095
  const pathExists = this.options.pathExists || defaultPathExists;
12258
13096
  if (await pathExists(destinationPath)) return { status: "destination_exists", existingPath: destinationPath };
12259
- const job = new CloneJob(`clone_${randomUUID5()}`, remoteUrl, destinationPath, input.shallow === true);
13097
+ const job = new CloneJob(`clone_${randomUUID6()}`, remoteUrl, destinationPath, input.shallow === true);
12260
13098
  this.jobs.set(job.id, job);
12261
13099
  await this.spawnClone(job, remoteUrl, false, input.clientId);
12262
13100
  return { status: "started", jobId: job.id };
@@ -12415,7 +13253,7 @@ function withToken(remoteUrl, username, token) {
12415
13253
  return parsed.toString();
12416
13254
  }
12417
13255
  async function defaultPathExists(path13) {
12418
- return stat5(path13).then(() => true, () => false);
13256
+ return stat6(path13).then(() => true, () => false);
12419
13257
  }
12420
13258
  async function defaultRemovePath(path13) {
12421
13259
  await rm4(path13, { recursive: true, force: true });
@@ -12423,8 +13261,8 @@ async function defaultRemovePath(path13) {
12423
13261
 
12424
13262
  // src/services/feishu-gateway.ts
12425
13263
  import { createDecipheriv, createHash as createHash7 } from "node:crypto";
12426
- import { stat as stat6 } from "node:fs/promises";
12427
- import { isAbsolute as isAbsolute4 } from "node:path";
13264
+ import { stat as stat7 } from "node:fs/promises";
13265
+ import { isAbsolute as isAbsolute5 } from "node:path";
12428
13266
 
12429
13267
  // src/services/gateway-command-aliases.ts
12430
13268
  var DEFAULT_COMMAND_PREFIX = "\u5C0F\u52A9\u7406";
@@ -12510,12 +13348,12 @@ var FeishuGatewayService = class {
12510
13348
  if (payload.type === "url_verification") {
12511
13349
  return { challenge: stringValue(payload.challenge) };
12512
13350
  }
12513
- const message = this.parseMessage(payload);
12514
- if (!message) return { ok: true, ignored: true };
12515
- if (this.markDuplicate(message.eventId) || this.markDuplicate(message.messageId)) {
13351
+ const message2 = this.parseMessage(payload);
13352
+ if (!message2) return { ok: true, ignored: true };
13353
+ if (this.markDuplicate(message2.eventId) || this.markDuplicate(message2.messageId)) {
12516
13354
  return { ok: true, duplicate: true };
12517
13355
  }
12518
- this.enqueue(message, config);
13356
+ this.enqueue(message2, config);
12519
13357
  return { ok: true };
12520
13358
  }
12521
13359
  loadConfig() {
@@ -12562,9 +13400,9 @@ var FeishuGatewayService = class {
12562
13400
  console.warn("[feishu-gateway] failed to decrypt encrypted callback payload", {
12563
13401
  reason: error instanceof Error ? error.message : "unknown error"
12564
13402
  });
12565
- const gatewayError = new Error("Invalid Feishu encrypted callback payload");
12566
- gatewayError.statusCode = 400;
12567
- throw gatewayError;
13403
+ const gatewayError2 = new Error("Invalid Feishu encrypted callback payload");
13404
+ gatewayError2.statusCode = 400;
13405
+ throw gatewayError2;
12568
13406
  }
12569
13407
  }
12570
13408
  verifyToken(payload, config) {
@@ -12587,22 +13425,22 @@ var FeishuGatewayService = class {
12587
13425
  const event = asRecord(payload.event);
12588
13426
  const eventType = stringValue(header.event_type);
12589
13427
  if (eventType && eventType !== "im.message.receive_v1") return null;
12590
- const message = asRecord(event.message);
12591
- const messageType = stringValue(message.message_type);
13428
+ const message2 = asRecord(event.message);
13429
+ const messageType = stringValue(message2.message_type);
12592
13430
  if (messageType && messageType !== "text") {
12593
- logUnsupportedFeishuMessage(message, messageType);
13431
+ logUnsupportedFeishuMessage(message2, messageType);
12594
13432
  return null;
12595
13433
  }
12596
- const text = parseFeishuTextContent(stringValue(message.content)).trim();
12597
- const chatId = stringValue(message.chat_id);
12598
- const messageId = stringValue(message.message_id);
13434
+ const text = parseFeishuTextContent(stringValue(message2.content)).trim();
13435
+ const chatId = stringValue(message2.chat_id);
13436
+ const messageId = stringValue(message2.message_id);
12599
13437
  if (!text || !chatId || !messageId) return null;
12600
13438
  return {
12601
13439
  eventId: stringValue(header.event_id) || messageId,
12602
13440
  messageId,
12603
13441
  chatId,
12604
- chatType: stringValue(message.chat_type) || "p2p",
12605
- threadId: stringValue(message.thread_id) || stringValue(message.parent_id) || stringValue(message.root_id),
13442
+ chatType: stringValue(message2.chat_type) || "p2p",
13443
+ threadId: stringValue(message2.thread_id) || stringValue(message2.parent_id) || stringValue(message2.root_id),
12606
13444
  text
12607
13445
  };
12608
13446
  }
@@ -12616,24 +13454,24 @@ var FeishuGatewayService = class {
12616
13454
  this.seen.set(id, now + DEDUP_TTL_MS);
12617
13455
  return false;
12618
13456
  }
12619
- enqueue(message, config) {
12620
- const clientId = this.clientIdForMessage(message);
13457
+ enqueue(message2, config) {
13458
+ const clientId = this.clientIdForMessage(message2);
12621
13459
  const previous = this.queues.get(clientId) || Promise.resolve();
12622
- const next = previous.catch(() => void 0).then(() => this.processMessage(clientId, message, config)).catch((error) => console.error("[feishu-gateway] message handling failed:", error)).finally(() => {
13460
+ const next = previous.catch(() => void 0).then(() => this.processMessage(clientId, message2, config)).catch((error) => console.error("[feishu-gateway] message handling failed:", error)).finally(() => {
12623
13461
  if (this.queues.get(clientId) === next) this.queues.delete(clientId);
12624
13462
  });
12625
13463
  this.queues.set(clientId, next);
12626
13464
  }
12627
- clientIdForMessage(message) {
12628
- const chatType = message.chatType === "p2p" ? "dm" : message.chatType || "chat";
12629
- const thread = message.threadId ? `:${message.threadId}` : "";
12630
- return `feishu:${chatType}:${message.chatId}${thread}`;
13465
+ clientIdForMessage(message2) {
13466
+ const chatType = message2.chatType === "p2p" ? "dm" : message2.chatType || "chat";
13467
+ const thread = message2.threadId ? `:${message2.threadId}` : "";
13468
+ return `feishu:${chatType}:${message2.chatId}${thread}`;
12631
13469
  }
12632
- async processMessage(clientId, message, config) {
13470
+ async processMessage(clientId, message2, config) {
12633
13471
  const effectiveConfig = this.resolveChatConfig(clientId, config);
12634
- if (await this.handleCommand(clientId, message, config, effectiveConfig)) return;
13472
+ if (await this.handleCommand(clientId, message2, config, effectiveConfig)) return;
12635
13473
  if (!effectiveConfig.defaultCwd) {
12636
- await this.sendReply(config, message.messageId, GATEWAY_FOLDERS_REQUIRED_MESSAGE);
13474
+ await this.sendReply(config, message2.messageId, GATEWAY_FOLDERS_REQUIRED_MESSAGE);
12637
13475
  return;
12638
13476
  }
12639
13477
  const session = await this.ensureSession(clientId, effectiveConfig);
@@ -12647,13 +13485,13 @@ var FeishuGatewayService = class {
12647
13485
  });
12648
13486
  try {
12649
13487
  await this.sessionService.runForegroundWithClientProfileProxy(clientId, async () => {
12650
- await session.prompt(message.text);
13488
+ await session.prompt(message2.text);
12651
13489
  });
12652
13490
  } finally {
12653
13491
  unsubscribe();
12654
13492
  }
12655
13493
  const response = chunks.join("").trim() || extractLastAssistantText(session.messages) || "(Pi finished without a text response.)";
12656
- await this.sendReply(config, message.messageId, response);
13494
+ await this.sendReply(config, message2.messageId, response);
12657
13495
  }
12658
13496
  async ensureSession(clientId, config) {
12659
13497
  await this.sessionService.setClientAgentProfile(clientId, config.agentProfile || "default");
@@ -12668,8 +13506,8 @@ var FeishuGatewayService = class {
12668
13506
  this.saveSessionMapping(clientId, result.session.sessionId);
12669
13507
  return result.session;
12670
13508
  }
12671
- async handleCommand(clientId, message, baseConfig, effectiveConfig) {
12672
- const commandText = normalizeGatewayCommandText(message.text);
13509
+ async handleCommand(clientId, message2, baseConfig, effectiveConfig) {
13510
+ const commandText = normalizeGatewayCommandText(message2.text);
12673
13511
  const [command = "", ...args] = commandText.split(/\s+/);
12674
13512
  const normalized = command.toLowerCase();
12675
13513
  if (!normalized.startsWith("/")) return false;
@@ -12678,66 +13516,66 @@ var FeishuGatewayService = class {
12678
13516
  this.resetSession(clientId);
12679
13517
  await this.sendReply(
12680
13518
  baseConfig,
12681
- message.messageId,
13519
+ message2.messageId,
12682
13520
  await this.formatStatus(clientId, this.resolveChatConfig(clientId, baseConfig), "Reset this Feishu chat. A fresh Pi session will start with your next message.")
12683
13521
  );
12684
13522
  return true;
12685
13523
  }
12686
13524
  if (normalized === "/help") {
12687
- await this.sendReply(baseConfig, message.messageId, this.formatHelp());
13525
+ await this.sendReply(baseConfig, message2.messageId, this.formatHelp());
12688
13526
  return true;
12689
13527
  }
12690
13528
  if (normalized === "/status") {
12691
- await this.sendReply(baseConfig, message.messageId, await this.formatStatus(clientId, effectiveConfig));
13529
+ await this.sendReply(baseConfig, message2.messageId, await this.formatStatus(clientId, effectiveConfig));
12692
13530
  return true;
12693
13531
  }
12694
13532
  if (normalized === "/profiles") {
12695
- await this.sendReply(baseConfig, message.messageId, await this.formatProfiles(effectiveConfig.agentProfile || "default"));
13533
+ await this.sendReply(baseConfig, message2.messageId, await this.formatProfiles(effectiveConfig.agentProfile || "default"));
12696
13534
  return true;
12697
13535
  }
12698
13536
  if (normalized === "/profile" || normalized === "/use-profile" || normalized === "/set-profile") {
12699
13537
  await this.setChatProfile(clientId, args[0]);
12700
13538
  this.resetSession(clientId);
12701
- await this.sendReply(baseConfig, message.messageId, await this.formatStatus(clientId, this.resolveChatConfig(clientId, baseConfig), "Profile updated."));
13539
+ await this.sendReply(baseConfig, message2.messageId, await this.formatStatus(clientId, this.resolveChatConfig(clientId, baseConfig), "Profile updated."));
12702
13540
  return true;
12703
13541
  }
12704
13542
  if (normalized === "/cwds" || normalized === "/pwds") {
12705
- await this.sendReply(baseConfig, message.messageId, this.formatCwdChoices(baseConfig));
13543
+ await this.sendReply(baseConfig, message2.messageId, this.formatCwdChoices(baseConfig));
12706
13544
  return true;
12707
13545
  }
12708
13546
  if (normalized === "/cwd") {
12709
13547
  if (args.length === 0) {
12710
- await this.sendReply(baseConfig, message.messageId, effectiveConfig.defaultCwd || NO_GATEWAY_FOLDERS_MESSAGE);
13548
+ await this.sendReply(baseConfig, message2.messageId, effectiveConfig.defaultCwd || NO_GATEWAY_FOLDERS_MESSAGE);
12711
13549
  return true;
12712
13550
  }
12713
13551
  await this.setChatCwd(clientId, args[0], baseConfig);
12714
13552
  this.resetSession(clientId);
12715
- await this.sendReply(baseConfig, message.messageId, await this.formatStatus(clientId, this.resolveChatConfig(clientId, baseConfig), "Working directory updated."));
13553
+ await this.sendReply(baseConfig, message2.messageId, await this.formatStatus(clientId, this.resolveChatConfig(clientId, baseConfig), "Working directory updated."));
12716
13554
  return true;
12717
13555
  }
12718
13556
  if (normalized === "/skillsets") {
12719
- await this.sendReply(baseConfig, message.messageId, this.formatSkillsets(effectiveConfig.skillPresetId));
13557
+ await this.sendReply(baseConfig, message2.messageId, this.formatSkillsets(effectiveConfig.skillPresetId));
12720
13558
  return true;
12721
13559
  }
12722
13560
  if (normalized === "/skillset") {
12723
13561
  await this.setChatSkillset(clientId, args.join(" "));
12724
13562
  this.resetSession(clientId);
12725
- await this.sendReply(baseConfig, message.messageId, await this.formatStatus(clientId, this.resolveChatConfig(clientId, baseConfig), "Skillset updated."));
13563
+ await this.sendReply(baseConfig, message2.messageId, await this.formatStatus(clientId, this.resolveChatConfig(clientId, baseConfig), "Skillset updated."));
12726
13564
  return true;
12727
13565
  }
12728
13566
  if (normalized === "/clear-config") {
12729
13567
  this.clearChatConfig(clientId);
12730
13568
  this.resetSession(clientId);
12731
- await this.sendReply(baseConfig, message.messageId, await this.formatStatus(clientId, this.resolveChatConfig(clientId, baseConfig), "Feishu chat config cleared."));
13569
+ await this.sendReply(baseConfig, message2.messageId, await this.formatStatus(clientId, this.resolveChatConfig(clientId, baseConfig), "Feishu chat config cleared."));
12732
13570
  return true;
12733
13571
  }
12734
13572
  } catch (error) {
12735
- await this.sendReply(baseConfig, message.messageId, `Command failed: ${error instanceof Error ? error.message : "unknown error"}
13573
+ await this.sendReply(baseConfig, message2.messageId, `Command failed: ${error instanceof Error ? error.message : "unknown error"}
12736
13574
 
12737
13575
  ${this.formatHelp()}`);
12738
13576
  return true;
12739
13577
  }
12740
- await this.sendReply(baseConfig, message.messageId, `Unknown command: ${command}
13578
+ await this.sendReply(baseConfig, message2.messageId, `Unknown command: ${command}
12741
13579
 
12742
13580
  ${this.formatHelp()}`);
12743
13581
  return true;
@@ -12812,8 +13650,8 @@ ${this.formatHelp()}`);
12812
13650
  const slot = Number(slotText.trim());
12813
13651
  if (!Number.isInteger(slot) || slot < 1 || slot > config.cwdChoices.length) throw new Error(`Usage: /cwd <1-${config.cwdChoices.length}>`);
12814
13652
  const target = config.cwdChoices[slot - 1];
12815
- if (!isAbsolute4(target)) throw new Error(`Configured CWD ${slot} must be an absolute path`);
12816
- const info = await stat6(target);
13653
+ if (!isAbsolute5(target)) throw new Error(`Configured CWD ${slot} must be an absolute path`);
13654
+ const info = await stat7(target);
12817
13655
  if (!info.isDirectory()) throw new Error(`Configured CWD ${slot} must be an existing directory`);
12818
13656
  this.saveChatConfig(clientId, { defaultCwd: target });
12819
13657
  }
@@ -12974,13 +13812,13 @@ function parseFeishuTextContent(content) {
12974
13812
  return content;
12975
13813
  }
12976
13814
  }
12977
- function logUnsupportedFeishuMessage(message, messageType) {
12978
- const content = parseJsonRecord(stringValue(message.content));
13815
+ function logUnsupportedFeishuMessage(message2, messageType) {
13816
+ const content = parseJsonRecord(stringValue(message2.content));
12979
13817
  console.info("[feishu-gateway] unsupported inbound message", {
12980
13818
  messageType,
12981
- messageId: stringValue(message.message_id) || void 0,
12982
- chatType: stringValue(message.chat_type) || void 0,
12983
- messageKeys: Object.keys(message).sort(),
13819
+ messageId: stringValue(message2.message_id) || void 0,
13820
+ chatType: stringValue(message2.chat_type) || void 0,
13821
+ messageKeys: Object.keys(message2).sort(),
12984
13822
  contentKeys: Object.keys(content).sort(),
12985
13823
  contentStringFields: Object.fromEntries(
12986
13824
  Object.entries(content).filter(([, value]) => typeof value === "string").map(([key, value]) => [key, value.length])
@@ -13004,9 +13842,9 @@ function truncateFeishuText(text) {
13004
13842
  function extractLastAssistantText(messages) {
13005
13843
  if (!Array.isArray(messages)) return "";
13006
13844
  for (let index = messages.length - 1; index >= 0; index -= 1) {
13007
- const message = messages[index];
13008
- if (message?.role !== "assistant") continue;
13009
- const content = message.content;
13845
+ const message2 = messages[index];
13846
+ if (message2?.role !== "assistant") continue;
13847
+ const content = message2.content;
13010
13848
  if (typeof content === "string") return content.trim();
13011
13849
  if (Array.isArray(content)) {
13012
13850
  const text = content.map((part) => typeof part === "string" ? part : part?.text || part?.content || "").join("").trim();
@@ -13017,9 +13855,9 @@ function extractLastAssistantText(messages) {
13017
13855
  }
13018
13856
 
13019
13857
  // src/services/weixin-gateway.ts
13020
- import { randomUUID as randomUUID6 } from "node:crypto";
13021
- import { stat as stat7 } from "node:fs/promises";
13022
- import { isAbsolute as isAbsolute5 } from "node:path";
13858
+ import { randomUUID as randomUUID7 } from "node:crypto";
13859
+ import { stat as stat8 } from "node:fs/promises";
13860
+ import { isAbsolute as isAbsolute6 } from "node:path";
13023
13861
  import QRCode from "qrcode";
13024
13862
  import { Agent, fetch as undiciFetch2 } from "undici";
13025
13863
 
@@ -13280,11 +14118,11 @@ var WeixinGatewayService = class {
13280
14118
  }
13281
14119
  const messages = Array.isArray(response.msgs) ? response.msgs : [];
13282
14120
  for (const raw of messages) {
13283
- const message = await this.parseMessage(asRecord2(raw), config);
13284
- if (!message) continue;
13285
- if (this.markDuplicate(message.messageId)) continue;
13286
- if (message.contextToken) this.saveContextToken(config.accountId, message.senderId, message.contextToken);
13287
- this.enqueue(message, config);
14121
+ const message2 = await this.parseMessage(asRecord2(raw), config);
14122
+ if (!message2) continue;
14123
+ if (this.markDuplicate(message2.messageId)) continue;
14124
+ if (message2.contextToken) this.saveContextToken(config.accountId, message2.senderId, message2.contextToken);
14125
+ this.enqueue(message2, config);
13288
14126
  }
13289
14127
  } catch (error) {
13290
14128
  if (!this.running) break;
@@ -13296,17 +14134,17 @@ var WeixinGatewayService = class {
13296
14134
  }
13297
14135
  console.info("[weixin-gateway] stopped");
13298
14136
  }
13299
- async parseMessage(message, config) {
13300
- const senderId = stringValue2(message.from_user_id);
14137
+ async parseMessage(message2, config) {
14138
+ const senderId = stringValue2(message2.from_user_id);
13301
14139
  if (!senderId || senderId === config.accountId) return null;
13302
- const { chatType, chatId } = this.guessChat(message, config.accountId);
14140
+ const { chatType, chatId } = this.guessChat(message2, config.accountId);
13303
14141
  if (chatType === "group") return null;
13304
14142
  if (!this.isDmAllowed(senderId, config)) return null;
13305
- const items = Array.isArray(message.item_list) ? message.item_list.map(asRecord2) : [];
14143
+ const items = Array.isArray(message2.item_list) ? message2.item_list.map(asRecord2) : [];
13306
14144
  const text = extractText2(items).trim();
13307
14145
  const imageResult = await this.extractImages(config, items);
13308
- const messageId = stringValue2(message.message_id) || randomUUID6();
13309
- if (!text && imageResult.images.length === 0 && !imageResult.error) this.logUnsupportedInboundMessage(message, items, "no text or supported image item");
14146
+ const messageId = stringValue2(message2.message_id) || randomUUID7();
14147
+ if (!text && imageResult.images.length === 0 && !imageResult.error) this.logUnsupportedInboundMessage(message2, items, "no text or supported image item");
13310
14148
  if (!text && imageResult.images.length === 0 && !imageResult.error || !chatId) return null;
13311
14149
  return {
13312
14150
  messageId,
@@ -13316,15 +14154,15 @@ var WeixinGatewayService = class {
13316
14154
  text,
13317
14155
  images: imageResult.images,
13318
14156
  imageError: imageResult.error,
13319
- contextToken: stringValue2(message.context_token) || void 0
14157
+ contextToken: stringValue2(message2.context_token) || void 0
13320
14158
  };
13321
14159
  }
13322
- guessChat(message, accountId) {
13323
- const roomId = stringValue2(message.room_id) || stringValue2(message.chat_room_id);
13324
- const toUserId = stringValue2(message.to_user_id);
13325
- const isGroup = Boolean(roomId) || Boolean(toUserId && accountId && toUserId !== accountId && Number(message.msg_type) === 1);
13326
- if (isGroup) return { chatType: "group", chatId: roomId || toUserId || stringValue2(message.from_user_id) };
13327
- return { chatType: "dm", chatId: stringValue2(message.from_user_id) };
14160
+ guessChat(message2, accountId) {
14161
+ const roomId = stringValue2(message2.room_id) || stringValue2(message2.chat_room_id);
14162
+ const toUserId = stringValue2(message2.to_user_id);
14163
+ const isGroup = Boolean(roomId) || Boolean(toUserId && accountId && toUserId !== accountId && Number(message2.msg_type) === 1);
14164
+ if (isGroup) return { chatType: "group", chatId: roomId || toUserId || stringValue2(message2.from_user_id) };
14165
+ return { chatType: "dm", chatId: stringValue2(message2.from_user_id) };
13328
14166
  }
13329
14167
  isDmAllowed(senderId, config) {
13330
14168
  if (config.dmPolicy === "disabled") return false;
@@ -13332,13 +14170,13 @@ var WeixinGatewayService = class {
13332
14170
  if (config.dmPolicy === "open") return parseBoolean3(process.env.PI_CLOUD_WECHAT_ALLOW_ALL_USERS || process.env.GATEWAY_ALLOW_ALL_USERS, false);
13333
14171
  return true;
13334
14172
  }
13335
- logUnsupportedInboundMessage(message, items, reason) {
14173
+ logUnsupportedInboundMessage(message2, items, reason) {
13336
14174
  console.info("[weixin-gateway] unsupported inbound message", JSON.stringify({
13337
14175
  reason,
13338
- messageId: message.message_id,
13339
- msgType: message.msg_type,
13340
- messageType: message.message_type,
13341
- messageKeys: Object.keys(message).sort(),
14176
+ messageId: message2.message_id,
14177
+ msgType: message2.msg_type,
14178
+ messageType: message2.message_type,
14179
+ messageKeys: Object.keys(message2).sort(),
13342
14180
  itemSummaries: items.map(summarizeWeixinItem)
13343
14181
  }, null, 2));
13344
14182
  }
@@ -13426,36 +14264,36 @@ var WeixinGatewayService = class {
13426
14264
  this.seen.set(id, now + DEDUP_TTL_MS2);
13427
14265
  return false;
13428
14266
  }
13429
- enqueue(message, config) {
13430
- const clientId = this.clientIdForMessage(message);
14267
+ enqueue(message2, config) {
14268
+ const clientId = this.clientIdForMessage(message2);
13431
14269
  const previous = this.queues.get(clientId) || Promise.resolve();
13432
- const next = previous.catch(() => void 0).then(() => this.processMessage(clientId, message, config)).catch((error) => console.error("[weixin-gateway] message handling failed:", error)).finally(() => {
14270
+ const next = previous.catch(() => void 0).then(() => this.processMessage(clientId, message2, config)).catch((error) => console.error("[weixin-gateway] message handling failed:", error)).finally(() => {
13433
14271
  if (this.queues.get(clientId) === next) this.queues.delete(clientId);
13434
14272
  });
13435
14273
  this.queues.set(clientId, next);
13436
14274
  }
13437
- clientIdForMessage(message) {
13438
- return `weixin:${message.chatType}:${message.chatId}`;
14275
+ clientIdForMessage(message2) {
14276
+ return `weixin:${message2.chatType}:${message2.chatId}`;
13439
14277
  }
13440
- async processMessage(clientId, message, config) {
14278
+ async processMessage(clientId, message2, config) {
13441
14279
  const effectiveConfig = this.resolveChatConfig(clientId, config);
13442
- if (await this.handleCommand(clientId, message, config, effectiveConfig)) return;
14280
+ if (await this.handleCommand(clientId, message2, config, effectiveConfig)) return;
13443
14281
  if (!effectiveConfig.defaultCwd) {
13444
- await this.sendReply(config, message.chatId, GATEWAY_FOLDERS_REQUIRED_MESSAGE2);
14282
+ await this.sendReply(config, message2.chatId, GATEWAY_FOLDERS_REQUIRED_MESSAGE2);
13445
14283
  return;
13446
14284
  }
13447
- if (message.imageError && message.images.length === 0) {
13448
- await this.sendReply(config, message.chatId, message.imageError);
14285
+ if (message2.imageError && message2.images.length === 0) {
14286
+ await this.sendReply(config, message2.chatId, message2.imageError);
13449
14287
  return;
13450
14288
  }
13451
14289
  const session = await this.ensureSession(clientId, effectiveConfig);
13452
- const imageResult = validateImages(message.images, session.model);
14290
+ const imageResult = validateImages(message2.images, session.model);
13453
14291
  if (!imageResult.ok) {
13454
- await this.sendReply(config, message.chatId, imageResult.message);
14292
+ await this.sendReply(config, message2.chatId, imageResult.message);
13455
14293
  return;
13456
14294
  }
13457
- await this.saveInboundImages(effectiveConfig.defaultCwd, message.messageId, imageResult.images);
13458
- const promptText = message.text || (imageResult.images.length ? "Please analyze the attached image." : "");
14295
+ await this.saveInboundImages(effectiveConfig.defaultCwd, message2.messageId, imageResult.images);
14296
+ const promptText = message2.text || (imageResult.images.length ? "Please analyze the attached image." : "");
13459
14297
  const chunks = [];
13460
14298
  const unsubscribe = session.subscribe((event) => {
13461
14299
  if (event?.type !== "message_update") return;
@@ -13464,7 +14302,7 @@ var WeixinGatewayService = class {
13464
14302
  chunks.push(update.delta);
13465
14303
  }
13466
14304
  });
13467
- const stopTypingIndicator = this.startTypingIndicator(config, message.chatId);
14305
+ const stopTypingIndicator = this.startTypingIndicator(config, message2.chatId);
13468
14306
  try {
13469
14307
  await this.sessionService.runForegroundWithClientProfileProxy(clientId, async () => {
13470
14308
  await session.prompt(promptText, imageResult.images.length ? { images: imageResult.images } : void 0);
@@ -13474,7 +14312,7 @@ var WeixinGatewayService = class {
13474
14312
  unsubscribe();
13475
14313
  }
13476
14314
  const response = chunks.join("").trim() || extractLastAssistantText2(session.messages) || "(Pi finished without a text response.)";
13477
- await this.sendReply(config, message.chatId, response);
14315
+ await this.sendReply(config, message2.chatId, response);
13478
14316
  }
13479
14317
  async saveInboundImages(projectRoot, messageId, images) {
13480
14318
  if (!projectRoot || images.length === 0) return;
@@ -13505,8 +14343,8 @@ var WeixinGatewayService = class {
13505
14343
  this.saveSessionMapping(clientId, result.session.sessionId);
13506
14344
  return result.session;
13507
14345
  }
13508
- async handleCommand(clientId, message, baseConfig, effectiveConfig) {
13509
- const commandText = normalizeGatewayCommandText(message.text);
14346
+ async handleCommand(clientId, message2, baseConfig, effectiveConfig) {
14347
+ const commandText = normalizeGatewayCommandText(message2.text);
13510
14348
  const [command = "", ...args] = commandText.split(/\s+/);
13511
14349
  const normalized = command.toLowerCase();
13512
14350
  if (!normalized.startsWith("/")) return false;
@@ -13515,66 +14353,66 @@ var WeixinGatewayService = class {
13515
14353
  this.resetSession(clientId);
13516
14354
  await this.sendReply(
13517
14355
  baseConfig,
13518
- message.chatId,
14356
+ message2.chatId,
13519
14357
  await this.formatStatus(clientId, this.resolveChatConfig(clientId, baseConfig), "Reset this WeChat chat. A fresh Pi session will start with your next message.")
13520
14358
  );
13521
14359
  return true;
13522
14360
  }
13523
14361
  if (normalized === "/help") {
13524
- await this.sendReply(baseConfig, message.chatId, this.formatHelp());
14362
+ await this.sendReply(baseConfig, message2.chatId, this.formatHelp());
13525
14363
  return true;
13526
14364
  }
13527
14365
  if (normalized === "/status") {
13528
- await this.sendReply(baseConfig, message.chatId, await this.formatStatus(clientId, effectiveConfig));
14366
+ await this.sendReply(baseConfig, message2.chatId, await this.formatStatus(clientId, effectiveConfig));
13529
14367
  return true;
13530
14368
  }
13531
14369
  if (normalized === "/profiles") {
13532
- await this.sendReply(baseConfig, message.chatId, await this.formatProfiles(effectiveConfig.agentProfile || "default"));
14370
+ await this.sendReply(baseConfig, message2.chatId, await this.formatProfiles(effectiveConfig.agentProfile || "default"));
13533
14371
  return true;
13534
14372
  }
13535
14373
  if (normalized === "/profile" || normalized === "/use-profile" || normalized === "/set-profile") {
13536
14374
  await this.setChatProfile(clientId, args[0]);
13537
14375
  this.resetSession(clientId);
13538
- await this.sendReply(baseConfig, message.chatId, await this.formatStatus(clientId, this.resolveChatConfig(clientId, baseConfig), "Profile updated."));
14376
+ await this.sendReply(baseConfig, message2.chatId, await this.formatStatus(clientId, this.resolveChatConfig(clientId, baseConfig), "Profile updated."));
13539
14377
  return true;
13540
14378
  }
13541
14379
  if (normalized === "/cwds" || normalized === "/pwds") {
13542
- await this.sendReply(baseConfig, message.chatId, this.formatCwdChoices(baseConfig));
14380
+ await this.sendReply(baseConfig, message2.chatId, this.formatCwdChoices(baseConfig));
13543
14381
  return true;
13544
14382
  }
13545
14383
  if (normalized === "/cwd") {
13546
14384
  if (args.length === 0) {
13547
- await this.sendReply(baseConfig, message.chatId, effectiveConfig.defaultCwd || NO_GATEWAY_FOLDERS_MESSAGE2);
14385
+ await this.sendReply(baseConfig, message2.chatId, effectiveConfig.defaultCwd || NO_GATEWAY_FOLDERS_MESSAGE2);
13548
14386
  return true;
13549
14387
  }
13550
14388
  await this.setChatCwd(clientId, args[0], baseConfig);
13551
14389
  this.resetSession(clientId);
13552
- await this.sendReply(baseConfig, message.chatId, await this.formatStatus(clientId, this.resolveChatConfig(clientId, baseConfig), "Working directory updated."));
14390
+ await this.sendReply(baseConfig, message2.chatId, await this.formatStatus(clientId, this.resolveChatConfig(clientId, baseConfig), "Working directory updated."));
13553
14391
  return true;
13554
14392
  }
13555
14393
  if (normalized === "/skillsets") {
13556
- await this.sendReply(baseConfig, message.chatId, this.formatSkillsets(effectiveConfig.skillPresetId));
14394
+ await this.sendReply(baseConfig, message2.chatId, this.formatSkillsets(effectiveConfig.skillPresetId));
13557
14395
  return true;
13558
14396
  }
13559
14397
  if (normalized === "/skillset") {
13560
14398
  await this.setChatSkillset(clientId, args.join(" "));
13561
14399
  this.resetSession(clientId);
13562
- await this.sendReply(baseConfig, message.chatId, await this.formatStatus(clientId, this.resolveChatConfig(clientId, baseConfig), "Skillset updated."));
14400
+ await this.sendReply(baseConfig, message2.chatId, await this.formatStatus(clientId, this.resolveChatConfig(clientId, baseConfig), "Skillset updated."));
13563
14401
  return true;
13564
14402
  }
13565
14403
  if (normalized === "/clear-config") {
13566
14404
  this.clearChatConfig(clientId);
13567
14405
  this.resetSession(clientId);
13568
- await this.sendReply(baseConfig, message.chatId, await this.formatStatus(clientId, this.resolveChatConfig(clientId, baseConfig), "WeChat chat config cleared."));
14406
+ await this.sendReply(baseConfig, message2.chatId, await this.formatStatus(clientId, this.resolveChatConfig(clientId, baseConfig), "WeChat chat config cleared."));
13569
14407
  return true;
13570
14408
  }
13571
14409
  } catch (error) {
13572
- await this.sendReply(baseConfig, message.chatId, `Command failed: ${error instanceof Error ? error.message : "unknown error"}
14410
+ await this.sendReply(baseConfig, message2.chatId, `Command failed: ${error instanceof Error ? error.message : "unknown error"}
13573
14411
 
13574
14412
  ${this.formatHelp()}`);
13575
14413
  return true;
13576
14414
  }
13577
- await this.sendReply(baseConfig, message.chatId, `Unknown command: ${command}
14415
+ await this.sendReply(baseConfig, message2.chatId, `Unknown command: ${command}
13578
14416
 
13579
14417
  ${this.formatHelp()}`);
13580
14418
  return true;
@@ -13649,8 +14487,8 @@ ${this.formatHelp()}`);
13649
14487
  const slot = Number(slotText.trim());
13650
14488
  if (!Number.isInteger(slot) || slot < 1 || slot > config.cwdChoices.length) throw new Error(`Usage: /cwd <1-${config.cwdChoices.length}>`);
13651
14489
  const target = config.cwdChoices[slot - 1];
13652
- if (!isAbsolute5(target)) throw new Error(`Configured CWD ${slot} must be an absolute path`);
13653
- const info = await stat7(target);
14490
+ if (!isAbsolute6(target)) throw new Error(`Configured CWD ${slot} must be an absolute path`);
14491
+ const info = await stat8(target);
13654
14492
  if (!info.isDirectory()) throw new Error(`Configured CWD ${slot} must be an existing directory`);
13655
14493
  this.saveChatConfig(clientId, { defaultCwd: target });
13656
14494
  }
@@ -13821,7 +14659,7 @@ ${this.formatHelp()}`);
13821
14659
  msg: {
13822
14660
  from_user_id: "",
13823
14661
  to_user_id: chatId,
13824
- client_id: randomUUID6(),
14662
+ client_id: randomUUID7(),
13825
14663
  message_type: MSG_TYPE_BOT,
13826
14664
  ...msg,
13827
14665
  ...withContextToken(this.getContextToken(config.accountId, chatId))
@@ -14066,122 +14904,820 @@ function safeId(value) {
14066
14904
  }
14067
14905
  function extractLastAssistantText2(messages) {
14068
14906
  for (let index = messages.length - 1; index >= 0; index -= 1) {
14069
- const message = messages[index];
14070
- if (message?.role !== "assistant") continue;
14071
- if (typeof message.content === "string") return message.content.trim();
14072
- if (Array.isArray(message.content)) {
14073
- const text = message.content.map((part) => typeof part === "string" ? part : part?.text || part?.content || "").join("").trim();
14907
+ const message2 = messages[index];
14908
+ if (message2?.role !== "assistant") continue;
14909
+ if (typeof message2.content === "string") return message2.content.trim();
14910
+ if (Array.isArray(message2.content)) {
14911
+ const text = message2.content.map((part) => typeof part === "string" ? part : part?.text || part?.content || "").join("").trim();
14074
14912
  if (text) return text;
14075
14913
  }
14076
14914
  }
14077
14915
  return void 0;
14078
14916
  }
14079
14917
 
14080
- // src/services/review-source-store.ts
14081
- import * as os8 from "os";
14082
- import * as path11 from "path";
14918
+ // src/services/wecom-gateway.ts
14919
+ import { randomBytes as randomBytes3 } from "node:crypto";
14920
+ import { stat as stat9 } from "node:fs/promises";
14921
+ import { isAbsolute as isAbsolute7 } from "node:path";
14083
14922
 
14084
- // src/services/review-source-providers.ts
14085
- import * as fs8 from "fs";
14086
- import * as os7 from "os";
14087
- import * as path10 from "path";
14923
+ // src/services/wecom-crypto.ts
14924
+ import { createDecipheriv as createDecipheriv3, createHash as createHash8, timingSafeEqual as timingSafeEqual2 } from "node:crypto";
14925
+ function verifyWecomSignature(input) {
14926
+ const expected = createHash8("sha1").update([input.token, input.timestamp, input.nonce, input.encrypted].sort().join("")).digest();
14927
+ if (!/^[0-9a-f]{40}$/i.test(input.signature)) return false;
14928
+ const actual = Buffer.from(input.signature, "hex");
14929
+ return timingSafeEqual2(expected, actual);
14930
+ }
14931
+ function decryptWecomPayload(encrypted, encodingAesKey, expectedReceiveId) {
14932
+ const key = parseEncodingAesKey(encodingAesKey);
14933
+ const ciphertext = Buffer.from(encrypted, "base64");
14934
+ if (!ciphertext.length || ciphertext.length % 16 !== 0) throw new Error("Invalid WeCom encrypted payload");
14935
+ const decipher = createDecipheriv3("aes-256-cbc", key, key.subarray(0, 16));
14936
+ decipher.setAutoPadding(false);
14937
+ const padded = Buffer.concat([decipher.update(ciphertext), decipher.final()]);
14938
+ const plaintext = removePkcs7Padding(padded);
14939
+ if (plaintext.length < 20) throw new Error("Invalid WeCom decrypted payload");
14940
+ const messageLength = plaintext.readUInt32BE(16);
14941
+ const messageStart = 20;
14942
+ const messageEnd = messageStart + messageLength;
14943
+ if (messageEnd > plaintext.length) throw new Error("Invalid WeCom message length");
14944
+ const receiveId = plaintext.subarray(messageEnd).toString("utf8");
14945
+ if (receiveId !== expectedReceiveId) throw new Error("Invalid WeCom callback receive ID");
14946
+ return plaintext.subarray(messageStart, messageEnd).toString("utf8");
14947
+ }
14948
+ function parseEncodingAesKey(value) {
14949
+ if (!/^[A-Za-z0-9]{43}$/.test(value)) throw new Error("WeCom EncodingAESKey must contain 43 letters or digits");
14950
+ const key = Buffer.from(`${value}=`, "base64");
14951
+ if (key.length !== 32) throw new Error("Invalid WeCom EncodingAESKey");
14952
+ return key;
14953
+ }
14954
+ function removePkcs7Padding(value) {
14955
+ const padding = value[value.length - 1];
14956
+ if (!padding || padding > 32 || padding > value.length) throw new Error("Invalid WeCom PKCS#7 padding");
14957
+ for (let index = value.length - padding; index < value.length; index += 1) {
14958
+ if (value[index] !== padding) throw new Error("Invalid WeCom PKCS#7 padding");
14959
+ }
14960
+ return value.subarray(0, value.length - padding);
14961
+ }
14088
14962
 
14089
- // src/services/review-source-adapters/claude-code-adapter.ts
14090
- import * as fs5 from "fs";
14091
- import * as path7 from "path";
14092
- var ClaudeCodeReviewSourceAdapter = class {
14093
- constructor(dataPath) {
14094
- this.dataPath = dataPath;
14095
- }
14096
- dataPath;
14097
- async list(options = {}) {
14098
- const items = this.readSessions().map((session) => session.item).sort((a, b) => b.modified.localeCompare(a.modified));
14099
- return this.applyListOptions(items, options);
14100
- }
14101
- async search(query, options = {}) {
14102
- const normalized = query.trim().toLowerCase();
14103
- if (!normalized) return this.list(options);
14104
- const matches = this.readSessions().filter(({ item, filePath }) => {
14105
- if (item.name?.toLowerCase().includes(normalized) || item.firstMessage?.toLowerCase().includes(normalized)) return true;
14106
- return this.readTranscript(filePath).messages.some((message) => this.messageText(message.content).toLowerCase().includes(normalized));
14107
- }).map((session) => session.item).sort((a, b) => b.modified.localeCompare(a.modified));
14108
- return this.applyListOptions(matches, options);
14963
+ // src/services/wecom-gateway.ts
14964
+ var WECOM_API_BASE_URL = "https://qyapi.weixin.qq.com";
14965
+ var TOKEN_REFRESH_SKEW_MS2 = 5 * 60 * 1e3;
14966
+ var DEDUP_TTL_MS3 = 5 * 60 * 1e3;
14967
+ var TEXT_CHUNK_MAX_BYTES = 1900;
14968
+ function generateEncodingAesKey() {
14969
+ while (true) {
14970
+ const key = randomBytes3(32).toString("base64").slice(0, 43);
14971
+ if (/^[A-Za-z0-9]+$/.test(key)) return key;
14109
14972
  }
14110
- async getTranscript(sessionId) {
14111
- const session = this.findSession(sessionId);
14112
- if (!session) throw new Error(`Transcript not found for session ${sessionId}`);
14113
- return this.readTranscript(session.filePath);
14973
+ }
14974
+ var WecomGatewayService = class {
14975
+ constructor(db, gatewaySettings, sessionService) {
14976
+ this.db = db;
14977
+ this.sessionService = sessionService;
14978
+ this.gatewaySettings = gatewaySettings || new GatewaySettingsStore(db);
14979
+ this.presetStore = new SkillPresetStore(db);
14114
14980
  }
14115
- async delete(_sessionId) {
14116
- throw new Error("Claude Code review sessions are read-only");
14981
+ db;
14982
+ sessionService;
14983
+ gatewaySettings;
14984
+ presetStore;
14985
+ seen = /* @__PURE__ */ new Map();
14986
+ queues = /* @__PURE__ */ new Map();
14987
+ tokenCache;
14988
+ environmentCallbackVerified = false;
14989
+ status() {
14990
+ const config = this.loadConfig();
14991
+ const managedBy = this.isEnvironmentManaged() ? "environment" : this.loadSavedCredentials() ? "database" : "none";
14992
+ return {
14993
+ configured: Boolean(config.corpId && config.corpSecret && config.agentId && config.callbackToken && config.encodingAesKey),
14994
+ managedBy,
14995
+ ...config.corpId ? { corpId: config.corpId } : {},
14996
+ ...config.agentId ? { agentId: config.agentId } : {},
14997
+ allowedUsers: config.allowedUsers,
14998
+ callbackVerified: managedBy === "database" ? Boolean(this.loadSavedCredentials()?.callbackVerified) : this.environmentCallbackVerified
14999
+ };
14117
15000
  }
14118
- async listProjectPaths() {
14119
- const sessions = await this.list();
14120
- return Array.from(new Set(sessions.map((item) => item.cwd).filter((cwd) => Boolean(cwd))));
15001
+ saveConfiguration(input) {
15002
+ if (this.isEnvironmentManaged()) throw new Error("WeCom credentials are managed by environment variables");
15003
+ const corpId = input.corpId.trim();
15004
+ const corpSecret = input.corpSecret.trim();
15005
+ const agentId = input.agentId.trim();
15006
+ if (!corpId || !corpSecret || !/^\d+$/.test(agentId)) throw new Error("Corp ID, app secret, and a numeric Agent ID are required");
15007
+ const allowedUsers = normalizeList(input.allowedUsers);
15008
+ const existing = this.loadSavedCredentials();
15009
+ const callbackToken = existing?.callbackToken || randomBytes3(16).toString("hex");
15010
+ const encodingAesKey = existing?.encodingAesKey || generateEncodingAesKey();
15011
+ const now = (/* @__PURE__ */ new Date()).toISOString();
15012
+ this.db.prepare(`
15013
+ INSERT INTO wecom_gateway_credentials (
15014
+ id, corp_id, corp_secret, agent_id, callback_token, encoding_aes_key,
15015
+ allowed_users_json, callback_verified_at, created_at, updated_at
15016
+ ) VALUES (1, ?, ?, ?, ?, ?, ?, NULL, ?, ?)
15017
+ ON CONFLICT(id) DO UPDATE SET
15018
+ corp_id = excluded.corp_id,
15019
+ corp_secret = excluded.corp_secret,
15020
+ agent_id = excluded.agent_id,
15021
+ callback_token = excluded.callback_token,
15022
+ encoding_aes_key = excluded.encoding_aes_key,
15023
+ allowed_users_json = excluded.allowed_users_json,
15024
+ callback_verified_at = NULL,
15025
+ updated_at = excluded.updated_at
15026
+ `).run(corpId, corpSecret, agentId, callbackToken, encodingAesKey, JSON.stringify(allowedUsers), now, now);
15027
+ this.tokenCache = void 0;
15028
+ return { status: this.status(), callbackToken, encodingAesKey };
15029
+ }
15030
+ regenerateCallbackSecrets() {
15031
+ if (this.isEnvironmentManaged()) throw new Error("WeCom credentials are managed by environment variables");
15032
+ const saved = this.loadSavedCredentials();
15033
+ if (!saved) throw new Error("Save the WeCom configuration first");
15034
+ const callbackToken = randomBytes3(16).toString("hex");
15035
+ const encodingAesKey = generateEncodingAesKey();
15036
+ this.db.prepare(`
15037
+ UPDATE wecom_gateway_credentials
15038
+ SET callback_token = ?, encoding_aes_key = ?, callback_verified_at = NULL, updated_at = ?
15039
+ WHERE id = 1
15040
+ `).run(callbackToken, encodingAesKey, (/* @__PURE__ */ new Date()).toISOString());
15041
+ return { status: this.status(), callbackToken, encodingAesKey };
15042
+ }
15043
+ disconnect() {
15044
+ if (this.isEnvironmentManaged()) throw new Error("WeCom credentials are managed by environment variables");
15045
+ const clients = this.db.prepare("SELECT client_id FROM wecom_gateway_sessions").all();
15046
+ for (const { client_id: clientId } of clients) this.sessionService.disposeSession(clientId);
15047
+ this.db.exec("DELETE FROM wecom_gateway_sessions; DELETE FROM wecom_gateway_configs; DELETE FROM wecom_gateway_credentials;");
15048
+ this.tokenCache = void 0;
14121
15049
  }
14122
- readSessions() {
14123
- return this.sessionFiles().flatMap((filePath) => {
14124
- try {
14125
- const records = this.readRecords(filePath);
14126
- if (!records.length) return [];
14127
- const stat8 = fs5.statSync(filePath);
14128
- const sessionRecord = records.find((record) => record.sessionId || record.session_id);
14129
- const sessionId = sessionRecord?.sessionId || sessionRecord?.session_id || path7.basename(filePath, ".jsonl");
14130
- const cwd = records.find((record) => record.cwd)?.cwd;
14131
- const timestamps = records.map((record) => this.timestamp(record.timestamp)).filter((value) => value !== void 0);
14132
- const messages = this.recordsToMessages(records);
14133
- const title = [...records].reverse().find((record) => typeof record.aiTitle === "string")?.aiTitle;
14134
- const firstUserMessage = messages.find((message) => message.role === "user");
14135
- return [{
14136
- filePath,
14137
- item: {
14138
- id: sessionId,
14139
- sourceId: "",
14140
- name: title,
14141
- path: filePath,
14142
- 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(),
14145
- messageCount: messages.length,
14146
- firstMessage: firstUserMessage ? this.messageText(firstUserMessage.content) : void 0
14147
- }
14148
- }];
14149
- } catch {
14150
- return [];
14151
- }
15050
+ async testConnection() {
15051
+ const config = this.requireConfig();
15052
+ this.tokenCache = void 0;
15053
+ await this.getAccessToken(config);
15054
+ return this.status();
15055
+ }
15056
+ handleVerification(query) {
15057
+ const config = this.requireConfig();
15058
+ const encrypted = stringValue3(query.echostr);
15059
+ this.verifySignature(query, encrypted, config);
15060
+ const challenge = decryptWecomPayload(encrypted, config.encodingAesKey, config.corpId);
15061
+ this.markCallbackVerified();
15062
+ return challenge;
15063
+ }
15064
+ handleCallback(query, body) {
15065
+ const config = this.requireConfig();
15066
+ const encrypted = xmlValue(body, "Encrypt");
15067
+ if (!encrypted) throw gatewayError("Invalid WeCom callback envelope", 400);
15068
+ this.verifySignature(query, encrypted, config);
15069
+ const message2 = parseWecomMessage(decryptWecomPayload(encrypted, config.encodingAesKey, config.corpId));
15070
+ this.markCallbackVerified();
15071
+ if (!message2 || message2.agentId !== config.agentId) return "success";
15072
+ if (config.allowedUsers.length && !config.allowedUsers.includes(message2.userId)) return "success";
15073
+ if (this.markDuplicate(message2.messageId)) return "success";
15074
+ if (!["text", "image", "voice"].includes(message2.messageType)) {
15075
+ void this.sendReply(config, message2.userId, "This WeCom gateway supports text, image, and voice messages only.").catch((error) => {
15076
+ console.warn("[wecom-gateway] unsupported-message reply failed:", error instanceof Error ? error.message : error);
15077
+ });
15078
+ return "success";
15079
+ }
15080
+ this.enqueue({
15081
+ messageId: message2.messageId,
15082
+ userId: message2.userId,
15083
+ text: message2.text,
15084
+ messageType: message2.messageType,
15085
+ mediaId: message2.mediaId,
15086
+ picUrl: message2.picUrl
15087
+ }, config);
15088
+ return "success";
15089
+ }
15090
+ verifySignature(query, encrypted, config) {
15091
+ const valid = encrypted && verifyWecomSignature({
15092
+ token: config.callbackToken,
15093
+ timestamp: stringValue3(query.timestamp),
15094
+ nonce: stringValue3(query.nonce),
15095
+ encrypted,
15096
+ signature: stringValue3(query.msg_signature)
14152
15097
  });
15098
+ if (!valid) throw gatewayError("Invalid WeCom callback signature", 401);
14153
15099
  }
14154
- sessionFiles() {
14155
- if (!fs5.existsSync(this.dataPath)) return [];
14156
- const files = [];
14157
- const addJsonlFiles = (directory) => {
14158
- for (const entry of fs5.readdirSync(directory, { withFileTypes: true })) {
14159
- if (entry.isFile() && entry.name.endsWith(".jsonl")) files.push(path7.join(directory, entry.name));
14160
- }
14161
- };
14162
- addJsonlFiles(this.dataPath);
14163
- for (const entry of fs5.readdirSync(this.dataPath, { withFileTypes: true })) {
14164
- if (entry.isDirectory()) addJsonlFiles(path7.join(this.dataPath, entry.name));
15100
+ requireConfig() {
15101
+ const config = this.loadConfig();
15102
+ if (!config.corpId || !config.corpSecret || !config.agentId || !config.callbackToken || !config.encodingAesKey) {
15103
+ throw gatewayError("WeCom gateway is not configured", 503);
14165
15104
  }
14166
- return files;
14167
- }
14168
- findSession(sessionId) {
14169
- if (!sessionId || path7.basename(sessionId) !== sessionId) throw new Error("Invalid review session ID");
14170
- return this.readSessions().find((session) => session.item.id === sessionId);
15105
+ return config;
14171
15106
  }
14172
- readTranscript(filePath) {
14173
- const records = this.readRecords(filePath);
15107
+ loadConfig() {
15108
+ const gatewaySettings = this.gatewaySettings.get();
15109
+ const saved = this.loadSavedCredentials();
15110
+ const skillset = this.resolveSkillsetConfig(gatewaySettings.defaultSkillset);
14174
15111
  return {
14175
- messages: this.recordsToMessages(records),
14176
- metadata: { sessionId: path7.basename(filePath, ".jsonl"), format: "claude-code-jsonl" }
15112
+ corpId: process.env.PI_CLOUD_WECOM_CORP_ID?.trim() || saved?.corpId || "",
15113
+ corpSecret: process.env.PI_CLOUD_WECOM_CORP_SECRET?.trim() || saved?.corpSecret || "",
15114
+ agentId: process.env.PI_CLOUD_WECOM_AGENT_ID?.trim() || saved?.agentId || "",
15115
+ callbackToken: process.env.PI_CLOUD_WECOM_CALLBACK_TOKEN?.trim() || saved?.callbackToken || "",
15116
+ encodingAesKey: process.env.PI_CLOUD_WECOM_ENCODING_AES_KEY?.trim() || saved?.encodingAesKey || "",
15117
+ allowedUsers: process.env.PI_CLOUD_WECOM_ALLOWED_USERS !== void 0 ? parseList2(process.env.PI_CLOUD_WECOM_ALLOWED_USERS) : saved?.allowedUsers || [],
15118
+ defaultCwd: gatewaySettings.cwds[0],
15119
+ cwdChoices: gatewaySettings.cwds,
15120
+ agentProfile: gatewaySettings.defaultProfile,
15121
+ modelProvider: gatewaySettings.defaultModelProvider,
15122
+ modelId: gatewaySettings.defaultModelId,
15123
+ ...skillset
14177
15124
  };
14178
15125
  }
14179
- readRecords(filePath) {
14180
- return fs5.readFileSync(filePath, "utf8").split(/\r?\n/).filter(Boolean).flatMap((line) => {
14181
- try {
14182
- return [JSON.parse(line)];
14183
- } catch {
14184
- return [];
15126
+ isEnvironmentManaged() {
15127
+ return [
15128
+ "PI_CLOUD_WECOM_CORP_ID",
15129
+ "PI_CLOUD_WECOM_CORP_SECRET",
15130
+ "PI_CLOUD_WECOM_AGENT_ID",
15131
+ "PI_CLOUD_WECOM_CALLBACK_TOKEN",
15132
+ "PI_CLOUD_WECOM_ENCODING_AES_KEY"
15133
+ ].some((key) => Boolean(process.env[key]?.trim()));
15134
+ }
15135
+ loadSavedCredentials() {
15136
+ const row = this.db.prepare(`
15137
+ SELECT corp_id, corp_secret, agent_id, callback_token, encoding_aes_key,
15138
+ allowed_users_json, callback_verified_at
15139
+ FROM wecom_gateway_credentials WHERE id = 1
15140
+ `).get();
15141
+ if (!row) return null;
15142
+ let allowedUsers = [];
15143
+ try {
15144
+ allowedUsers = normalizeList(JSON.parse(row.allowed_users_json));
15145
+ } catch {
15146
+ }
15147
+ return {
15148
+ corpId: row.corp_id,
15149
+ corpSecret: row.corp_secret,
15150
+ agentId: row.agent_id,
15151
+ callbackToken: row.callback_token,
15152
+ encodingAesKey: row.encoding_aes_key,
15153
+ allowedUsers,
15154
+ callbackVerified: Boolean(row.callback_verified_at)
15155
+ };
15156
+ }
15157
+ markCallbackVerified() {
15158
+ if (this.isEnvironmentManaged()) {
15159
+ this.environmentCallbackVerified = true;
15160
+ return;
15161
+ }
15162
+ 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());
15163
+ }
15164
+ markDuplicate(id) {
15165
+ const now = Date.now();
15166
+ for (const [key, expiresAt] of this.seen) {
15167
+ if (expiresAt <= now) this.seen.delete(key);
15168
+ }
15169
+ if (this.seen.has(id)) return true;
15170
+ this.seen.set(id, now + DEDUP_TTL_MS3);
15171
+ return false;
15172
+ }
15173
+ enqueue(message2, config) {
15174
+ const clientId = `wecom:dm:${message2.userId}`;
15175
+ const previous = this.queues.get(clientId) || Promise.resolve();
15176
+ const next = previous.catch(() => void 0).then(() => this.processMessage(clientId, message2, config)).catch((error) => console.error("[wecom-gateway] message handling failed:", error)).finally(() => {
15177
+ if (this.queues.get(clientId) === next) this.queues.delete(clientId);
15178
+ });
15179
+ this.queues.set(clientId, next);
15180
+ }
15181
+ async processMessage(clientId, message2, config) {
15182
+ const effectiveConfig = this.resolveChatConfig(clientId, config);
15183
+ if (await this.handleCommand(clientId, message2, config, effectiveConfig)) return;
15184
+ if (!effectiveConfig.defaultCwd) {
15185
+ await this.sendReply(config, message2.userId, "No gateway folders configured. Add at least one allowed folder in Web UI Settings > Gateway.");
15186
+ return;
15187
+ }
15188
+ let promptText = message2.text;
15189
+ if (message2.messageType === "voice") {
15190
+ if (!message2.mediaId) {
15191
+ await this.sendReply(config, message2.userId, "The WeCom voice message did not include downloadable media. Try sending it again.");
15192
+ return;
15193
+ }
15194
+ try {
15195
+ promptText = (await transcribeAudio(await this.downloadVoice(config, message2.mediaId), "audio/amr")).trim();
15196
+ } catch (error) {
15197
+ console.warn("[wecom-gateway] failed to transcribe inbound voice:", error instanceof Error ? error.message : error);
15198
+ await this.sendReply(config, message2.userId, "The voice message could not be transcribed. Check the speech-to-text configuration and try again.");
15199
+ return;
15200
+ }
15201
+ if (!promptText) {
15202
+ await this.sendReply(config, message2.userId, "The voice message did not contain recognizable speech. Try sending it again.");
15203
+ return;
15204
+ }
15205
+ }
15206
+ const session = await this.ensureSession(clientId, effectiveConfig);
15207
+ let images = [];
15208
+ if (message2.messageType === "image") {
15209
+ if (!message2.mediaId) {
15210
+ await this.sendReply(config, message2.userId, "The WeCom image message did not include downloadable media. Try sending it again.");
15211
+ return;
15212
+ }
15213
+ try {
15214
+ images = [await this.downloadImage(config, message2.mediaId)];
15215
+ } catch (error) {
15216
+ console.warn("[wecom-gateway] failed to download inbound image:", error instanceof Error ? error.message : error);
15217
+ await this.sendReply(config, message2.userId, "The image could not be downloaded from WeCom. Try sending it again.");
15218
+ return;
15219
+ }
15220
+ }
15221
+ const imageResult = validateImages(images, session.model);
15222
+ if (!imageResult.ok) {
15223
+ await this.sendReply(config, message2.userId, imageResult.message);
15224
+ return;
15225
+ }
15226
+ promptText ||= imageResult.images.length ? "Please analyze the attached image." : "";
15227
+ const chunks = [];
15228
+ const unsubscribe = session.subscribe((event) => {
15229
+ const update = event?.type === "message_update" ? event.assistantMessageEvent : void 0;
15230
+ if (update?.type === "text_delta" && typeof update.delta === "string") chunks.push(update.delta);
15231
+ });
15232
+ try {
15233
+ await this.sessionService.runForegroundWithClientProfileProxy(clientId, () => session.prompt(promptText, imageResult.images.length ? { images: imageResult.images } : void 0));
15234
+ } finally {
15235
+ unsubscribe();
15236
+ }
15237
+ const response = chunks.join("").trim() || extractLastAssistantText3(session.messages) || "(Pi finished without a text response.)";
15238
+ await this.sendReply(config, message2.userId, response);
15239
+ }
15240
+ async ensureSession(clientId, config) {
15241
+ await this.sessionService.setClientAgentProfile(clientId, config.agentProfile || "default");
15242
+ const active = this.sessionService.getSession(clientId);
15243
+ if (active) return active;
15244
+ const mapped = this.db.prepare("SELECT pi_session_id FROM wecom_gateway_sessions WHERE client_id = ?").get(clientId);
15245
+ if (mapped) {
15246
+ const persisted = await this.sessionService.findPersistedSession(clientId, mapped.pi_session_id);
15247
+ if (persisted) return this.sessionService.resumeSession(clientId, persisted.path);
15248
+ }
15249
+ const preset = config.skillPresetId ? this.presetStore.getById(config.skillPresetId) : null;
15250
+ const result = await this.sessionService.createSession(clientId, {
15251
+ cwd: config.defaultCwd,
15252
+ modelProvider: config.modelProvider,
15253
+ modelId: config.modelId,
15254
+ enabledSkills: preset?.mode === "enabled" ? preset.skills : void 0,
15255
+ disabledSkills: preset?.mode === "disabled" ? preset.skills : void 0,
15256
+ presetId: preset?.id,
15257
+ skillMode: preset?.mode || config.skillMode || "all"
15258
+ });
15259
+ const now = (/* @__PURE__ */ new Date()).toISOString();
15260
+ this.db.prepare(`
15261
+ INSERT INTO wecom_gateway_sessions (session_key, client_id, pi_session_id, created_at, updated_at)
15262
+ VALUES (?, ?, ?, ?, ?)
15263
+ ON CONFLICT(session_key) DO UPDATE SET pi_session_id = excluded.pi_session_id, updated_at = excluded.updated_at
15264
+ `).run(clientId, clientId, result.session.sessionId, now, now);
15265
+ return result.session;
15266
+ }
15267
+ async handleCommand(clientId, message2, baseConfig, effectiveConfig) {
15268
+ const commandText = normalizeGatewayCommandText(message2.text);
15269
+ const [command = "", ...args] = commandText.split(/\s+/);
15270
+ const normalized = command.toLowerCase();
15271
+ if (!normalized.startsWith("/")) return false;
15272
+ try {
15273
+ if (normalized === "/new" || normalized === "/reset") {
15274
+ this.resetSession(clientId);
15275
+ await this.sendReply(baseConfig, message2.userId, await this.formatStatus(clientId, this.resolveChatConfig(clientId, baseConfig), "Reset this WeCom chat. A fresh Pi session will start with your next message."));
15276
+ return true;
15277
+ }
15278
+ if (normalized === "/help") {
15279
+ await this.sendReply(baseConfig, message2.userId, this.formatHelp());
15280
+ return true;
15281
+ }
15282
+ if (normalized === "/status") {
15283
+ await this.sendReply(baseConfig, message2.userId, await this.formatStatus(clientId, effectiveConfig));
15284
+ return true;
15285
+ }
15286
+ if (normalized === "/profiles") {
15287
+ const profiles = await this.sessionService.listAgentProfiles();
15288
+ await this.sendReply(baseConfig, message2.userId, ["Available profiles:", ...profiles.map((profile) => `${profile.id === (effectiveConfig.agentProfile || "default") ? "*" : " "} ${profile.id}`)].join("\n"));
15289
+ return true;
15290
+ }
15291
+ if (normalized === "/profile" || normalized === "/use-profile" || normalized === "/set-profile") {
15292
+ const profile = args[0]?.trim();
15293
+ if (!profile) throw new Error("Usage: /profile <profile-id>");
15294
+ const profiles = await this.sessionService.listAgentProfiles();
15295
+ if (!profiles.some((item) => item.id === profile)) throw new Error(`Unknown profile: ${profile}`);
15296
+ this.saveChatConfig(clientId, { agentProfile: profile });
15297
+ this.resetSession(clientId);
15298
+ await this.sendReply(baseConfig, message2.userId, await this.formatStatus(clientId, this.resolveChatConfig(clientId, baseConfig), "Profile updated."));
15299
+ return true;
15300
+ }
15301
+ if (normalized === "/cwds" || normalized === "/pwds") {
15302
+ await this.sendReply(baseConfig, message2.userId, baseConfig.cwdChoices.length ? baseConfig.cwdChoices.map((cwd, index) => `${index + 1}: ${cwd}`).join("\n") : "No gateway folders configured.");
15303
+ return true;
15304
+ }
15305
+ if (normalized === "/cwd") {
15306
+ const slot = Number(args[0]);
15307
+ if (!Number.isInteger(slot) || slot < 1 || slot > baseConfig.cwdChoices.length) throw new Error(`Usage: /cwd <1-${baseConfig.cwdChoices.length}>`);
15308
+ const target = baseConfig.cwdChoices[slot - 1];
15309
+ if (!isAbsolute7(target) || !(await stat9(target)).isDirectory()) throw new Error(`Configured CWD ${slot} must be an existing absolute directory`);
15310
+ this.saveChatConfig(clientId, { defaultCwd: target });
15311
+ this.resetSession(clientId);
15312
+ await this.sendReply(baseConfig, message2.userId, await this.formatStatus(clientId, this.resolveChatConfig(clientId, baseConfig), "Working directory updated."));
15313
+ return true;
15314
+ }
15315
+ if (normalized === "/skillsets") {
15316
+ await this.sendReply(baseConfig, message2.userId, this.formatSkillsets(effectiveConfig.skillPresetId));
15317
+ return true;
15318
+ }
15319
+ if (normalized === "/skillset") {
15320
+ const selected = args.join(" ").trim();
15321
+ if (!selected) throw new Error("Usage: /skillset <all|preset-name>");
15322
+ this.saveChatConfig(clientId, this.resolveSkillsetConfig(selected, true));
15323
+ this.resetSession(clientId);
15324
+ await this.sendReply(baseConfig, message2.userId, await this.formatStatus(clientId, this.resolveChatConfig(clientId, baseConfig), "Skillset updated."));
15325
+ return true;
15326
+ }
15327
+ if (normalized === "/clear-config") {
15328
+ this.db.prepare("DELETE FROM wecom_gateway_configs WHERE client_id = ?").run(clientId);
15329
+ this.resetSession(clientId);
15330
+ await this.sendReply(baseConfig, message2.userId, await this.formatStatus(clientId, this.resolveChatConfig(clientId, baseConfig), "WeCom chat config cleared."));
15331
+ return true;
15332
+ }
15333
+ } catch (error) {
15334
+ await this.sendReply(baseConfig, message2.userId, `Command failed: ${error instanceof Error ? error.message : "unknown error"}
15335
+
15336
+ ${this.formatHelp()}`);
15337
+ return true;
15338
+ }
15339
+ await this.sendReply(baseConfig, message2.userId, `Unknown command: ${command}
15340
+
15341
+ ${this.formatHelp()}`);
15342
+ return true;
15343
+ }
15344
+ resolveChatConfig(clientId, base) {
15345
+ const chat = this.loadChatConfig(clientId);
15346
+ const useBaseModel = !chat.agentProfile || chat.agentProfile === base.agentProfile;
15347
+ return {
15348
+ ...base,
15349
+ agentProfile: chat.agentProfile || base.agentProfile,
15350
+ defaultCwd: chat.defaultCwd && base.cwdChoices.includes(chat.defaultCwd) ? chat.defaultCwd : base.defaultCwd,
15351
+ modelProvider: useBaseModel ? base.modelProvider : void 0,
15352
+ modelId: useBaseModel ? base.modelId : void 0,
15353
+ skillMode: chat.skillMode || base.skillMode,
15354
+ skillPresetId: chat.skillMode ? chat.skillPresetId : base.skillPresetId
15355
+ };
15356
+ }
15357
+ loadChatConfig(clientId) {
15358
+ const row = this.db.prepare("SELECT agent_profile, default_cwd, skill_mode, skill_preset_id FROM wecom_gateway_configs WHERE client_id = ?").get(clientId);
15359
+ return row ? {
15360
+ agentProfile: row.agent_profile || void 0,
15361
+ defaultCwd: row.default_cwd || void 0,
15362
+ skillMode: row.skill_mode || void 0,
15363
+ skillPresetId: row.skill_preset_id
15364
+ } : {};
15365
+ }
15366
+ saveChatConfig(clientId, patch) {
15367
+ const next = { ...this.loadChatConfig(clientId), ...patch };
15368
+ const now = (/* @__PURE__ */ new Date()).toISOString();
15369
+ this.db.prepare(`
15370
+ INSERT INTO wecom_gateway_configs (client_id, agent_profile, default_cwd, skill_mode, skill_preset_id, created_at, updated_at)
15371
+ VALUES (?, ?, ?, ?, ?, ?, ?)
15372
+ ON CONFLICT(client_id) DO UPDATE SET
15373
+ agent_profile = excluded.agent_profile,
15374
+ default_cwd = excluded.default_cwd,
15375
+ skill_mode = excluded.skill_mode,
15376
+ skill_preset_id = excluded.skill_preset_id,
15377
+ updated_at = excluded.updated_at
15378
+ `).run(clientId, next.agentProfile || null, next.defaultCwd || null, next.skillMode || null, next.skillPresetId || null, now, now);
15379
+ }
15380
+ resetSession(clientId) {
15381
+ this.sessionService.disposeSession(clientId);
15382
+ this.db.prepare("DELETE FROM wecom_gateway_sessions WHERE client_id = ?").run(clientId);
15383
+ }
15384
+ async formatStatus(clientId, config, prefix) {
15385
+ const profiles = await this.sessionService.listAgentProfiles();
15386
+ const profile = profiles.find((item) => item.id === (config.agentProfile || "default")) || profiles[0];
15387
+ const active = this.sessionService.getSession(clientId);
15388
+ return [
15389
+ prefix,
15390
+ "Pi session status",
15391
+ "",
15392
+ `- Profile: ${profile?.id || "default"}`,
15393
+ `- CWD: ${config.defaultCwd || "(not configured)"}`,
15394
+ `- Provider: ${active?.model?.provider || config.modelProvider || profile?.defaultProvider || "(profile default)"}`,
15395
+ `- Model: ${active?.model?.id || config.modelId || profile?.defaultModel || "(profile default)"}`,
15396
+ `- Skillset: ${this.formatSkillsetName(config.skillPresetId)}`,
15397
+ `- Session: ${active ? `active (${active.sessionId})` : "not started"}`
15398
+ ].filter((line) => line !== void 0).join("\n");
15399
+ }
15400
+ formatHelp() {
15401
+ return [
15402
+ "Pi Cloud WeCom commands",
15403
+ "/new - reset this chat session",
15404
+ "/status - show current configuration",
15405
+ "/profiles - list profiles",
15406
+ "/profile <id> - switch profile",
15407
+ "/cwds - list allowed folders",
15408
+ "/cwd <number> - switch folder",
15409
+ "/skillsets - list skillsets",
15410
+ "/skillset <name> - switch skillset",
15411
+ "/clear-config - restore gateway defaults",
15412
+ "/help - show this help",
15413
+ "",
15414
+ GATEWAY_COMMON_ALIAS_HELP
15415
+ ].join("\n");
15416
+ }
15417
+ resolveSkillsetConfig(skillset, strict = false) {
15418
+ const selected = skillset?.trim();
15419
+ if (!selected || selected.toLowerCase() === "all") return { skillMode: "all", skillPresetId: null };
15420
+ const preset = this.findSkillPreset(selected);
15421
+ if (preset) return { skillMode: preset.mode, skillPresetId: preset.id };
15422
+ if (strict) throw new Error(`Unknown skillset: ${selected}`);
15423
+ console.warn("[wecom-gateway] default gateway skillset was not found; using all skills", { skillset: selected });
15424
+ return { skillMode: "all", skillPresetId: null };
15425
+ }
15426
+ formatSkillsets(currentPresetId) {
15427
+ const presets = this.presetStore.list();
15428
+ return [
15429
+ "Available skillsets:",
15430
+ `${currentPresetId ? " " : "*"} all - use all skills`,
15431
+ ...presets.map((preset) => `${preset.id === currentPresetId ? "*" : " "} ${preset.name} (${preset.mode}: ${preset.skills.join(", ") || "none"})`)
15432
+ ].join("\n");
15433
+ }
15434
+ formatSkillsetName(presetId) {
15435
+ if (!presetId) return "all";
15436
+ const preset = this.presetStore.getById(presetId);
15437
+ return preset ? `${preset.name} (${preset.mode})` : `unknown preset (${presetId})`;
15438
+ }
15439
+ findSkillPreset(nameOrId) {
15440
+ const normalized = nameOrId.trim().toLowerCase();
15441
+ return this.presetStore.list().find((preset) => preset.id === nameOrId || preset.name.toLowerCase() === normalized) || null;
15442
+ }
15443
+ async sendReply(config, userId, text) {
15444
+ for (const chunk of splitUtf8Text(formatWecomMarkdown(text), TEXT_CHUNK_MAX_BYTES)) {
15445
+ let token = await this.getAccessToken(config);
15446
+ let data = await this.sendApplicationMarkdown(config, token, userId, chunk);
15447
+ if ([40014, 42001].includes(Number(data.errcode))) {
15448
+ this.tokenCache = void 0;
15449
+ token = await this.getAccessToken(config);
15450
+ data = await this.sendApplicationMarkdown(config, token, userId, chunk);
15451
+ }
15452
+ if (Number(data.errcode) !== 0) throw new Error(`WeCom send message failed: ${stringValue3(data.errmsg) || `errcode=${data.errcode}`}`);
15453
+ }
15454
+ }
15455
+ async sendApplicationMarkdown(config, token, userId, text) {
15456
+ const response = await fetch(`${WECOM_API_BASE_URL}/cgi-bin/message/send?access_token=${encodeURIComponent(token)}`, {
15457
+ method: "POST",
15458
+ headers: { "content-type": "application/json" },
15459
+ body: JSON.stringify({
15460
+ touser: userId,
15461
+ msgtype: "markdown",
15462
+ agentid: Number(config.agentId),
15463
+ markdown: { content: text },
15464
+ safe: 0,
15465
+ enable_duplicate_check: 1,
15466
+ duplicate_check_interval: 1800
15467
+ })
15468
+ });
15469
+ return parseWecomResponse(response);
15470
+ }
15471
+ async downloadImage(config, mediaId) {
15472
+ const bytes = await this.downloadMedia(config, mediaId, MAX_IMAGE_BYTES, "An image is larger than 10 MB.");
15473
+ const mimeType = sniffImageMimeType(bytes);
15474
+ if (!mimeType) throw new Error("WeCom media response was not a supported image");
15475
+ return { type: "image", data: bytes.toString("base64"), mimeType };
15476
+ }
15477
+ downloadVoice(config, mediaId) {
15478
+ return this.downloadMedia(config, mediaId, MAX_AUDIO_BYTES, "A voice message is larger than 25 MB.");
15479
+ }
15480
+ async downloadMedia(config, mediaId, maxBytes, tooLargeMessage) {
15481
+ for (let attempt = 0; attempt < 2; attempt += 1) {
15482
+ const token = await this.getAccessToken(config);
15483
+ const response = await fetch(`${WECOM_API_BASE_URL}/cgi-bin/media/get?access_token=${encodeURIComponent(token)}&media_id=${encodeURIComponent(mediaId)}`);
15484
+ const contentType = response.headers.get("content-type") || "";
15485
+ if (contentType.toLowerCase().includes("application/json")) {
15486
+ const data = await parseWecomResponse(response);
15487
+ const errorCode = Number(data.errcode);
15488
+ if (attempt === 0 && [40014, 42001].includes(errorCode)) {
15489
+ this.tokenCache = void 0;
15490
+ continue;
15491
+ }
15492
+ throw new Error(`WeCom media download failed: ${stringValue3(data.errmsg) || `errcode=${data.errcode}`}`);
15493
+ }
15494
+ if (!response.ok) throw new Error(`WeCom media download failed: HTTP ${response.status}`);
15495
+ return readResponseBytes(response, maxBytes, tooLargeMessage);
15496
+ }
15497
+ throw new Error("WeCom media download failed after refreshing the access token");
15498
+ }
15499
+ async getAccessToken(config) {
15500
+ const now = Date.now();
15501
+ if (this.tokenCache && this.tokenCache.expiresAt - TOKEN_REFRESH_SKEW_MS2 > now) return this.tokenCache.token;
15502
+ const response = await fetch(`${WECOM_API_BASE_URL}/cgi-bin/gettoken?corpid=${encodeURIComponent(config.corpId)}&corpsecret=${encodeURIComponent(config.corpSecret)}`);
15503
+ const data = await parseWecomResponse(response);
15504
+ const token = stringValue3(data.access_token);
15505
+ if (Number(data.errcode) !== 0 || !token) throw new Error(`WeCom access token failed: ${stringValue3(data.errmsg) || `errcode=${data.errcode}`}`);
15506
+ this.tokenCache = { token, expiresAt: now + (Number(data.expires_in) || 7200) * 1e3 };
15507
+ return token;
15508
+ }
15509
+ };
15510
+ async function readResponseBytes(response, maxBytes, tooLargeMessage) {
15511
+ const contentLength = Number(response.headers.get("content-length"));
15512
+ if (Number.isFinite(contentLength) && contentLength > maxBytes) {
15513
+ await response.body?.cancel();
15514
+ throw new Error(tooLargeMessage);
15515
+ }
15516
+ if (!response.body) return Buffer.alloc(0);
15517
+ const reader = response.body.getReader();
15518
+ const chunks = [];
15519
+ let totalBytes = 0;
15520
+ try {
15521
+ while (true) {
15522
+ const { done, value } = await reader.read();
15523
+ if (done) break;
15524
+ totalBytes += value.byteLength;
15525
+ if (totalBytes > maxBytes) {
15526
+ await reader.cancel();
15527
+ throw new Error(tooLargeMessage);
15528
+ }
15529
+ chunks.push(Buffer.from(value));
15530
+ }
15531
+ } finally {
15532
+ reader.releaseLock();
15533
+ }
15534
+ return Buffer.concat(chunks, totalBytes);
15535
+ }
15536
+ function parseWecomMessage(xml) {
15537
+ const messageType = xmlValue(xml, "MsgType");
15538
+ const userId = xmlValue(xml, "FromUserName");
15539
+ const messageId = xmlValue(xml, "MsgId");
15540
+ const agentId = xmlValue(xml, "AgentID");
15541
+ if (!messageType || !userId || !messageId || !agentId) return null;
15542
+ return {
15543
+ messageType,
15544
+ userId,
15545
+ messageId,
15546
+ agentId,
15547
+ text: xmlValue(xml, "Content").trim(),
15548
+ mediaId: xmlValue(xml, "MediaId") || void 0,
15549
+ picUrl: xmlValue(xml, "PicUrl") || void 0
15550
+ };
15551
+ }
15552
+ function xmlValue(xml, tag) {
15553
+ const match = new RegExp(`<${tag}>(?:<!\\[CDATA\\[([\\s\\S]*?)\\]\\]>|([\\s\\S]*?))<\\/${tag}>`, "i").exec(xml);
15554
+ return decodeXmlEntities((match?.[1] ?? match?.[2] ?? "").trim());
15555
+ }
15556
+ function decodeXmlEntities(value) {
15557
+ return value.replace(/&lt;/g, "<").replace(/&gt;/g, ">").replace(/&quot;/g, '"').replace(/&apos;/g, "'").replace(/&amp;/g, "&");
15558
+ }
15559
+ function parseList2(value) {
15560
+ return Array.from(new Set((value || "").split(",").map((item) => item.trim()).filter(Boolean)));
15561
+ }
15562
+ function normalizeList(value) {
15563
+ return Array.from(new Set((Array.isArray(value) ? value : []).filter((item) => typeof item === "string").map((item) => item.trim()).filter(Boolean)));
15564
+ }
15565
+ function stringValue3(value) {
15566
+ return typeof value === "string" ? value : value === void 0 || value === null ? "" : String(value);
15567
+ }
15568
+ function gatewayError(message2, statusCode) {
15569
+ const error = new Error(message2);
15570
+ error.statusCode = statusCode;
15571
+ return error;
15572
+ }
15573
+ async function parseWecomResponse(response) {
15574
+ const text = await response.text();
15575
+ let data = {};
15576
+ try {
15577
+ data = JSON.parse(text);
15578
+ } catch {
15579
+ }
15580
+ if (!response.ok) throw new Error(`WeCom HTTP ${response.status}: ${text.slice(0, 200)}`);
15581
+ return data;
15582
+ }
15583
+ function extractLastAssistantText3(messages) {
15584
+ for (let index = messages.length - 1; index >= 0; index -= 1) {
15585
+ const message2 = messages[index];
15586
+ if (message2?.role !== "assistant") continue;
15587
+ if (typeof message2.content === "string" && message2.content.trim()) return message2.content.trim();
15588
+ if (!Array.isArray(message2.content)) continue;
15589
+ const text = message2.content.filter((part) => part?.type === "text" && typeof part.text === "string").map((part) => part.text).join("").trim();
15590
+ if (text) return text;
15591
+ }
15592
+ return void 0;
15593
+ }
15594
+ function formatWecomMarkdown(text) {
15595
+ return text.replace(/^([ \t]*)[-+*][ \t]+/gm, (_match, indentation) => `${indentation}${indentation ? "\u25E6" : "\u2022"} `);
15596
+ }
15597
+ function splitUtf8Text(text, maxBytes) {
15598
+ if (Buffer.byteLength(text, "utf8") <= maxBytes) return [text];
15599
+ const chunks = [];
15600
+ let current = "";
15601
+ let currentBytes = 0;
15602
+ for (const character of text) {
15603
+ const characterBytes = Buffer.byteLength(character, "utf8");
15604
+ if (current && currentBytes + characterBytes > maxBytes) {
15605
+ chunks.push(current);
15606
+ current = "";
15607
+ currentBytes = 0;
15608
+ }
15609
+ current += character;
15610
+ currentBytes += characterBytes;
15611
+ }
15612
+ if (current) chunks.push(current);
15613
+ return chunks;
15614
+ }
15615
+
15616
+ // src/services/review-source-store.ts
15617
+ import * as os8 from "os";
15618
+ import * as path11 from "path";
15619
+
15620
+ // src/services/review-source-providers.ts
15621
+ import * as fs8 from "fs";
15622
+ import * as os7 from "os";
15623
+ import * as path10 from "path";
15624
+
15625
+ // src/services/review-source-adapters/claude-code-adapter.ts
15626
+ import * as fs5 from "fs";
15627
+ import * as path7 from "path";
15628
+ var ClaudeCodeReviewSourceAdapter = class {
15629
+ constructor(dataPath) {
15630
+ this.dataPath = dataPath;
15631
+ }
15632
+ dataPath;
15633
+ async list(options = {}) {
15634
+ const items = this.readSessions().map((session) => session.item).sort((a, b) => b.modified.localeCompare(a.modified));
15635
+ return this.applyListOptions(items, options);
15636
+ }
15637
+ async search(query, options = {}) {
15638
+ const normalized = query.trim().toLowerCase();
15639
+ if (!normalized) return this.list(options);
15640
+ const matches = this.readSessions().filter(({ item, filePath }) => {
15641
+ if (item.name?.toLowerCase().includes(normalized) || item.firstMessage?.toLowerCase().includes(normalized)) return true;
15642
+ return this.readTranscript(filePath).messages.some((message2) => this.messageText(message2.content).toLowerCase().includes(normalized));
15643
+ }).map((session) => session.item).sort((a, b) => b.modified.localeCompare(a.modified));
15644
+ return this.applyListOptions(matches, options);
15645
+ }
15646
+ async getTranscript(sessionId) {
15647
+ const session = this.findSession(sessionId);
15648
+ if (!session) throw new Error(`Transcript not found for session ${sessionId}`);
15649
+ return this.readTranscript(session.filePath);
15650
+ }
15651
+ async delete(_sessionId) {
15652
+ throw new Error("Claude Code review sessions are read-only");
15653
+ }
15654
+ async listProjectPaths() {
15655
+ const sessions = await this.list();
15656
+ return Array.from(new Set(sessions.map((item) => item.cwd).filter((cwd) => Boolean(cwd))));
15657
+ }
15658
+ readSessions() {
15659
+ return this.sessionFiles().flatMap((filePath) => {
15660
+ try {
15661
+ const records = this.readRecords(filePath);
15662
+ if (!records.length) return [];
15663
+ const stat10 = fs5.statSync(filePath);
15664
+ const sessionRecord = records.find((record) => record.sessionId || record.session_id);
15665
+ const sessionId = sessionRecord?.sessionId || sessionRecord?.session_id || path7.basename(filePath, ".jsonl");
15666
+ const cwd = records.find((record) => record.cwd)?.cwd;
15667
+ const timestamps = records.map((record) => this.timestamp(record.timestamp)).filter((value) => value !== void 0);
15668
+ const messages = this.recordsToMessages(records);
15669
+ const title = [...records].reverse().find((record) => typeof record.aiTitle === "string")?.aiTitle;
15670
+ const firstUserMessage = messages.find((message2) => message2.role === "user");
15671
+ return [{
15672
+ filePath,
15673
+ item: {
15674
+ id: sessionId,
15675
+ sourceId: "",
15676
+ name: title,
15677
+ path: filePath,
15678
+ cwd,
15679
+ created: new Date(timestamps.length ? Math.min(...timestamps) : stat10.birthtimeMs).toISOString(),
15680
+ modified: new Date(timestamps.length ? Math.max(...timestamps) : stat10.mtimeMs).toISOString(),
15681
+ messageCount: messages.length,
15682
+ firstMessage: firstUserMessage ? this.messageText(firstUserMessage.content) : void 0
15683
+ }
15684
+ }];
15685
+ } catch {
15686
+ return [];
15687
+ }
15688
+ });
15689
+ }
15690
+ sessionFiles() {
15691
+ if (!fs5.existsSync(this.dataPath)) return [];
15692
+ const files = [];
15693
+ const addJsonlFiles = (directory) => {
15694
+ for (const entry of fs5.readdirSync(directory, { withFileTypes: true })) {
15695
+ if (entry.isFile() && entry.name.endsWith(".jsonl")) files.push(path7.join(directory, entry.name));
15696
+ }
15697
+ };
15698
+ addJsonlFiles(this.dataPath);
15699
+ for (const entry of fs5.readdirSync(this.dataPath, { withFileTypes: true })) {
15700
+ if (entry.isDirectory()) addJsonlFiles(path7.join(this.dataPath, entry.name));
15701
+ }
15702
+ return files;
15703
+ }
15704
+ findSession(sessionId) {
15705
+ if (!sessionId || path7.basename(sessionId) !== sessionId) throw new Error("Invalid review session ID");
15706
+ return this.readSessions().find((session) => session.item.id === sessionId);
15707
+ }
15708
+ readTranscript(filePath) {
15709
+ const records = this.readRecords(filePath);
15710
+ return {
15711
+ messages: this.recordsToMessages(records),
15712
+ metadata: { sessionId: path7.basename(filePath, ".jsonl"), format: "claude-code-jsonl" }
15713
+ };
15714
+ }
15715
+ readRecords(filePath) {
15716
+ return fs5.readFileSync(filePath, "utf8").split(/\r?\n/).filter(Boolean).flatMap((line) => {
15717
+ try {
15718
+ return [JSON.parse(line)];
15719
+ } catch {
15720
+ return [];
14185
15721
  }
14186
15722
  });
14187
15723
  }
@@ -14266,7 +15802,7 @@ var CodexReviewSourceAdapter = class {
14266
15802
  if (!normalized) return this.list(options);
14267
15803
  const matches = this.readSessions().filter(({ item, filePath }) => {
14268
15804
  if (item.name?.toLowerCase().includes(normalized) || item.firstMessage?.toLowerCase().includes(normalized)) return true;
14269
- return this.readTranscript(filePath).messages.some((message) => this.messageText(message.content).toLowerCase().includes(normalized));
15805
+ return this.readTranscript(filePath).messages.some((message2) => this.messageText(message2.content).toLowerCase().includes(normalized));
14270
15806
  }).map((session) => session.item).sort((a, b) => b.modified.localeCompare(a.modified));
14271
15807
  return this.applyListOptions(matches, options);
14272
15808
  }
@@ -14287,11 +15823,11 @@ var CodexReviewSourceAdapter = class {
14287
15823
  try {
14288
15824
  const records = this.readRecords(filePath);
14289
15825
  if (!records.length) return [];
14290
- const stat8 = fs6.statSync(filePath);
15826
+ const stat10 = fs6.statSync(filePath);
14291
15827
  const metadata = records.find((record) => record.type === "session_meta")?.payload;
14292
15828
  const messages = this.recordsToMessages(records);
14293
15829
  const timestamps = records.map((record) => this.timestamp(record.timestamp)).filter((value) => value !== void 0);
14294
- const firstUserMessage = messages.find((message) => message.role === "user" && !message.detailOnly);
15830
+ const firstUserMessage = messages.find((message2) => message2.role === "user" && !message2.detailOnly);
14295
15831
  return [{
14296
15832
  filePath,
14297
15833
  item: {
@@ -14299,8 +15835,8 @@ var CodexReviewSourceAdapter = class {
14299
15835
  sourceId: "",
14300
15836
  path: filePath,
14301
15837
  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(),
15838
+ created: new Date(timestamps.length ? Math.min(...timestamps) : stat10.birthtimeMs).toISOString(),
15839
+ modified: new Date(timestamps.length ? Math.max(...timestamps) : stat10.mtimeMs).toISOString(),
14304
15840
  messageCount: messages.length,
14305
15841
  firstMessage: firstUserMessage ? this.messageText(firstUserMessage.content) : void 0
14306
15842
  }
@@ -14567,15 +16103,15 @@ var DevinReviewSourceAdapter = class {
14567
16103
  }
14568
16104
  extractMessages(raw) {
14569
16105
  if (Array.isArray(raw.messages) && raw.messages.length > 0) {
14570
- return raw.messages.map((message) => ({
14571
- role: String(message.role || "unknown"),
14572
- content: message.content,
14573
- timestamp: typeof message.timestamp === "number" ? message.timestamp : void 0,
14574
- detailOnly: this.isDetailOnlyMessage(message.content)
16106
+ return raw.messages.map((message2) => ({
16107
+ role: String(message2.role || "unknown"),
16108
+ content: message2.content,
16109
+ timestamp: typeof message2.timestamp === "number" ? message2.timestamp : void 0,
16110
+ detailOnly: this.isDetailOnlyMessage(message2.content)
14575
16111
  }));
14576
16112
  }
14577
16113
  const steps = Array.isArray(raw.steps) ? raw.steps : [];
14578
- return steps.map((step) => this.devinStepToMessage(step)).filter((message) => message.content !== "");
16114
+ return steps.map((step) => this.devinStepToMessage(step)).filter((message2) => message2.content !== "");
14579
16115
  }
14580
16116
  devinStepToMessage(step) {
14581
16117
  const source = String(step.source || "unknown");
@@ -14617,16 +16153,16 @@ ${observation}
14617
16153
  const text = typeof content === "string" ? content.trimStart() : "";
14618
16154
  return text.startsWith("You are Devin, an interactive command line") || text.startsWith("You are powered by ") || /^Available subagent profiles for the\s+`?run_subagent`?\s+tool\./.test(text) || text.startsWith("The following skills can be invoked using the `skill` tool.");
14619
16155
  }
14620
- messageToText(message) {
14621
- if (!message) return void 0;
14622
- if (typeof message.content === "string") return message.content;
14623
- if (Array.isArray(message.content)) {
14624
- return message.content.map((item) => typeof item === "string" ? item : item?.text).filter(Boolean).join("\n");
16156
+ messageToText(message2) {
16157
+ if (!message2) return void 0;
16158
+ if (typeof message2.content === "string") return message2.content;
16159
+ if (Array.isArray(message2.content)) {
16160
+ return message2.content.map((item) => typeof item === "string" ? item : item?.text).filter(Boolean).join("\n");
14625
16161
  }
14626
16162
  return void 0;
14627
16163
  }
14628
16164
  transcriptToText(transcript) {
14629
- return transcript.messages.map((message) => this.messageToText(message) || "").join("\n");
16165
+ return transcript.messages.map((message2) => this.messageToText(message2) || "").join("\n");
14630
16166
  }
14631
16167
  applyListOptions(items, options) {
14632
16168
  let result = items;
@@ -14836,6 +16372,336 @@ var ReviewSourceService = class {
14836
16372
  }
14837
16373
  };
14838
16374
 
16375
+ // src/services/model-window-kickoff-store.ts
16376
+ import { randomUUID as randomUUID8 } from "node:crypto";
16377
+ var DEFAULT_KICKOFF_PROMPT = "Ping";
16378
+ var ModelWindowKickoffStore = class {
16379
+ constructor(db) {
16380
+ this.db = db;
16381
+ }
16382
+ db;
16383
+ list() {
16384
+ return this.db.prepare("SELECT * FROM model_window_kickoffs ORDER BY created_at").all().map(toRecord);
16385
+ }
16386
+ get(id) {
16387
+ const row = this.db.prepare("SELECT * FROM model_window_kickoffs WHERE id = ?").get(id);
16388
+ return row && toRecord(row);
16389
+ }
16390
+ listDue(now) {
16391
+ return this.db.prepare(`
16392
+ SELECT * FROM model_window_kickoffs WHERE enabled = 1 AND next_run_at <= ? ORDER BY next_run_at
16393
+ `).all(now).map(toRecord);
16394
+ }
16395
+ create(input, now = (/* @__PURE__ */ new Date()).toISOString()) {
16396
+ const value = validateInput2(input, now);
16397
+ const id = randomUUID8();
16398
+ this.db.prepare(`
16399
+ INSERT INTO model_window_kickoffs (
16400
+ id, enabled, profile_id, provider, model_id, project_path, prompt, window_duration_minutes,
16401
+ safety_buffer_seconds, notification_channel_id, next_run_at, created_at, updated_at
16402
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
16403
+ `).run(
16404
+ id,
16405
+ value.enabled ? 1 : 0,
16406
+ value.profileId,
16407
+ value.provider,
16408
+ value.modelId,
16409
+ value.projectPath,
16410
+ value.prompt,
16411
+ value.windowDurationMinutes,
16412
+ value.safetyBufferSeconds,
16413
+ value.notificationChannelId,
16414
+ value.nextRunAt,
16415
+ now,
16416
+ now
16417
+ );
16418
+ return this.get(id);
16419
+ }
16420
+ update(id, input, now = (/* @__PURE__ */ new Date()).toISOString()) {
16421
+ if (!this.get(id)) throw new Error("Model window kickoff not found");
16422
+ const value = validateInput2(input, now);
16423
+ this.db.prepare(`
16424
+ UPDATE model_window_kickoffs SET enabled = ?, profile_id = ?, provider = ?, model_id = ?, project_path = ?, prompt = ?,
16425
+ window_duration_minutes = ?, safety_buffer_seconds = ?, notification_channel_id = ?, next_run_at = ?, updated_at = ?
16426
+ WHERE id = ?
16427
+ `).run(
16428
+ value.enabled ? 1 : 0,
16429
+ value.profileId,
16430
+ value.provider,
16431
+ value.modelId,
16432
+ value.projectPath,
16433
+ value.prompt,
16434
+ value.windowDurationMinutes,
16435
+ value.safetyBufferSeconds,
16436
+ value.notificationChannelId,
16437
+ value.nextRunAt,
16438
+ now,
16439
+ id
16440
+ );
16441
+ return this.get(id);
16442
+ }
16443
+ delete(id) {
16444
+ if (this.db.prepare("DELETE FROM model_window_kickoffs WHERE id = ?").run(id).changes !== 1) {
16445
+ throw new Error("Model window kickoff not found");
16446
+ }
16447
+ }
16448
+ recordAttempt(id, at) {
16449
+ this.db.prepare("UPDATE model_window_kickoffs SET last_attempt_at = ?, updated_at = ? WHERE id = ?").run(at, at, id);
16450
+ }
16451
+ recordSuccess(id, at, nextRunAt) {
16452
+ this.db.prepare(`UPDATE model_window_kickoffs SET last_success_at = ?, next_run_at = ?, last_error = NULL, updated_at = ? WHERE id = ?`).run(at, nextRunAt, at, id);
16453
+ }
16454
+ recordFailure(id, at, error, nextRunAt) {
16455
+ this.db.prepare(`UPDATE model_window_kickoffs SET last_error = ?, next_run_at = ?, updated_at = ? WHERE id = ?`).run(error.slice(0, 2e3), nextRunAt, at, id);
16456
+ }
16457
+ };
16458
+ function validateInput2(input, now) {
16459
+ const profileId = input.profileId?.trim();
16460
+ const provider = input.provider?.trim();
16461
+ const modelId = input.modelId?.trim();
16462
+ const projectPath = input.projectPath?.trim() || "~";
16463
+ const prompt = (input.prompt ?? DEFAULT_KICKOFF_PROMPT).trim();
16464
+ const windowDurationMinutes = input.windowDurationMinutes ?? 300;
16465
+ const safetyBufferSeconds = input.safetyBufferSeconds ?? 60;
16466
+ const nextRunAt = input.nextRunAt ?? now;
16467
+ if (!profileId || !provider || !modelId || !prompt) {
16468
+ throw new Error("Profile, provider, model, and prompt are required");
16469
+ }
16470
+ if (!Number.isInteger(windowDurationMinutes) || windowDurationMinutes < 1 || windowDurationMinutes > 43200) {
16471
+ throw new Error("Window duration must be between 1 and 43200 minutes");
16472
+ }
16473
+ if (!Number.isInteger(safetyBufferSeconds) || safetyBufferSeconds < 0 || safetyBufferSeconds > 86400) {
16474
+ throw new Error("Safety buffer must be between 0 and 86400 seconds");
16475
+ }
16476
+ if (!Number.isFinite(Date.parse(nextRunAt))) {
16477
+ throw new Error("Next run time is invalid");
16478
+ }
16479
+ return {
16480
+ enabled: input.enabled ?? true,
16481
+ profileId,
16482
+ provider,
16483
+ modelId,
16484
+ projectPath,
16485
+ prompt,
16486
+ windowDurationMinutes,
16487
+ safetyBufferSeconds,
16488
+ notificationChannelId: input.notificationChannelId || null,
16489
+ nextRunAt: new Date(nextRunAt).toISOString()
16490
+ };
16491
+ }
16492
+ function toRecord(row) {
16493
+ return {
16494
+ id: row.id,
16495
+ enabled: Boolean(row.enabled),
16496
+ profileId: row.profile_id,
16497
+ provider: row.provider,
16498
+ modelId: row.model_id,
16499
+ projectPath: row.project_path,
16500
+ prompt: row.prompt,
16501
+ windowDurationMinutes: row.window_duration_minutes,
16502
+ safetyBufferSeconds: row.safety_buffer_seconds,
16503
+ notificationChannelId: row.notification_channel_id,
16504
+ nextRunAt: row.next_run_at,
16505
+ lastAttemptAt: row.last_attempt_at,
16506
+ lastSuccessAt: row.last_success_at,
16507
+ lastError: row.last_error,
16508
+ createdAt: row.created_at,
16509
+ updatedAt: row.updated_at
16510
+ };
16511
+ }
16512
+
16513
+ // src/services/model-window-kickoff-scheduler.ts
16514
+ var POLL_INTERVAL_MS = 1 * 6e4;
16515
+ var FAILURE_RETRY_MS = 60 * 6e4;
16516
+ var ModelWindowKickoffScheduler = class {
16517
+ constructor(dependencies) {
16518
+ this.dependencies = dependencies;
16519
+ this.now = dependencies.now ?? (() => /* @__PURE__ */ new Date());
16520
+ }
16521
+ dependencies;
16522
+ timer;
16523
+ running = /* @__PURE__ */ new Set();
16524
+ now;
16525
+ start() {
16526
+ if (this.timer) return;
16527
+ void this.tick();
16528
+ this.timer = setInterval(() => void this.tick(), this.dependencies.pollIntervalMs ?? POLL_INTERVAL_MS);
16529
+ this.timer.unref();
16530
+ }
16531
+ async stop() {
16532
+ if (this.timer) clearInterval(this.timer);
16533
+ this.timer = void 0;
16534
+ while (this.running.size) await new Promise((resolve10) => setTimeout(resolve10, 10));
16535
+ }
16536
+ async tick() {
16537
+ const due = this.dependencies.store.listDue(this.now().toISOString());
16538
+ await Promise.allSettled(due.map((kickoff) => this.run(kickoff)));
16539
+ }
16540
+ async run(kickoff) {
16541
+ if (this.running.has(kickoff.id)) return;
16542
+ this.running.add(kickoff.id);
16543
+ const attemptedAt = this.now();
16544
+ this.dependencies.store.recordAttempt(kickoff.id, attemptedAt.toISOString());
16545
+ try {
16546
+ const profile = await this.dependencies.resolveProfile(kickoff.profileId);
16547
+ if (!profile) throw new Error("Agent profile not found");
16548
+ const response = this.dependencies.probe ? await this.dependencies.probe(kickoff, profile) : await probeModel(kickoff, this.dependencies.sessions);
16549
+ const successfulAt = this.now();
16550
+ const next = new Date(successfulAt.getTime() + kickoff.windowDurationMinutes * 6e4 + kickoff.safetyBufferSeconds * 1e3);
16551
+ this.dependencies.store.recordSuccess(kickoff.id, successfulAt.toISOString(), next.toISOString());
16552
+ if (kickoff.notificationChannelId) {
16553
+ try {
16554
+ await this.dependencies.notifications.send(
16555
+ kickoff.notificationChannelId,
16556
+ formatSuccessNotification(kickoff, profile, response, attemptedAt, next)
16557
+ );
16558
+ } catch (error) {
16559
+ this.dependencies.log?.error(error, "Model window kickoff notification failed");
16560
+ }
16561
+ }
16562
+ } catch (error) {
16563
+ const failedAt = this.now();
16564
+ const failure = error instanceof Error ? error.message : String(error);
16565
+ const retryAt = new Date(failedAt.getTime() + FAILURE_RETRY_MS);
16566
+ this.dependencies.store.recordFailure(kickoff.id, failedAt.toISOString(), failure, retryAt.toISOString());
16567
+ if (kickoff.notificationChannelId) {
16568
+ try {
16569
+ await this.dependencies.notifications.send(
16570
+ kickoff.notificationChannelId,
16571
+ formatFailureNotification(kickoff, failure, attemptedAt, retryAt)
16572
+ );
16573
+ } catch (notificationError) {
16574
+ this.dependencies.log?.error(notificationError, "Model window kickoff failure notification failed");
16575
+ }
16576
+ }
16577
+ } finally {
16578
+ this.running.delete(kickoff.id);
16579
+ }
16580
+ }
16581
+ };
16582
+ function formatSuccessNotification(kickoff, profile, response, startedAt, next) {
16583
+ return [
16584
+ "\u2705Model window started",
16585
+ markdownField("Profile", profile.label || profile.id),
16586
+ markdownField("Model", `${kickoff.provider}/${kickoff.modelId}`),
16587
+ ...response ? [markdownField("Response", response)] : [],
16588
+ markdownField("Started time", formatLocalDateTime(startedAt), "warning"),
16589
+ markdownField("Next kickoff", formatLocalDateTime(next))
16590
+ ].join("\n");
16591
+ }
16592
+ function formatFailureNotification(kickoff, failure, startedAt, retryAt) {
16593
+ return [
16594
+ "\u274CModel window kickoff failed",
16595
+ markdownField("Profile", kickoff.profileId),
16596
+ markdownField("Model", `${kickoff.provider}/${kickoff.modelId}`),
16597
+ markdownField("Error", failure, "warning"),
16598
+ markdownField("Started time", formatLocalDateTime(startedAt), "warning"),
16599
+ markdownField("Retry", formatLocalDateTime(retryAt))
16600
+ ].join("\n");
16601
+ }
16602
+ function markdownField(label, value, color = "comment") {
16603
+ return `>${label}: <font color="${color}">${value}</font>`;
16604
+ }
16605
+ function formatLocalDateTime(value) {
16606
+ const pad = (part) => String(part).padStart(2, "0");
16607
+ return `${value.getFullYear()}-${pad(value.getMonth() + 1)}-${pad(value.getDate())} ${pad(value.getHours())}:${pad(value.getMinutes())}:${pad(value.getSeconds())}`;
16608
+ }
16609
+ async function probeModel(kickoff, sessions) {
16610
+ const clientId = `model-window-kickoff:${kickoff.id}`;
16611
+ const sessionName = `Model window kickoff: ${kickoff.provider}/${kickoff.modelId} (${kickoff.id.slice(0, 8)})`;
16612
+ await sessions.setClientAgentProfile(clientId, kickoff.profileId);
16613
+ const persisted = (await sessions.listSessions(clientId, kickoff.projectPath)).find((item) => item.name === sessionName);
16614
+ let activeSession;
16615
+ if (persisted) {
16616
+ activeSession = await sessions.resumeSession(clientId, persisted.path);
16617
+ } else {
16618
+ activeSession = (await sessions.createSession(clientId, {
16619
+ cwd: kickoff.projectPath,
16620
+ agentProfileId: kickoff.profileId,
16621
+ modelProvider: kickoff.provider,
16622
+ modelId: kickoff.modelId,
16623
+ memoryEnabled: false
16624
+ })).session;
16625
+ await sessions.renameSession(clientId, activeSession.sessionId, sessionName);
16626
+ }
16627
+ await sessions.setSessionModel(clientId, activeSession.sessionId, kickoff.provider, kickoff.modelId);
16628
+ await sessions.runForegroundWithClientProfileProxy(clientId, () => activeSession.prompt(kickoff.prompt));
16629
+ const response = [...activeSession.messages].reverse().find((message2) => message2.role === "assistant" && "stopReason" in message2);
16630
+ if (!response || !("stopReason" in response)) throw new Error("Model kickoff finished without an assistant response");
16631
+ if (response.stopReason === "error" || response.stopReason === "aborted") {
16632
+ throw new Error(response.errorMessage || `Model kickoff ${response.stopReason}`);
16633
+ }
16634
+ return extractAssistantText(response.content);
16635
+ }
16636
+ function extractAssistantText(content) {
16637
+ if (typeof content === "string") return content.trim();
16638
+ if (!Array.isArray(content)) return "";
16639
+ return content.filter((part) => typeof part === "object" && part !== null && "type" in part && part.type === "text" && "text" in part && typeof part.text === "string").map((part) => part.text).join("").trim();
16640
+ }
16641
+
16642
+ // src/services/notification-channel.ts
16643
+ import { randomUUID as randomUUID9 } from "node:crypto";
16644
+ var NotificationChannelService = class {
16645
+ constructor(db) {
16646
+ this.db = db;
16647
+ }
16648
+ db;
16649
+ list() {
16650
+ return this.db.prepare("SELECT id, type, name, config_json FROM notification_channels ORDER BY created_at").all().map(publicChannel);
16651
+ }
16652
+ saveWecom(input) {
16653
+ const existing = input.id ? this.getRow(input.id) : void 0;
16654
+ const botKey = input.botKey?.trim() || readBotKey(existing);
16655
+ if (!botKey) throw new Error("WeCom bot key is required");
16656
+ const id = existing?.id ?? randomUUID9();
16657
+ const now = (/* @__PURE__ */ new Date()).toISOString();
16658
+ const name = input.name?.trim() || existing?.name || "WeCom";
16659
+ this.db.prepare(`
16660
+ INSERT INTO notification_channels (id, type, name, config_json, created_at, updated_at)
16661
+ VALUES (?, 'wecom', ?, ?, ?, ?)
16662
+ ON CONFLICT(id) DO UPDATE SET name = excluded.name, config_json = excluded.config_json, updated_at = excluded.updated_at
16663
+ `).run(id, name, JSON.stringify({ botKey }), now, now);
16664
+ return publicChannel(this.getRow(id));
16665
+ }
16666
+ delete(id) {
16667
+ if (this.db.prepare("DELETE FROM notification_channels WHERE id = ?").run(id).changes !== 1) {
16668
+ throw new Error("Notification channel not found");
16669
+ }
16670
+ }
16671
+ async send(id, message2) {
16672
+ const row = this.getRow(id);
16673
+ if (!row) throw new Error("Notification channel not found");
16674
+ if (row.type !== "wecom") throw new Error(`Unsupported notification channel: ${row.type}`);
16675
+ const botKey = readBotKey(row);
16676
+ if (!botKey) throw new Error("WeCom bot key is not configured");
16677
+ const response = await fetch(`https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=${encodeURIComponent(botKey)}`, {
16678
+ method: "POST",
16679
+ headers: { "content-type": "application/json" },
16680
+ body: JSON.stringify({ msgtype: "markdown", markdown: { content: message2 } }),
16681
+ signal: AbortSignal.timeout(1e4)
16682
+ });
16683
+ if (!response.ok) throw new Error(`WeCom returned HTTP ${response.status}`);
16684
+ const result = await response.json();
16685
+ if (result.errcode !== 0) throw new Error(`WeCom notification failed: ${result.errmsg || result.errcode}`);
16686
+ }
16687
+ getRow(id) {
16688
+ return this.db.prepare("SELECT id, type, name, config_json FROM notification_channels WHERE id = ?").get(id);
16689
+ }
16690
+ };
16691
+ function readBotKey(row) {
16692
+ if (!row) {
16693
+ return "";
16694
+ }
16695
+ try {
16696
+ return JSON.parse(row.config_json).botKey || "";
16697
+ } catch {
16698
+ return "";
16699
+ }
16700
+ }
16701
+ function publicChannel(row) {
16702
+ return { id: row.id, type: "wecom", name: row.name, configured: Boolean(readBotKey(row)) };
16703
+ }
16704
+
14839
16705
  // src/index.ts
14840
16706
  var __filename = fileURLToPath2(import.meta.url);
14841
16707
  var __dirname = path12.dirname(__filename);
@@ -14866,7 +16732,7 @@ function createDefaultConfig() {
14866
16732
  if (process.env.NODE_ENV === "test") return;
14867
16733
  if (process.env.PI_CLOUD_AUTH_USERNAME || process.env.PI_CLOUD_AUTH_PASSWORD || process.env.PI_CLOUD_AUTH_PASSWORD_HASH) return;
14868
16734
  const username = "admin";
14869
- const password = randomBytes3(6).toString("base64url");
16735
+ const password = randomBytes4(6).toString("base64url");
14870
16736
  const samplePath = path12.resolve(__dirname, "../../.env.example");
14871
16737
  const content = renderDefaultConfig(readFileSync4(samplePath, "utf8"), username, password);
14872
16738
  mkdirSync2(userConfigDir, { recursive: true, mode: 448 });
@@ -14887,6 +16753,7 @@ var CONTENT_SECURITY_POLICY = [
14887
16753
  "object-src 'none'",
14888
16754
  "frame-ancestors 'none'",
14889
16755
  "img-src 'self' data: blob:",
16756
+ "media-src 'self' blob:",
14890
16757
  "font-src 'self' data:",
14891
16758
  "style-src 'self' 'unsafe-inline'",
14892
16759
  "script-src 'self'",
@@ -14909,6 +16776,7 @@ function isPublicRoute(req) {
14909
16776
  if (url === "/api/health") return true;
14910
16777
  if (url.startsWith("/api/auth")) return true;
14911
16778
  if (url === "/api/gateways/feishu/events") return true;
16779
+ if (url === "/api/gateways/wecom/callback") return true;
14912
16780
  if (req.method === "GET" && !url.startsWith("/api/")) return true;
14913
16781
  return false;
14914
16782
  }
@@ -14922,8 +16790,8 @@ function isSpaRoute(req) {
14922
16790
  if (path12.extname(url)) return false;
14923
16791
  return true;
14924
16792
  }
14925
- function getSecuritySetting(db, key) {
14926
- const row = db.prepare("SELECT value FROM security_settings WHERE key = ?").get(key);
16793
+ function getApplicationSetting(db, key) {
16794
+ const row = db.prepare("SELECT value FROM application_settings WHERE key = ?").get(key);
14927
16795
  return row?.value || "";
14928
16796
  }
14929
16797
  function formatServerTimestamp() {
@@ -14981,6 +16849,16 @@ async function buildApp() {
14981
16849
  const skillPresetStore = new SkillPresetStore(db);
14982
16850
  const reviewSourceStore = new ReviewSourceStore(db);
14983
16851
  const reviewSourceService = new ReviewSourceService(reviewSourceStore);
16852
+ const modelWindowKickoffStore = new ModelWindowKickoffStore(db);
16853
+ const notificationChannels = new NotificationChannelService(db);
16854
+ const modelWindowKickoffScheduler = new ModelWindowKickoffScheduler({
16855
+ store: modelWindowKickoffStore,
16856
+ notifications: notificationChannels,
16857
+ sessions: piSessionService,
16858
+ resolveProfile: async (profileId) => (await piSessionService.listAgentProfiles()).find((profile) => profile.id === profileId),
16859
+ log: app.log
16860
+ });
16861
+ modelWindowKickoffScheduler.start();
14984
16862
  const sessionActivityStore = new SessionActivityStore(db);
14985
16863
  const sessionPinStore = new SessionPinStore(db);
14986
16864
  projectTaskStore.restoreAllStarting();
@@ -14989,6 +16867,7 @@ async function buildApp() {
14989
16867
  const githubSettings = new GithubSettingsStore(db);
14990
16868
  const feishuGateway = new FeishuGatewayService(db, gatewaySettings, piSessionService);
14991
16869
  const weixinGateway = new WeixinGatewayService(db, gatewaySettings, piSessionService);
16870
+ const wecomGateway = new WecomGatewayService(db, gatewaySettings, piSessionService);
14992
16871
  weixinGateway.start();
14993
16872
  const repositoryCloner = new RepositoryCloner({
14994
16873
  spawnGit: (args, options) => spawn3("git", args, options),
@@ -14998,7 +16877,7 @@ async function buildApp() {
14998
16877
  ...await piSessionService.getClientAgentProxyEnvForRoutes(clientId || "default"),
14999
16878
  ...githubSettings.proxyEnv()
15000
16879
  }),
15001
- gitCloneParentPath: () => getSecuritySetting(db, "ui.gitCloneParentPath") || path12.join(os9.homedir(), "git", "github")
16880
+ gitCloneParentPath: () => getApplicationSetting(db, "ui.gitCloneParentPath") || path12.join(os9.homedir(), "git", "github")
15002
16881
  });
15003
16882
  const gitHosting = new GitHostingService();
15004
16883
  const worktreeManager = new WorktreeManager();
@@ -15023,6 +16902,7 @@ async function buildApp() {
15023
16902
  worktreeMetadata: worktreeMetadataStore
15024
16903
  });
15025
16904
  app.addHook("onClose", async () => {
16905
+ await modelWindowKickoffScheduler.stop();
15026
16906
  await weixinGateway.stop();
15027
16907
  terminalManager.disposeAll();
15028
16908
  piSessionService.disposeAll();
@@ -15040,7 +16920,7 @@ async function buildApp() {
15040
16920
  reply.header("X-Content-Type-Options", "nosniff");
15041
16921
  reply.header("X-Frame-Options", "DENY");
15042
16922
  reply.header("Referrer-Policy", "no-referrer");
15043
- reply.header("Permissions-Policy", "camera=(), microphone=(), geolocation=()");
16923
+ reply.header("Permissions-Policy", "camera=(), microphone=(self), geolocation=()");
15044
16924
  if (authConfig.cookieSecure) {
15045
16925
  reply.header("Strict-Transport-Security", "max-age=31536000");
15046
16926
  }
@@ -15089,6 +16969,7 @@ async function buildApp() {
15089
16969
  await app.register(taskRoutes, { prefix: "/api/tasks", store: projectTaskStore, starter: projectTaskStarter, activityStore: sessionActivityStore });
15090
16970
  await app.register(gatewayRoutes, { prefix: "/api/gateways", settings: gatewaySettings, weixin: weixinGateway });
15091
16971
  await app.register(feishuGatewayRoutes, { prefix: "/api/gateways/feishu", service: feishuGateway });
16972
+ await app.register(wecomGatewayRoutes, { prefix: "/api/gateways/wecom", service: wecomGateway });
15092
16973
  const gitHostingRouteOptions = {
15093
16974
  settings: giteaSettings,
15094
16975
  githubSettings,
@@ -15108,7 +16989,14 @@ async function buildApp() {
15108
16989
  });
15109
16990
  await app.register(slashCommandRoutes, { prefix: "/api/slash-commands" });
15110
16991
  await app.register(changelogRoutes, { prefix: "/api/changelog" });
16992
+ await app.register(speechRoutes, { prefix: "/api/speech" });
15111
16993
  await app.register(reviewSourceRoutes, { prefix: "/api/review-sources", reviewSourceService, pinStore: sessionPinStore });
16994
+ await app.register(modelWindowKickoffRoutes, {
16995
+ prefix: "/api/model-window-kickoffs",
16996
+ store: modelWindowKickoffStore,
16997
+ notifications: notificationChannels,
16998
+ sessions: piSessionService
16999
+ });
15112
17000
  await app.register(chatWebSocket);
15113
17001
  await app.register(terminalWebSocket);
15114
17002
  app.get("/api/health", async () => {