@runeya/runeya 2.0.69 → 2.0.71

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 (278) hide show
  1. package/agent/index.js +63 -10
  2. package/agent/index.js.map +1 -1
  3. package/agent-manager-UUSQK6S2.js +7 -0
  4. package/{chunk-YSJ6OJGN.js → chunk-Z3KAXI7V.js} +197 -12
  5. package/chunk-Z3KAXI7V.js.map +1 -0
  6. package/index.js +1 -1
  7. package/package.json +1 -1
  8. package/{src-MQWOIUB5.js → src-Q3PSCMS7.js} +1713 -1751
  9. package/src-Q3PSCMS7.js.map +1 -0
  10. package/web/assets/{AgentScalarChatInterface.vue-vU3z8Etw.js → AgentScalarChatInterface.vue-Cin_Rbay.js} +1 -1
  11. package/web/assets/AgentTerminalPage-BD2w4XRc.css +1 -0
  12. package/web/assets/AgentTerminalPage-YgCQj_Mj.js +1 -0
  13. package/web/assets/AiChatPage-cIE_ZdF6.js +1 -0
  14. package/web/assets/{AppLayout-CvkK_Pp3.js → AppLayout-dYfSESzv.js} +1 -1
  15. package/web/assets/{AuditPage-BZuVgUf1.js → AuditPage-Bw3-G8ZM.js} +1 -1
  16. package/web/assets/CloudAccountPage-CA4dMsIm.js +1 -0
  17. package/web/assets/{CloudAccountPage-B__2SGAw.js → CloudAccountPage-DjljrEI_.js} +1 -1
  18. package/web/assets/{CloudMessagingPage-C5pD6Qsd.js → CloudMessagingPage-CbfH2vPo.js} +1 -1
  19. package/web/assets/{DashboardPage-66q5K841.js → DashboardPage-Cn3bUyhR.js} +1 -1
  20. package/web/assets/{DirectoryPickerDialog-wZYtGEZ9.js → DirectoryPickerDialog-BmsAi5gR.js} +1 -1
  21. package/web/assets/{EditorIcon-DUDLTmUd.js → EditorIcon-JRryeySJ.js} +1 -1
  22. package/web/assets/{EnvironmentForm-C0Q1pRdW.js → EnvironmentForm-BfF9IFRB.js} +1 -1
  23. package/web/assets/{EnvironmentFormPage-LO-mwupn.js → EnvironmentFormPage-CebWv3I_.js} +1 -1
  24. package/web/assets/{EnvironmentListPage-CvknyN3O.js → EnvironmentListPage-DDJGOHvR.js} +1 -1
  25. package/web/assets/{KanbanBoardPage-DUsAyowy.js → KanbanBoardPage-BMXyvHOl.js} +3 -3
  26. package/web/assets/{KanbanListPage-9RBCauSd.js → KanbanListPage-YMm_7vzg.js} +1 -1
  27. package/web/assets/{LaunchProjectPage-obaNCuEY.js → LaunchProjectPage-Da21oFjw.js} +1 -1
  28. package/web/assets/{LaunchServicesPage-CiWzQIyC.js → LaunchServicesPage-8YJT1stQ.js} +1 -1
  29. package/web/assets/{LogViewer-BiSD_TNt.js → LogViewer-BYNRWqlP.js} +1 -1
  30. package/web/assets/{OrgKeyPrompt-zhMl_nTd.js → OrgKeyPrompt-De8D0zzy.js} +1 -1
  31. package/web/assets/{ProcessControls-DMxvJmDe.js → ProcessControls-BPQXGKib.js} +1 -1
  32. package/web/assets/{ProcessDetailPage-CsCqQh2V.js → ProcessDetailPage-CVwMbU6x.js} +1 -1
  33. package/web/assets/{ProcessMetrics-DK4Dgdb0.js → ProcessMetrics-D7L708Em.js} +1 -1
  34. package/web/assets/{ProjectDetailPage-B0vJySvH.js → ProjectDetailPage-BjBN3hnN.js} +1 -1
  35. package/web/assets/{ProjectListPage-srxBmrbO.js → ProjectListPage-BRXKT6op.js} +1 -1
  36. package/web/assets/{RandomJobTestPage-jGfX-6W9.js → RandomJobTestPage-dhDNam5v.js} +1 -1
  37. package/web/assets/{ScenarioEditorPage-CxLy6gs7.js → ScenarioEditorPage-yOp4ZF14.js} +1 -1
  38. package/web/assets/{SendToChatDialog-AHkuFQjt.js → SendToChatDialog-o4y1Q-OD.js} +1 -1
  39. package/web/assets/{ServiceControlsBar-C8P9_qWK.js → ServiceControlsBar-BH1ZEFkK.js} +1 -1
  40. package/web/assets/{ServiceDetailPage-DTBpQegV.js → ServiceDetailPage-CFb0blqq.js} +1 -1
  41. package/web/assets/{ServiceFormPage-CsPSMWdD.js → ServiceFormPage-DzFQ_M1-.js} +1 -1
  42. package/web/assets/SettingsPage-Bcs3tnUy.js +2063 -0
  43. package/web/assets/{SettingsPage-uCS5lY2z.css → SettingsPage-CScovMi5.css} +1 -1
  44. package/web/assets/{UnlockPage-Bkp_hR-F.js → UnlockPage-BAxKs_K9.js} +1 -1
  45. package/web/assets/{VariablesTable-jo61MhbL.js → VariablesTable-CSuAPeF-.js} +1 -1
  46. package/web/assets/{VaultMigrationPage-D-Dxg5xH.js → VaultMigrationPage-BJX9kunL.js} +1 -1
  47. package/web/assets/{VersionBadge-CwNZLgqN.js → VersionBadge-B9Wvyvf6.js} +1 -1
  48. package/web/assets/WikiPage-CBZFOPL_.js +1 -0
  49. package/web/assets/{WikiView-BpJECJ2Y.js → WikiView-DxPrGyFy.js} +1 -1
  50. package/web/assets/{add-scalar-classes-CqLAxAGv.js → add-scalar-classes-vtTXFTZ6.js} +3 -3
  51. package/web/assets/auth-errors-DpUvxKid.js +1 -0
  52. package/web/assets/{autocomplete-CrMQ2sCk.js → autocomplete-BakcEoGP.js} +1 -1
  53. package/web/assets/{basic-shape-diamond.svg-CacrbinE.js → basic-shape-diamond.svg-C8K6YjGj.js} +1 -1
  54. package/web/assets/{basic-shape-hexagon.svg-GGJ54oyX.js → basic-shape-hexagon.svg-C9PeXR6S.js} +1 -1
  55. package/web/assets/{basic-shape-primary-circle-ellipse-round.svg-CuWfwdJM.js → basic-shape-primary-circle-ellipse-round.svg-CfBLN6Yj.js} +1 -1
  56. package/web/assets/{basic-shape-primary-square-rectangle.svg-C2Bewjip.js → basic-shape-primary-square-rectangle.svg-D2QPBP5_.js} +1 -1
  57. package/web/assets/{basic-shape-shield.svg-DNtg0KuJ.js → basic-shape-shield.svg-ClsEFdja.js} +1 -1
  58. package/web/assets/{browser-XO2fm4BX.js → browser-CF5_9QqR.js} +1 -1
  59. package/web/assets/{browser-5NN8ieli.js → browser-D7ZckEFz.js} +1 -1
  60. package/web/assets/browser-DNbnH79q.js +6 -0
  61. package/web/assets/{card-CjcU4SfX.js → card-DcpoBJHQ.js} +1 -1
  62. package/web/assets/{checkbox-iq9mU5eP.js → checkbox-CfwNXlF_.js} +1 -1
  63. package/web/assets/{chip-BsccUnHb.js → chip-3tOoBrD7.js} +1 -1
  64. package/web/assets/{client-B9UJRQa-.js → client-CKBKBNVm.js} +1 -1
  65. package/web/assets/{cloud-chat.store-Dw2lzuBY.js → cloud-chat.store-Ci1DBP5j.js} +1 -1
  66. package/web/assets/{cloud-session-DIhbWyP5.js → cloud-session-CzYQ65k8.js} +1 -1
  67. package/web/assets/{column-BGKYk6b6.js → column-CVuKYYbZ.js} +1 -1
  68. package/web/assets/{computer-device-desktop-monitor.svg-DSwpF_ss.js → computer-device-desktop-monitor.svg-9f4DSNLj.js} +1 -1
  69. package/web/assets/{computer-device-desktop.svg-Caj_PZBV.js → computer-device-desktop.svg-BYoXiEOE.js} +1 -1
  70. package/web/assets/{computer-device-laptop.svg-Bv3ienI_.js → computer-device-laptop.svg-DR1sEMdL.js} +1 -1
  71. package/web/assets/{computer-device-mobile-phone-android-samsung-back.svg-IWlhEKx_.js → computer-device-mobile-phone-android-samsung-back.svg-B-AOaBuq.js} +1 -1
  72. package/web/assets/{computer-device-mobile-phone-android-samsung.svg-CglQAFq2.js → computer-device-mobile-phone-android-samsung.svg-Dw4z_H5H.js} +1 -1
  73. package/web/assets/{computer-device-mobile-phone-iphone-x-back.svg-CgjNnTTw.js → computer-device-mobile-phone-iphone-x-back.svg-BtqMb2Wo.js} +1 -1
  74. package/web/assets/{computer-device-mobile-phone-iphone-x.svg-CotZ-4M5.js → computer-device-mobile-phone-iphone-x.svg-eYEM_VXA.js} +1 -1
  75. package/web/assets/{computer-device-mobile-tablet-touch.svg-B2cemd61.js → computer-device-mobile-tablet-touch.svg-jLD41nvm.js} +1 -1
  76. package/web/assets/{computer-device-mobile-tablet.svg-8nPVs4u_.js → computer-device-mobile-tablet.svg-D_7_M1GK.js} +1 -1
  77. package/web/assets/{computer-device-network-ethernet-cat6.svg-Bl56Qw4v.js → computer-device-network-ethernet-cat6.svg-B_527QGY.js} +1 -1
  78. package/web/assets/{computer-device-network-lan-www.svg-C_x6Rf8N.js → computer-device-network-lan-www.svg-ClZIaUOH.js} +1 -1
  79. package/web/assets/{computer-device-network-wifi-connection.svg-oz6dsDQB.js → computer-device-network-wifi-connection.svg-DC4OCJY0.js} +1 -1
  80. package/web/assets/{computer-device-network-wifi-router.svg-BciHU6Nj.js → computer-device-network-wifi-router.svg-BWPpobCc.js} +1 -1
  81. package/web/assets/{cssMode-DSX5tYcm.js → cssMode-CXUrkdrQ.js} +1 -1
  82. package/web/assets/{divider-DnPOdDs3.js → divider-BrO5nkpd.js} +1 -1
  83. package/web/assets/docx-preview-tE714WSD.js +21 -0
  84. package/web/assets/{ecology-science-erlenmeyer-flask.svg-BzQP6y9L.js → ecology-science-erlenmeyer-flask.svg-o8D0ASmr.js} +1 -1
  85. package/web/assets/{fieldset-D9lkV70l.js → fieldset-LX5cdTvN.js} +1 -1
  86. package/web/assets/{freemarker2-D9yXX9Vw.js → freemarker2-DJ82s0fP.js} +1 -1
  87. package/web/assets/{handlebars-BmcjEksL.js → handlebars-BXFlY2Lu.js} +1 -1
  88. package/web/assets/{html-BQF9vZLL.js → html-CqDVypOC.js} +1 -1
  89. package/web/assets/{htmlMode-DxSRpgHE.js → htmlMode-DrHxdTQP.js} +1 -1
  90. package/web/assets/{image-flash-lightning.svg-DQX09uxo.js → image-flash-lightning.svg-BDPMOynq.js} +1 -1
  91. package/web/assets/{image-picture-flower.svg-CUBdrnoc.js → image-picture-flower.svg-D-AJaery.js} +1 -1
  92. package/web/assets/{index-enuBsJBa.js → index-a6DAapTb.js} +299 -299
  93. package/web/assets/{interface-alert-exclamation-diamond.svg-DXQdQSvN.js → interface-alert-exclamation-diamond.svg-CV7M0IVQ.js} +1 -1
  94. package/web/assets/{interface-alert-exclamation-triangle-warning.svg-BoCeNIlU.js → interface-alert-exclamation-triangle-warning.svg-B8NapiB7.js} +1 -1
  95. package/web/assets/{interface-alert-information-circle.svg-BCA57JL2.js → interface-alert-information-circle.svg-eQIFULjG.js} +1 -1
  96. package/web/assets/{interface-award-crown.svg-B1hTlsb0.js → interface-award-crown.svg-D-4fclUD.js} +1 -1
  97. package/web/assets/{interface-bookmark.svg-zf3cIGKM.js → interface-bookmark-tag.svg-Jy6EOdau.js} +1 -1
  98. package/web/assets/{interface-bookmark-tag.svg-AosfQ0Ip.js → interface-bookmark.svg-XdbYDqqk.js} +1 -1
  99. package/web/assets/{interface-calendar-date-one.svg-D58WqPie.js → interface-calendar-date-one.svg-BIoXVZrz.js} +1 -1
  100. package/web/assets/{interface-content-book-open-pages.svg-B74HWMlw.js → interface-content-book-open-pages.svg-DHwJbBvQ.js} +1 -1
  101. package/web/assets/{interface-content-book-page.svg-C2QWk62-.js → interface-content-book-page.svg-47msUBN_.js} +1 -1
  102. package/web/assets/{interface-content-file.svg-xKu0qAOQ.js → interface-content-file.svg-DXw9W8QM.js} +1 -1
  103. package/web/assets/interface-content-folder.svg-BikaY449.js +1 -0
  104. package/web/assets/{interface-content-folder.svg-ZkrY9WkQ.js → interface-content-folder.svg-BmK_IJv8.js} +1 -1
  105. package/web/assets/{interface-copy-clipboard.svg-BEkNG3vO.js → interface-copy-clipboard.svg-CQ1rQN0L.js} +1 -1
  106. package/web/assets/{interface-edit-attachment.svg-5ztQ3WTq.js → interface-edit-attachment.svg-C3aTaTh_.js} +1 -1
  107. package/web/assets/{interface-edit-binocular.svg-CC9U1Gnc.js → interface-edit-binocular.svg-B2Ue_tgs.js} +1 -1
  108. package/web/assets/{interface-edit-magic-wand.svg-CPl6ZoE9.js → interface-edit-magic-wand.svg-DDKse-Wy.js} +1 -1
  109. package/web/assets/{interface-edit-tool-paint-roller.svg-B85t7jG2.js → interface-edit-tool-paint-roller.svg-C2ABby1f.js} +1 -1
  110. package/web/assets/{interface-edit-tool-pencil.svg-BSTdqWUg.js → interface-edit-tool-pencil.svg-CNjYbknQ.js} +1 -1
  111. package/web/assets/{interface-favorite-award.svg-DbPr_WKt.js → interface-favorite-award.svg-DWqSHfIa.js} +1 -1
  112. package/web/assets/{interface-favorite-flag.svg-CgKYXc7k.js → interface-favorite-flag.svg-CNUsr98g.js} +1 -1
  113. package/web/assets/{interface-favorite-heart.svg-DfnXyJMg.js → interface-favorite-heart.svg-3Eb366FO.js} +1 -1
  114. package/web/assets/{interface-favorite-star.svg-0sT7jJMK.js → interface-favorite-star.svg-CVJ3aq1n.js} +1 -1
  115. package/web/assets/{interface-favorite-stars-sparkles.svg-BcM2EXgn.js → interface-favorite-stars-sparkles.svg-BfXhPBbs.js} +1 -1
  116. package/web/assets/{interface-hierarchy-flowchart.svg-BM62WcKt.js → interface-hierarchy-flowchart.svg-DzsqKrYq.js} +1 -1
  117. package/web/assets/{interface-home-house.svg-KqP7iMC4.js → interface-home-house.svg-BnXbuyZn.js} +1 -1
  118. package/web/assets/{interface-hyperlink.svg-DBKFbihR.js → interface-hyperlink.svg-BsmSmyP5.js} +1 -1
  119. package/web/assets/{interface-lighting-brightness.svg-DTkCS7PY.js → interface-lighting-brightness.svg-BywVQRo9.js} +1 -1
  120. package/web/assets/{interface-lock-closed.svg-Div5xlvI.js → interface-lock-closed.svg-BLPORc_h.js} +1 -1
  121. package/web/assets/{interface-lock-open-unlock.svg-Du5L_iMV.js → interface-lock-open-unlock.svg-CaYlbVmU.js} +1 -1
  122. package/web/assets/{interface-login-key.svg-28TYVVzH.js → interface-login-key.svg-D5F_9MJ0.js} +1 -1
  123. package/web/assets/{interface-search.svg-BS_owhIG.js → interface-search.svg-CF1D8GIV.js} +1 -1
  124. package/web/assets/{interface-setting-cog.svg-afYc0TQv.js → interface-setting-cog.svg-D4AOKG2g.js} +1 -1
  125. package/web/assets/{interface-share-megaphone-bullhorn.svg-BnW8HhjG.js → interface-share-megaphone-bullhorn.svg-BIfr2_va.js} +1 -1
  126. package/web/assets/{interface-share-rocket.svg-DvX_9178.js → interface-share-rocket.svg-NDd3dv1t.js} +1 -1
  127. package/web/assets/{interface-share-satellite.svg-B4nQqv6w.js → interface-share-satellite.svg-CVDJbmT-.js} +1 -1
  128. package/web/assets/{interface-share-space-ship.svg-Ciol-6JO.js → interface-share-space-ship.svg-D0ZLJNO2.js} +1 -1
  129. package/web/assets/{interface-share.svg-COm1OVr8.js → interface-share.svg-WpwbpOes.js} +1 -1
  130. package/web/assets/{interface-signal-square.svg-DXuF6sqG.js → interface-signal-square.svg-DnfzUw_B.js} +1 -1
  131. package/web/assets/{interface-time-clock-circle.svg-CWLe4mcs.js → interface-time-clock-circle.svg-BhxciNvX.js} +1 -1
  132. package/web/assets/{interface-time-hour-glass.svg-DXz9wBiv.js → interface-time-hour-glass.svg-DBzSaSHQ.js} +1 -1
  133. package/web/assets/{interface-users-multiple.svg-CXOw1J5l.js → interface-users-multiple.svg-BULh1giq.js} +1 -1
  134. package/web/assets/{interface-weather-moon.svg-B9H9xib3.js → interface-weather-moon.svg-D377V2os.js} +1 -1
  135. package/web/assets/{javascript-BY1URXQK.js → javascript-BTXoJ3Qy.js} +1 -1
  136. package/web/assets/{jsonMode-CxipJdzJ.js → jsonMode-D4a3Khim.js} +1 -1
  137. package/web/assets/{liquid-BbqI_ezD.js → liquid-DfO0R4Mk.js} +1 -1
  138. package/web/assets/{lspLanguageFeatures-BrbDxmfl.js → lspLanguageFeatures-BvPWuQFt.js} +1 -1
  139. package/web/assets/{mail-chat-bubble-square.svg-CAlBGAlW.js → mail-chat-bubble-square.svg-SK3IJ6QN.js} +1 -1
  140. package/web/assets/{mail-send-email-paper-airplane.svg-BoNc-FEY.js → mail-send-email-paper-airplane.svg-CJFA7B2E.js} +1 -1
  141. package/web/assets/{mail-send-envelope.svg-CnMOSib5.js → mail-send-envelope.svg-eO_-QNyE.js} +1 -1
  142. package/web/assets/{mdx-C4PcQ_oe.js → mdx-G_vwFSSe.js} +1 -1
  143. package/web/assets/{message-BWsbEnoG.js → message-CxPw04rG.js} +1 -1
  144. package/web/assets/{minus-BpDIA3FO.js → minus-Dp5ez1-1.js} +1 -1
  145. package/web/assets/{modal-DZYAJM4x.js → modal-CGT4d1lM.js} +1 -1
  146. package/web/assets/modal-D_N6NaGL.js +1 -0
  147. package/web/assets/{money-cashier-receipt.svg-DZQ9mrBZ.js → money-cashier-receipt.svg-EutWthy8.js} +1 -1
  148. package/web/assets/{money-currency-dollar-pay.svg-BCaw-GeJ.js → money-currency-dollar-pay.svg-MnLj-orw.js} +1 -1
  149. package/web/assets/{money-graph-arrow-increase.svg-DZXh-i2p.js → money-graph-arrow-increase.svg-CVCZ3TyJ.js} +1 -1
  150. package/web/assets/{money-graph-bar-chart-increase.svg-j_1tUlGM.js → money-graph-bar-chart-increase.svg-DiUmCuhj.js} +1 -1
  151. package/web/assets/{multiselect-CmjxW2sW.js → multiselect-C6uayHqj.js} +1 -1
  152. package/web/assets/{nature-ecology-leaf.svg-bajA57tM.js → nature-ecology-leaf.svg-ByaLs72Y.js} +1 -1
  153. package/web/assets/{password-W9W3PSE2.js → password-CxtOqagM.js} +1 -1
  154. package/web/assets/{phone-telephone.svg-DD5MGHqV.js → phone-telephone.svg-BKz6Jkwn.js} +1 -1
  155. package/web/assets/{plus-RuIcofM_.js → plus-BPT_7GGk.js} +1 -1
  156. package/web/assets/process.store-CMxuFjFg.js +1 -0
  157. package/web/assets/{programming-bug.svg-CjtqBc8Z.js → programming-bug.svg-BwSc8f0s.js} +1 -1
  158. package/web/assets/{programming-cloud.svg-BXQ_h3dK.js → programming-cloud.svg-CEIulNEp.js} +1 -1
  159. package/web/assets/{programming-computer-database-server.svg-D6TzOHsK.js → programming-computer-database-server.svg-Bc_F3NH0.js} +1 -1
  160. package/web/assets/{programming-computer-database.svg-Dmdh0lIx.js → programming-computer-database.svg-AYLkzJJE.js} +1 -1
  161. package/web/assets/{programming-module-four-layout.svg-CtGhpLhO.js → programming-module-four-layout.svg-DLpzPxNB.js} +1 -1
  162. package/web/assets/{programming-module-three.svg-hz_w8W7I.js → programming-module-three.svg-CcuZn8dz.js} +1 -1
  163. package/web/assets/{programming-module.svg-oDT_fQLn.js → programming-module.svg-BjbGpQeu.js} +1 -1
  164. package/web/assets/{programming-script-code.svg-CxHHTsaU.js → programming-script-code.svg-poyfodgC.js} +1 -1
  165. package/web/assets/{python-DH-AyaLR.js → python-DpMMlrWM.js} +1 -1
  166. package/web/assets/{razor-BUpT1gRX.js → razor-Ba_mg6U5.js} +1 -1
  167. package/web/assets/router-QZ5Z8Lh7.js +1 -0
  168. package/web/assets/settings-scope-D7umae8c.js +1 -0
  169. package/web/assets/{settings.store-Dj2nbYYG.js → settings.store-Dte8TdJb.js} +1 -1
  170. package/web/assets/{shopping-cart.svg-D1eK38-E.js → shopping-cart.svg-BX2KYVCw.js} +1 -1
  171. package/web/assets/{shopping-gift-present.svg-CxSfFNAS.js → shopping-gift-present.svg-BOl2vYLg.js} +1 -1
  172. package/web/assets/{shopping-shipping-box-parcel-package.svg-M5Njo2qJ.js → shopping-shipping-box-parcel-package.svg-gxfbOgnM.js} +1 -1
  173. package/web/assets/{src-CEK4EPEJ.js → src-D0LXRijj.js} +8 -8
  174. package/web/assets/{src-BTWvqafq.js → src-DWvNj-EG.js} +1 -1
  175. package/web/assets/{tabpanel-CWwOXePg.js → tabpanel-Bkt-f7d6.js} +1 -1
  176. package/web/assets/{tag-new-circle.svg-v_Go3fcT.js → tag-new-circle.svg-CTRMBJyc.js} +1 -1
  177. package/web/assets/{toggleswitch-C4A0_850.js → toggleswitch-DcDrXRKn.js} +1 -1
  178. package/web/assets/{travel-map-earth-globe.svg-Db2uRb3M.js → travel-map-earth-globe.svg-BPp4IapC.js} +1 -1
  179. package/web/assets/trpc-Dtgb4BMw.js +1 -0
  180. package/web/assets/{tsMode-CXB5XugS.js → tsMode-HfLOT-e5.js} +1 -1
  181. package/web/assets/{typescript-CHJkC9r6.js → typescript-DJAEsDnw.js} +1 -1
  182. package/web/assets/useAiChatDrawer-fzyQ-fyM.js +1 -0
  183. package/web/assets/useCloudOrg-BdpqnN19.css +1 -0
  184. package/web/assets/useCloudOrg-Bg7-icbW.js +97 -0
  185. package/web/assets/{useProjectCatalog-DDm0I6sX.js → useProjectCatalog-C95cDPIr.js} +1 -1
  186. package/web/assets/{xml-DB4EKWC9.js → xml-1frg7aol.js} +1 -1
  187. package/web/assets/{yaml-CqvLyuow.js → yaml-DXzDcMOS.js} +1 -1
  188. package/web/index.html +1 -1
  189. package/agent-manager-74WZPMVQ.js +0 -7
  190. package/chunk-YSJ6OJGN.js.map +0 -1
  191. package/src-MQWOIUB5.js.map +0 -1
  192. package/web/assets/AgentTerminalPage-A8GIX6dZ.css +0 -1
  193. package/web/assets/AgentTerminalPage-DE3GUoXn.js +0 -1
  194. package/web/assets/AiChatPage-BIJczUeS.js +0 -1
  195. package/web/assets/CloudAccountPage-MSWkg9Ke.js +0 -1
  196. package/web/assets/SettingsPage-CmtRn6VJ.js +0 -2063
  197. package/web/assets/WikiPage-DZLKTPi3.js +0 -1
  198. package/web/assets/auth-errors-BOjTx5eo.js +0 -1
  199. package/web/assets/interface-content-folder.svg-DjEfXjEF.js +0 -1
  200. package/web/assets/modal-Fe2dzlxp.js +0 -1
  201. package/web/assets/process.store-COWqXNUm.js +0 -1
  202. package/web/assets/router-BIJ4goD-.js +0 -1
  203. package/web/assets/settings-scope-DGzA7Be-.js +0 -1
  204. package/web/assets/trpc-DfyQNjZd.js +0 -1
  205. package/web/assets/useAiChatDrawer-Cuhlc2BI.js +0 -1
  206. package/web/assets/useCloudOrg-DYOv1Oak.js +0 -93
  207. package/web/assets/useCloudOrg-Dc6yRgro.css +0 -1
  208. /package/{agent-manager-74WZPMVQ.js.map → agent-manager-UUSQK6S2.js.map} +0 -0
  209. /package/web/assets/{abap-Q7FyqTce.js → abap-BvxTqXT2.js} +0 -0
  210. /package/web/assets/{apex-DBNDq2a_.js → apex-CIPCCkBr.js} +0 -0
  211. /package/web/assets/{azcli-BHW8ozYN.js → azcli-Rs0NIwkA.js} +0 -0
  212. /package/web/assets/{bat-BL-_3Nfo.js → bat-BxAhM6gR.js} +0 -0
  213. /package/web/assets/{bicep-DR2vgumW.js → bicep-oV_ehPcG.js} +0 -0
  214. /package/web/assets/{cameligo-BCjoEAZU.js → cameligo-CJLaUyQs.js} +0 -0
  215. /package/web/assets/{clojure-zw1Irdul.js → clojure-CjvMureh.js} +0 -0
  216. /package/web/assets/{coffee-BdEjkiiW.js → coffee-motWC5XQ.js} +0 -0
  217. /package/web/assets/{cpp-DKUlBIAG.js → cpp-DiWIfYNM.js} +0 -0
  218. /package/web/assets/{csharp-BsNC4zVq.js → csharp-GBlGBeai.js} +0 -0
  219. /package/web/assets/{csp-X4Chv5_9.js → csp-tSyK5eO1.js} +0 -0
  220. /package/web/assets/{css-Dh0Dlycs.js → css-BkFK9HG_.js} +0 -0
  221. /package/web/assets/{cypher-BL_DRjGX.js → cypher-Bcp8PXr2.js} +0 -0
  222. /package/web/assets/{dart-DaM4j487.js → dart-uz9Rxe0M.js} +0 -0
  223. /package/web/assets/{dockerfile-Df5WJXAt.js → dockerfile-iJizx3Zp.js} +0 -0
  224. /package/web/assets/{ecl-BjUo3uTs.js → ecl-BalaS36Z.js} +0 -0
  225. /package/web/assets/{elixir-DrLHYSxt.js → elixir-D6CLrLGy.js} +0 -0
  226. /package/web/assets/{flow9-DKiuX7rg.js → flow9-CFt3-vVY.js} +0 -0
  227. /package/web/assets/{fsharp-CAN78557.js → fsharp-DGa7raxa.js} +0 -0
  228. /package/web/assets/{go-DGjr9vA5.js → go-wlqInk4z.js} +0 -0
  229. /package/web/assets/{graphql-CcoU3Cj2.js → graphql-CP1guigM.js} +0 -0
  230. /package/web/assets/{hcl-udfyCcTm.js → hcl-CK0UkKD0.js} +0 -0
  231. /package/web/assets/{ini-BVbXatjh.js → ini-Ch56nrUj.js} +0 -0
  232. /package/web/assets/{java-Drpmh61g.js → java-C9_WIayl.js} +0 -0
  233. /package/web/assets/{julia-asFGk-ZV.js → julia-Can2_mhR.js} +0 -0
  234. /package/web/assets/{kotlin-HmkKz1TT.js → kotlin-DUvGf_BX.js} +0 -0
  235. /package/web/assets/{less-C5i2m2Dn.js → less-DoTbG-rc.js} +0 -0
  236. /package/web/assets/{lexon-B3Xh2kL6.js → lexon-BFMO2_kL.js} +0 -0
  237. /package/web/assets/{lua-DnOtgOOu.js → lua-CC-QtBo7.js} +0 -0
  238. /package/web/assets/{m3-Brj4HNKq.js → m3-C51MyH5Q.js} +0 -0
  239. /package/web/assets/{markdown-C3-4Ntpz.js → markdown-CDwLHhgZ.js} +0 -0
  240. /package/web/assets/{mips-vFQKPHzn.js → mips-YuxZtPGU.js} +0 -0
  241. /package/web/assets/{msdax-D5MYLS2R.js → msdax-CJjWhmte.js} +0 -0
  242. /package/web/assets/{mysql-CRQHhUON.js → mysql-B1riwLaD.js} +0 -0
  243. /package/web/assets/{objective-c-8cZEp0HR.js → objective-c-drXT-gmc.js} +0 -0
  244. /package/web/assets/{pascal-BPn_2jro.js → pascal-KUYgrN-Z.js} +0 -0
  245. /package/web/assets/{pascaligo-L7QnU_8B.js → pascaligo-DQmQJP-r.js} +0 -0
  246. /package/web/assets/{perl-B9zSPnp3.js → perl-09XLBmCS.js} +0 -0
  247. /package/web/assets/{pgsql-DiRJukCJ.js → pgsql-BFj9IFFu.js} +0 -0
  248. /package/web/assets/{php-Dv4g8Lhr.js → php-UnC5BqwY.js} +0 -0
  249. /package/web/assets/{pla-CKAoz6vp.js → pla-BFuYrPHG.js} +0 -0
  250. /package/web/assets/{postiats-B_PYgUTS.js → postiats-n4ft3jnO.js} +0 -0
  251. /package/web/assets/{powerquery-CAagSyo-.js → powerquery-D-s-oFSc.js} +0 -0
  252. /package/web/assets/{powershell-Cj9BzLhS.js → powershell-CTboTf93.js} +0 -0
  253. /package/web/assets/{protobuf-DTRY4FFo.js → protobuf-Dt62GuJP.js} +0 -0
  254. /package/web/assets/{pug-qADGmCS8.js → pug-CEFWkUzw.js} +0 -0
  255. /package/web/assets/{qsharp-xkZT0ZOm.js → qsharp-B0DUmgPZ.js} +0 -0
  256. /package/web/assets/{r-C7wfNC2Z.js → r-BVnF0c4T.js} +0 -0
  257. /package/web/assets/{redis-BoccK80D.js → redis-DLl3KQXi.js} +0 -0
  258. /package/web/assets/{redshift-EL8PjTts.js → redshift-cif1I7h1.js} +0 -0
  259. /package/web/assets/{restructuredtext-CA6rgR7C.js → restructuredtext-PNfql36m.js} +0 -0
  260. /package/web/assets/{ruby-BymrInur.js → ruby-j3jxDjUN.js} +0 -0
  261. /package/web/assets/{rust-BTbPL-fg.js → rust-BaIO-mAt.js} +0 -0
  262. /package/web/assets/{sb-BTkOICcb.js → sb-D8XNVNUQ.js} +0 -0
  263. /package/web/assets/{scala-CzaUHeQF.js → scala-DiK9Ps90.js} +0 -0
  264. /package/web/assets/{scheme-DZe2DRuY.js → scheme-DPKqXHmF.js} +0 -0
  265. /package/web/assets/{scss-D5oFt9cg.js → scss-DpMwfP2H.js} +0 -0
  266. /package/web/assets/{shell-DCkIF5jZ.js → shell-Dw9mKVg2.js} +0 -0
  267. /package/web/assets/{solidity-CfOO4gXi.js → solidity-DTxo-7fe.js} +0 -0
  268. /package/web/assets/{sophia-0-sROOHR.js → sophia-BjA3Q87y.js} +0 -0
  269. /package/web/assets/{sparql-Dzzr9hol.js → sparql-q1EpMUqb.js} +0 -0
  270. /package/web/assets/{sql-DbXHiHh_.js → sql-CjcQ4wIr.js} +0 -0
  271. /package/web/assets/{st-DEaYV8xH.js → st-AuOpxVl7.js} +0 -0
  272. /package/web/assets/{swift-BQItveav.js → swift-BUWnv6f7.js} +0 -0
  273. /package/web/assets/{systemverilog-nE6YfGlx.js → systemverilog-FhXn-3OZ.js} +0 -0
  274. /package/web/assets/{tcl-Do7Q0SM0.js → tcl-B0lqcTtS.js} +0 -0
  275. /package/web/assets/{twig-B2CLZN2j.js → twig-B0OeGPVa.js} +0 -0
  276. /package/web/assets/{typespec-r9R1AgVi.js → typespec-Bmh6VX11.js} +0 -0
  277. /package/web/assets/{vb-DGnD02qr.js → vb-bmgX6veu.js} +0 -0
  278. /package/web/assets/{wgsl-C2fD4koY.js → wgsl-FflLnFqI.js} +0 -0
@@ -60,9 +60,11 @@ import {
60
60
  isVaultLocked,
61
61
  keyRoot,
62
62
  keyedFilePath,
63
+ killInstance,
63
64
  legacyMachineFilePath,
64
65
  legacyVaultRoot,
65
66
  listCloudVaults,
67
+ listInstances,
66
68
  listOrgVaults,
67
69
  loadFromEnv,
68
70
  localVaultRoot,
@@ -79,6 +81,7 @@ import {
79
81
  readOrgNames,
80
82
  refOfProjectAmong,
81
83
  refsOfProjectId,
84
+ registerInstance,
82
85
  registerProjectLoader,
83
86
  rememberFile,
84
87
  rememberOrgName,
@@ -93,11 +96,12 @@ import {
93
96
  stableEncryptValue,
94
97
  stampOrgCloud,
95
98
  traefikManager,
99
+ unregisterInstance,
96
100
  vaultFor,
97
101
  vaultIdOfRoot,
98
102
  wasRead,
99
103
  writeEntity
100
- } from "./chunk-YSJ6OJGN.js";
104
+ } from "./chunk-Z3KAXI7V.js";
101
105
 
102
106
  // ../server/src/create-server.ts
103
107
  import express from "express";
@@ -2555,1440 +2559,1448 @@ var settingsManager = new SettingsManager();
2555
2559
  import { isAbsolute as isAbsolute3 } from "path";
2556
2560
 
2557
2561
  // ../server/src/services/workspace-cwd.ts
2558
- import { isAbsolute as isAbsolute2, join as join9, dirname as dirname4 } from "path";
2559
- async function projectRoot(ref) {
2560
- const project = await projectStore.get(ref);
2561
- if (!project?.orgId) return null;
2562
- return projectPathStore.get(project);
2562
+ import { isAbsolute as isAbsolute2, join as join10, dirname as dirname5 } from "path";
2563
+
2564
+ // ../server/src/services/conversation-store.ts
2565
+ import { randomUUID as randomUUID3 } from "crypto";
2566
+ import { readFile as readFile8, writeFile as writeFile8, rename as rename8, mkdir as mkdir8, chmod as chmod7 } from "fs/promises";
2567
+ import { dirname as dirname4, join as join9 } from "path";
2568
+ import { EventEmitter as EventEmitter2 } from "events";
2569
+ var FILENAME4 = "conversations.private.json";
2570
+ async function localProjectId() {
2571
+ const projects = await projectStore.list().catch(() => []);
2572
+ return projects.find((p) => !p.orgId)?.id ?? null;
2563
2573
  }
2564
- async function resolveWorkspaceCwd(target) {
2565
- const launchDir = localVaultRootParent();
2566
- if (target.serviceId) {
2567
- const service = await serviceStore.get(target.serviceId);
2568
- if (service) {
2569
- const raw = (target.override ?? service.cwd ?? "").trim();
2570
- const projects = await projectStore.list();
2571
- const owner = projects.find((p) => p.serviceIds.includes(service.id));
2572
- const root = owner ? await projectRoot(owner) : null;
2573
- return root ? anchor(root, raw) : raw || ".";
2574
- }
2574
+ var ConversationStore = class {
2575
+ conversations = /* @__PURE__ */ new Map();
2576
+ /** Which vault each conversation was read from, until its project is known. */
2577
+ roots = /* @__PURE__ */ new Map();
2578
+ /**
2579
+ * Les conversations qu'on n'a pas su déchiffrer, par coffre, sous leur forme
2580
+ * de disque.
2581
+ *
2582
+ * Une entrée illisible était simplement ignorée. Tant qu'aucune de ses
2583
+ * voisines ne se chargeait, le fichier n'était jamais réécrit et la donnée
2584
+ * survivait par accident ; dès qu'une seule s'ouvrait, la sauvegarde suivante
2585
+ * réécrivait le fichier avec elle seule — et effaçait les autres. Elles sont
2586
+ * donc gardées ici, telles quelles, et réémises à l'identique.
2587
+ */
2588
+ unreadable = /* @__PURE__ */ new Map();
2589
+ loaded = false;
2590
+ events = new EventEmitter2().setMaxListeners(0);
2591
+ saveQueue = Promise.resolve();
2592
+ snapshotConversation(conv) {
2593
+ return structuredClone(conv);
2575
2594
  }
2576
- if (target.projectId) {
2577
- const root = await projectRoot({ id: target.projectId, orgId: target.orgId });
2578
- const raw = (target.override ?? "").trim();
2579
- return root ? anchor(root, raw) : raw || launchDir;
2595
+ /**
2596
+ * Encrypted, so it lives beside the key that opens it — usually the machine
2597
+ * root, but a launch directory owning its own key keeps both together.
2598
+ */
2599
+ getFilePath() {
2600
+ return keyedFilePath(FILENAME4);
2580
2601
  }
2581
- return anchor(launchDir, (target.override ?? "").trim());
2582
- }
2583
- function anchor(base, raw) {
2584
- if (!raw || raw === ".") return base;
2585
- if (raw.startsWith("~") || raw.startsWith("$") || isAbsolute2(raw)) return raw;
2586
- return join9(base, raw);
2587
- }
2588
- function localVaultRootParent() {
2589
- return dirname4(localVaultRoot());
2590
- }
2591
- async function resolveAgentCwd(target) {
2592
- if (target.serviceId) {
2593
- const service = await serviceStore.get(target.serviceId);
2594
- if (service) {
2595
- const projects = await projectStore.list();
2596
- const owner = projects.find((p) => p.serviceIds.includes(service.id));
2597
- const root = owner ? await projectRoot(owner) : null;
2598
- if (!root) return void 0;
2599
- return anchor(root, (target.override ?? service.cwd ?? "").trim());
2600
- }
2602
+ /** Where older versions kept it — read only, so an older Runeya keeps working. */
2603
+ getLegacyFilePath() {
2604
+ return legacyMachineFilePath(FILENAME4);
2601
2605
  }
2602
- if (target.projectId) {
2603
- const root = await projectRoot({ id: target.projectId, orgId: target.orgId });
2604
- if (!root) return void 0;
2605
- return anchor(root, (target.override ?? "").trim());
2606
+ /**
2607
+ * Read the machine file, falling back to the launch directory of old.
2608
+ *
2609
+ * The legacy file is left in place: the next save writes the new location,
2610
+ * and a directory still opened by an older Runeya keeps working meanwhile.
2611
+ */
2612
+ async readFromDisk() {
2613
+ try {
2614
+ return await readFile8(await this.getFilePath(), "utf-8");
2615
+ } catch {
2616
+ return readFile8(this.getLegacyFilePath(), "utf-8");
2617
+ }
2606
2618
  }
2607
- return void 0;
2608
- }
2609
-
2610
- // ../server/src/trpc/routers/service.ts
2611
- async function resolveEffectiveAgentId(service) {
2612
- const known = /* @__PURE__ */ new Set([
2613
- ...agentManager.listAgents().map((a) => a.config.id),
2614
- ...(await agentStore.list()).map((a) => a.id)
2615
- ]);
2616
- if (service.agentId && known.has(service.agentId)) return service.agentId;
2617
- const projects = await projectStore.list();
2618
- const project = projects.find((p) => p.serviceIds.includes(service.id));
2619
- if (project?.activeEnvironmentId) {
2620
- const activeEnv = await environmentStore.get(project.activeEnvironmentId);
2621
- if (activeEnv?.agentId && known.has(activeEnv.agentId)) return activeEnv.agentId;
2619
+ async ensureDir() {
2620
+ await mkdir8(dirname4(await this.getFilePath()), { recursive: true });
2622
2621
  }
2623
- return agentManager.getDefaultAgentId();
2624
- }
2625
- async function resolveServiceVariables(service) {
2626
- const projects = await projectStore.list();
2627
- const project = projects.find((p) => p.serviceIds.includes(service.id));
2628
- let resolvedService = service;
2629
- const anchored = await resolveWorkspaceCwd({ serviceId: service.id });
2630
- if (anchored !== (service.cwd ?? "").trim() && isAbsolute3(anchored)) {
2631
- resolvedService = { ...resolvedService, resolvedCwd: anchored };
2622
+ encryptMessages(messages, vault) {
2623
+ return encryptValue(JSON.stringify(messages), vault);
2632
2624
  }
2633
- try {
2634
- if (service.parserIds && service.parserIds.length > 0) {
2635
- const settings = await settingsManager.getSettings();
2636
- const allParsers = [...nativeParsers, ...settings.logParsers || []];
2637
- const resolvedParsers = service.parserIds.map((id) => allParsers.find((p) => p.id === id)).filter((p) => p !== void 0);
2638
- resolvedService = { ...resolvedService, resolvedLogParsers: resolvedParsers };
2639
- }
2640
- } catch (err) {
2641
- console.error("[service-router] Failed to resolve parsers for service", service.id + ":", err.message);
2625
+ decryptMessages(encrypted, vault) {
2626
+ const json = decryptValue(encrypted, vault);
2627
+ return JSON.parse(json);
2642
2628
  }
2643
- if (!project || !project.activeEnvironmentId) {
2644
- return resolvedService;
2629
+ /**
2630
+ * Refuser d'écrire dans le coffre d'une organisation sans nommer sa clé.
2631
+ *
2632
+ * `getEncryptionKey(undefined)` rend la clé machine, et c'est le bon défaut
2633
+ * pour le coffre local. Appliqué à un fichier rangé sous `orgs/<org>/`, il
2634
+ * produit un chiffré que ce coffre n'emploie pas — lisible par personne à la
2635
+ * relecture, et impossible à distinguer d'une corruption.
2636
+ *
2637
+ * Le cas a été observé plusieurs fois sans qu'on parvienne à nommer le
2638
+ * chemin fautif : les garde-fous en place traitent le coffre fermé, le
2639
+ * coffre mal résolu et la copie périmée, aucun ne couvrait « coffre non
2640
+ * précisé ». Ces écritures-là n'ont jamais fonctionné — elles échouaient en
2641
+ * silence, ce qui est pire. On les saute désormais en le disant, avec la
2642
+ * pile d'appels qui manquait pour remonter au chemin fautif.
2643
+ */
2644
+ isVaultNamed(root, vault) {
2645
+ const orgId = orgIdOfRoot(root);
2646
+ if (!orgId || vault) return true;
2647
+ console.error(
2648
+ `[conversation-store] \xC9criture ignor\xE9e dans le coffre de l'organisation ${orgId} (${root}) : aucune cl\xE9 d'organisation nomm\xE9e, le contenu serait chiffr\xE9 par la cl\xE9 machine et illisible \xE0 la relecture.
2649
+ ${new Error("trace").stack}`
2650
+ );
2651
+ return false;
2645
2652
  }
2646
- try {
2647
- const resolved = await environmentResolver.resolveForService({
2648
- serviceId: service.id,
2649
- activeEnvironmentId: project.activeEnvironmentId
2650
- });
2651
- const serviceBuiltins = {
2652
- HOST: env.HOST,
2653
- HOST_LAN: "0.0.0.0"
2653
+ toDisk(conv, vault) {
2654
+ return {
2655
+ id: conv.id,
2656
+ projectId: conv.projectId,
2657
+ title: conv.title,
2658
+ encryptedMessages: this.encryptMessages(conv.messages, vault),
2659
+ isCli: conv.isCli,
2660
+ runner: conv.runner,
2661
+ providerId: conv.providerId,
2662
+ codexThreadId: conv.codexThreadId,
2663
+ contextUsage: conv.contextUsage,
2664
+ workflowId: conv.workflowId,
2665
+ workflowType: conv.workflowType,
2666
+ kanbanBoardId: conv.kanbanBoardId,
2667
+ kanbanCardId: conv.kanbanCardId,
2668
+ archived: conv.archived,
2669
+ createdAt: conv.createdAt,
2670
+ updatedAt: conv.updatedAt
2654
2671
  };
2655
- const interpolatedEnv = {};
2656
- for (const [key, value] of Object.entries(resolved.env)) {
2657
- interpolatedEnv[key] = value.replace(
2658
- /\{\{service\.([^}]+)\}\}/g,
2659
- (match, k) => serviceBuiltins[k] !== void 0 ? serviceBuiltins[k] : match
2660
- );
2661
- }
2662
- resolvedService = {
2663
- ...resolvedService,
2664
- env: interpolatedEnv,
2665
- envSecrets: [...resolved.envSecrets]
2672
+ }
2673
+ fromDisk(disk, vault) {
2674
+ const workflowType = (disk.workflowType === "scenario" ? "scenario" : void 0) ?? (disk.scenarioId ? "scenario" : void 0);
2675
+ return {
2676
+ id: disk.id,
2677
+ projectId: disk.projectId,
2678
+ title: disk.title,
2679
+ messages: this.decryptMessages(disk.encryptedMessages, vault),
2680
+ isCli: disk.isCli ?? disk.isClaudeCode ?? false,
2681
+ runner: disk.runner,
2682
+ providerId: disk.providerId,
2683
+ codexThreadId: disk.codexThreadId,
2684
+ contextUsage: disk.contextUsage,
2685
+ workflowId: disk.workflowId ?? disk.scenarioId,
2686
+ workflowType,
2687
+ kanbanBoardId: disk.kanbanBoardId,
2688
+ kanbanCardId: disk.kanbanCardId,
2689
+ archived: disk.archived,
2690
+ createdAt: disk.createdAt,
2691
+ updatedAt: disk.updatedAt
2666
2692
  };
2667
- } catch (err) {
2668
- console.error("[service-router] Failed to resolve variables for service", service.id + ":", err.message);
2669
2693
  }
2670
- return resolvedService;
2671
- }
2672
- var DockerConfigInput = DockerConfigSchema;
2673
- var serviceRouter = router({
2674
- list: protectedProcedure.meta({ openapi: { method: "GET", path: "/services", tags: ["services"], summary: "List all services", protect: true } }).output(z4.any()).query(async ({ ctx }) => {
2675
- const all = await serviceStore.list();
2676
- const allowedIds = getAllowedServiceIds(ctx);
2677
- const filtered = allowedIds ? all.filter((s) => allowedIds.has(s.id)) : all;
2678
- return Promise.all(filtered.map(async (s) => ({
2679
- ...s,
2680
- effectiveAgentId: await resolveEffectiveAgentId(s)
2681
- })));
2682
- }),
2683
- get: protectedProcedure.meta({ openapi: { method: "GET", path: "/services/{id}", tags: ["services"], summary: "Get a service by ID", protect: true } }).input(z4.object({ id: z4.string().max(128) })).output(z4.any()).query(async ({ input, ctx }) => {
2684
- requireServiceAction(ctx, input.id, "service:read");
2685
- const service = await serviceStore.get(input.id);
2686
- if (!service) {
2687
- throw new TRPCError4({ code: "NOT_FOUND", message: `Service ${input.id} not found` });
2694
+ /**
2695
+ * Read every project vault, then the homes older versions used.
2696
+ *
2697
+ * A conversation found in a legacy file is attributed to the local project of
2698
+ * that directory which is what it was about, back when one directory meant
2699
+ * one project. With no local project to point at, it is left unattributed
2700
+ * rather than filed under a project picked at random.
2701
+ */
2702
+ async load() {
2703
+ if (this.loaded) return;
2704
+ if (!getEncryptionKey()) {
2705
+ return;
2688
2706
  }
2689
- return service;
2690
- }),
2691
- create: protectedProcedure.meta({ openapi: { method: "POST", path: "/services", tags: ["services"], summary: "Create a new service", protect: true } }).output(z4.any()).input(
2692
- z4.object({
2693
- agentId: z4.string().max(128).optional(),
2694
- name: z4.string().min(1).max(200),
2695
- commands: z4.array(ServiceCommandSchema).max(50).optional(),
2696
- cwd: z4.string().max(4096).optional(),
2697
- ports: z4.array(z4.number().int().min(1).max(65535)).max(20).optional(),
2698
- groups: z4.array(z4.string().max(200)).max(20).optional(),
2699
- description: z4.string().max(2e3).optional(),
2700
- docsPath: z4.string().max(4096).optional(),
2701
- url: z4.string().max(2048).optional(),
2702
- localUrls: z4.array(z4.object({
2703
- url: z4.string().max(253),
2704
- port: z4.number().int().min(1).max(65535)
2705
- })).max(20).optional(),
2706
- openapiUrl: z4.string().max(2048).optional(),
2707
- git: z4.object({ remote: z4.string().max(2048).optional(), home: z4.string().max(2048).optional() }).optional(),
2708
- meta: z4.record(z4.string().max(256), z4.string().max(2e3)).optional().refine((obj) => !obj || Object.keys(obj).length <= 100, { message: "Too many meta keys (max 100)" }),
2709
- runner: z4.enum(["native", "docker"]).optional().default("native"),
2710
- dockerConfig: DockerConfigInput.optional(),
2711
- healthCheck: HealthCheckSchema.optional(),
2712
- autoRestart: z4.boolean().optional().default(false),
2713
- restartStrategy: RestartStrategySchema.optional(),
2714
- maxRestarts: z4.number().int().nonnegative().optional(),
2715
- restartBackoffMs: z4.number().int().positive().optional().default(1e3),
2716
- orgId: z4.string().max(128).nullable().optional(),
2717
- shortcuts: z4.array(ServiceShortcutSchema).max(50).optional(),
2718
- parserIds: z4.array(z4.string().max(128)).max(50).optional()
2719
- })
2720
- ).mutation(async ({ input }) => {
2721
- if (input.agentId) {
2722
- const status = agentManager.getStatus(input.agentId);
2723
- if (!status) {
2724
- throw new TRPCError4({
2725
- code: "NOT_FOUND",
2726
- message: `Agent ${input.agentId} not found`
2727
- });
2707
+ this.loaded = true;
2708
+ this.unreadable.clear();
2709
+ await ensureProjectsLoaded();
2710
+ const vaults = await allVaultRoots();
2711
+ const legacyPaths = [this.getLegacyFilePath(), await keyedFilePath(FILENAME4)];
2712
+ const sources = [
2713
+ ...legacyPaths.map((path) => ({ path, legacy: true })),
2714
+ ...vaults.map((root) => ({ path: join9(root, FILENAME4), legacy: false }))
2715
+ ];
2716
+ for (const { path, legacy } of sources) {
2717
+ let data;
2718
+ try {
2719
+ data = JSON.parse(await readFile8(path, "utf-8"));
2720
+ } catch {
2721
+ continue;
2728
2722
  }
2729
- }
2730
- if (input.runner === "docker") {
2731
- throw new TRPCError4({ code: "BAD_REQUEST", message: 'Docker runner is not yet supported. Use "native" runner.' });
2732
- }
2733
- const service = await serviceStore.create({
2734
- ...input,
2735
- agentId: input.agentId
2736
- });
2737
- await auditService.log({ userId: "local", action: "create", entityType: "service", entityId: service.id, metadata: { name: input.name } });
2738
- return service;
2739
- }),
2740
- update: protectedProcedure.meta({ openapi: { method: "PATCH", path: "/services/{id}", tags: ["services"], summary: "Update a service", protect: true } }).output(z4.any()).input(
2741
- z4.object({
2742
- id: z4.string().max(128),
2743
- agentId: z4.string().max(128).nullable().optional(),
2744
- // null = clear (inherit from env)
2745
- name: z4.string().min(1).max(200).optional(),
2746
- commands: z4.array(ServiceCommandSchema).max(50).optional(),
2747
- cwd: z4.string().max(4096).nullable().optional(),
2748
- ports: z4.array(z4.number().int().min(1).max(65535)).max(20).optional(),
2749
- groups: z4.array(z4.string().max(200)).max(20).optional(),
2750
- description: z4.string().max(2e3).optional(),
2751
- docsPath: z4.string().max(4096).optional(),
2752
- url: z4.string().max(2048).optional(),
2753
- localUrls: z4.array(z4.object({
2754
- url: z4.string().max(253),
2755
- port: z4.number().int().min(1).max(65535)
2756
- })).max(20).optional(),
2757
- openapiUrl: z4.string().max(2048).optional(),
2758
- git: z4.object({ remote: z4.string().max(2048).optional(), home: z4.string().max(2048).optional() }).nullable().optional(),
2759
- meta: z4.record(z4.string().max(256), z4.string().max(2e3)).nullable().optional().refine((obj) => !obj || Object.keys(obj).length <= 100, { message: "Too many meta keys (max 100)" }),
2760
- runner: z4.enum(["native", "docker"]).optional(),
2761
- dockerConfig: DockerConfigInput.optional(),
2762
- healthCheck: HealthCheckSchema.nullable().optional(),
2763
- autoRestart: z4.boolean().optional(),
2764
- clearLogsOnStart: z4.boolean().optional(),
2765
- restartStrategy: RestartStrategySchema.optional(),
2766
- maxRestarts: z4.number().int().nonnegative().optional(),
2767
- restartBackoffMs: z4.number().int().positive().optional(),
2768
- orgId: z4.string().max(128).nullable().optional(),
2769
- shortcuts: z4.array(ServiceShortcutSchema).max(50).optional(),
2770
- parserIds: z4.array(z4.string().max(128)).max(50).optional(),
2771
- logSources: z4.array(LogSourceConfigSchema).max(20).optional()
2772
- })
2773
- ).mutation(async ({ input }) => {
2774
- const existing = await serviceStore.get(input.id);
2775
- if (!existing) {
2776
- throw new TRPCError4({ code: "NOT_FOUND", message: `Service ${input.id} not found` });
2777
- }
2778
- let stoppedForAgentChange = false;
2779
- if (input.agentId !== void 0 && input.agentId !== existing.agentId && existing.agentId) {
2780
- const oldAgentStatus = agentManager.getStatus(existing.agentId);
2781
- if (oldAgentStatus?.connected) {
2723
+ const inherited = legacy ? await localProjectId() : null;
2724
+ const root = dirname4(path);
2725
+ await rememberFile(path);
2726
+ for (const item of data) {
2782
2727
  try {
2783
- const processes = await agentManager.agentQuery(existing.agentId, "process.list");
2784
- const proc = processes.find((p) => p.serviceId === input.id);
2785
- if (proc && (proc.state === "running" || proc.state === "starting")) {
2786
- await agentManager.agentMutation(existing.agentId, "process.stop", { id: input.id }, AGENT_STOP_FETCH_TIMEOUT);
2787
- stoppedForAgentChange = true;
2788
- }
2728
+ const conv = this.fromDisk(item, vaultIdOfRoot(root));
2729
+ if (!conv.projectId && inherited) conv.projectId = inherited;
2730
+ this.conversations.set(item.id, conv);
2731
+ this.roots.set(item.id, root);
2789
2732
  } catch {
2790
- }
2791
- }
2792
- if (input.agentId) {
2793
- const newAgentStatus = agentManager.getStatus(input.agentId);
2794
- if (!newAgentStatus) {
2795
- throw new TRPCError4({
2796
- code: "NOT_FOUND",
2797
- message: `Agent ${input.agentId} not found`
2798
- });
2799
- }
2800
- }
2801
- }
2802
- const service = await serviceStore.update(input);
2803
- if (!service) {
2804
- throw new TRPCError4({ code: "NOT_FOUND", message: `Service ${input.id} not found` });
2805
- }
2806
- if (input.name && input.name !== existing.name) {
2807
- await environmentStore.renameByReference(input.id, input.name);
2808
- }
2809
- if (service.agentId) {
2810
- const agentStatus = agentManager.getStatus(service.agentId);
2811
- if (agentStatus?.connected) {
2812
- try {
2813
- const resolvedService = await resolveServiceVariables(service);
2814
- await agentManager.agentMutation(service.agentId, "process.updateConfig", {
2815
- serviceId: service.id,
2816
- config: resolvedService
2817
- });
2818
- } catch (err) {
2819
- console.debug("[service-router] Failed to propagate config to agent", service.agentId + ":", err.message);
2733
+ const bucket = this.unreadable.get(root) ?? [];
2734
+ bucket.push(item);
2735
+ this.unreadable.set(root, bucket);
2820
2736
  }
2821
2737
  }
2822
2738
  }
2823
- await auditService.log({ userId: "local", action: "update", entityType: "service", entityId: input.id });
2824
- return { ...service, effectiveAgentId: await resolveEffectiveAgentId(service), stoppedForAgentChange };
2825
- }),
2826
- delete: protectedProcedure.meta({ openapi: { method: "DELETE", path: "/services/{id}", tags: ["services"], summary: "Delete a service", protect: true } }).input(z4.object({ id: z4.string().max(128) })).output(z4.object({ success: z4.literal(true) })).mutation(async ({ input }) => {
2827
- const deleted = await serviceStore.delete(input.id);
2828
- if (!deleted) {
2829
- throw new TRPCError4({ code: "NOT_FOUND", message: `Service ${input.id} not found` });
2739
+ }
2740
+ /**
2741
+ * Oublier ce qui a été lu, pour tout relire sous la clé courante.
2742
+ *
2743
+ * Appelé après l'adoption d'une clé d'organisation : le disque vient d'être
2744
+ * re-chiffré sous les pieds du store, si bien que sa mémoire décrit un état
2745
+ * qui n'existe plus. La laisser en place n'affichait pas seulement des
2746
+ * comptes périmés — la sauvegarde suivante réécrivait l'ancien chiffré
2747
+ * par-dessus le nouveau, et défaisait l'adoption.
2748
+ */
2749
+ resetCache() {
2750
+ this.conversations.clear();
2751
+ this.roots.clear();
2752
+ this.unreadable.clear();
2753
+ this.loaded = false;
2754
+ this.events.emit("changed", { type: "reloaded" });
2755
+ }
2756
+ /**
2757
+ * Combien de conversations ce coffre garde sans savoir les lire, par
2758
+ * organisation. La machine elle-même est rangée sous `null`.
2759
+ */
2760
+ async unreadableByVault() {
2761
+ await this.load();
2762
+ const out = /* @__PURE__ */ new Map();
2763
+ for (const [root, items] of this.unreadable) {
2764
+ const vault = vaultIdOfRoot(root) ?? null;
2765
+ out.set(vault, (out.get(vault) ?? 0) + items.length);
2830
2766
  }
2831
- await auditService.log({ userId: "local", action: "delete", entityType: "service", entityId: input.id });
2832
- return { success: true };
2833
- }),
2834
- checkCwd: protectedProcedure.input(z4.object({ id: z4.string().max(128), checkReachability: z4.boolean().optional() })).query(async ({ input }) => {
2835
- const service = await serviceStore.get(input.id);
2836
- if (!service) {
2837
- throw new TRPCError4({ code: "NOT_FOUND", message: `Service ${input.id} not found` });
2767
+ return out;
2768
+ }
2769
+ /**
2770
+ * Write each vault that holds or held a conversation.
2771
+ *
2772
+ * A vault emptied by this save is still written, as an empty list: a
2773
+ * conversation that found its project must not survive in the file it was
2774
+ * read from.
2775
+ */
2776
+ async saveInternal() {
2777
+ if (!getEncryptionKey()) return;
2778
+ const byRoot = /* @__PURE__ */ new Map();
2779
+ for (const root of this.roots.values()) byRoot.set(root, []);
2780
+ for (const conv of this.conversations.values()) {
2781
+ const root = await this.rootFor(conv);
2782
+ this.roots.set(conv.id, root);
2783
+ const bucket = byRoot.get(root) ?? [];
2784
+ const vault = vaultIdOfRoot(root);
2785
+ if (!this.isVaultNamed(root, vault)) continue;
2786
+ bucket.push(this.toDisk(conv, vault));
2787
+ byRoot.set(root, bucket);
2838
2788
  }
2839
- const effectiveAgentId = await resolveEffectiveAgentId(service);
2840
- if (!effectiveAgentId) {
2841
- throw new TRPCError4({ code: "BAD_REQUEST", message: "No agent configured for this service" });
2789
+ for (const [root, items] of this.unreadable) {
2790
+ byRoot.set(root, [...items, ...byRoot.get(root) ?? []]);
2842
2791
  }
2843
- const agentResult = await agentManager.agentQuery(effectiveAgentId, "git.checkCwd", {
2844
- // Anchored on the project, so the check answers about the directory the
2845
- // service will actually run in.
2846
- cwd: await resolveWorkspaceCwd({ serviceId: service.id }),
2847
- remote: service.git?.remote,
2848
- checkReachability: input.checkReachability
2849
- });
2850
- const { exists: exists4, actualRemote, unreachable, resolvedCwd, unresolvedVars } = agentResult;
2851
- if (unresolvedVars.length > 0) {
2852
- return { exists: false, canClone: false, unresolvedVars, resolvedCwd, gitMismatch: null };
2792
+ const stale = /* @__PURE__ */ new Set();
2793
+ for (const root of byRoot.keys()) {
2794
+ const path = join9(root, FILENAME4);
2795
+ if (wasRead(path) && await hasChangedSinceRead(path)) stale.add(root);
2853
2796
  }
2854
- const canClone = !!service.git?.remote && (!exists4 || actualRemote === null);
2855
- let gitMismatch = null;
2856
- if (exists4 && service.git?.remote && !canClone) {
2857
- if (actualRemote === null) {
2858
- gitMismatch = { actual: null, configured: service.git.remote };
2859
- } else if (actualRemote !== service.git.remote) {
2860
- gitMismatch = { actual: actualRemote, configured: service.git.remote };
2861
- } else if (unreachable) {
2862
- gitMismatch = { actual: actualRemote, configured: service.git.remote, unreachable: true };
2797
+ for (const [root, data] of byRoot) {
2798
+ const filePath = join9(root, FILENAME4);
2799
+ if (stale.has(root)) {
2800
+ console.warn(
2801
+ `[conversation-store] ${filePath} a chang\xE9 depuis sa lecture \u2014 \xE9criture ignor\xE9e pour ne pas \xE9craser une r\xE9paration faite en dehors du serveur.`
2802
+ );
2803
+ continue;
2863
2804
  }
2805
+ await mkdir8(root, { recursive: true });
2806
+ const tmpPath = `${filePath}.tmp`;
2807
+ await writeFile8(tmpPath, JSON.stringify(data, null, 2), "utf-8");
2808
+ await rename8(tmpPath, filePath);
2809
+ await chmod7(filePath, 384);
2810
+ await rememberFile(filePath);
2864
2811
  }
2865
- return { exists: exists4, canClone, unresolvedVars: [], resolvedCwd, gitMismatch };
2866
- }),
2867
- cloneRepository: protectedProcedure.input(z4.object({ id: z4.string().max(128) })).mutation(async ({ input }) => {
2868
- const service = await serviceStore.get(input.id);
2869
- if (!service) {
2870
- throw new TRPCError4({ code: "NOT_FOUND", message: `Service ${input.id} not found` });
2871
- }
2872
- if (!service.git?.remote) {
2873
- throw new TRPCError4({ code: "BAD_REQUEST", message: "Service has no git remote configured" });
2874
- }
2875
- const effectiveAgentId = await resolveEffectiveAgentId(service);
2876
- if (!effectiveAgentId) {
2877
- throw new TRPCError4({ code: "BAD_REQUEST", message: "No agent configured for this service" });
2878
- }
2879
- await agentManager.agentMutation(effectiveAgentId, "git.clone", {
2880
- remote: service.git.remote,
2881
- // Clones WRITE: landing this in the launch directory instead of the
2882
- // project's is how a repository ends up in the wrong place.
2883
- cwd: await resolveWorkspaceCwd({ serviceId: service.id })
2884
- }, 5 * 60 * 1e3);
2885
- return { success: true };
2886
- })
2887
- });
2888
-
2889
- // ../server/src/trpc/routers/agent.ts
2890
- import { z as z5 } from "zod";
2891
- import { TRPCError as TRPCError5 } from "@trpc/server";
2892
- var SUBSCRIPTION_QUEUE_MAX = LOG_BUFFER_SIZE;
2893
- var agentRouter = router({
2894
- /** List all registered agents with their connection status */
2895
- list: protectedProcedure.query(() => {
2896
- return agentManager.listAgents();
2897
- }),
2898
- /** Get a single agent's config and status */
2899
- get: protectedProcedure.input(z5.object({ id: z5.string().max(128) })).query(({ input }) => {
2900
- const agent = agentManager.getAgent(input.id);
2901
- if (!agent) {
2902
- throw new TRPCError5({ code: "NOT_FOUND", message: `Agent ${input.id} not found` });
2903
- }
2904
- return {
2905
- config: { ...agent.config, passphrase: "***" },
2906
- status: agent.status
2907
- };
2908
- }),
2909
- /** Test connectivity to an agent given url + passphrase */
2910
- testConnection: protectedProcedure.input(
2911
- z5.object({
2912
- url: z5.string().url().max(2048),
2913
- passphrase: z5.string().min(1).max(1024).optional(),
2914
- agentId: z5.string().max(128).optional()
2915
- })
2916
- ).mutation(async ({ input }) => {
2917
- let passphrase = input.passphrase;
2918
- let targetUrl = input.url;
2919
- if (!passphrase && input.agentId) {
2920
- const agent = agentManager.getAgentConfig(input.agentId);
2921
- if (!agent) {
2922
- throw new TRPCError5({ code: "NOT_FOUND", message: "Agent not found" });
2923
- }
2924
- passphrase = agent.passphrase;
2925
- targetUrl = agent.url;
2926
- }
2927
- if (!passphrase) {
2928
- throw new TRPCError5({ code: "BAD_REQUEST", message: "Passphrase is required" });
2929
- }
2930
- const url = new URL("/api/trpc/health.check", targetUrl);
2812
+ }
2813
+ /**
2814
+ * The vault a conversation is written to: its project's.
2815
+ *
2816
+ * One that names no project stays where it was read, falling back to the
2817
+ * key's own directory never lost for want of an owner.
2818
+ */
2819
+ async rootFor(conv) {
2820
+ const known = this.roots.get(conv.id) ?? null;
2821
+ if (conv.projectId) return rootOfProjectAmong(conv.projectId, known);
2822
+ return known ?? dirname4(await keyedFilePath(FILENAME4));
2823
+ }
2824
+ /**
2825
+ * Le projet d'une conversation, organisation comprise.
2826
+ *
2827
+ * Ce que la conversation ne dit pas d'elle-même : elle ne nomme que l'id.
2828
+ * Le coffre d'où elle vient fournit l'organisation, et c'est ce couple qu'il
2829
+ * faut pour retrouver le répertoire de travail du projet.
2830
+ */
2831
+ async projectRefOf(conv) {
2832
+ if (!conv.projectId) return null;
2833
+ const known = this.roots.get(conv.id) ?? null;
2834
+ const ref = refOfProjectAmong(conv.projectId, known);
2835
+ return { id: ref.id, orgId: ref.orgId ?? null };
2836
+ }
2837
+ /**
2838
+ * Réécrire chaque coffre, en déplaçant ce qui appartient désormais ailleurs.
2839
+ *
2840
+ * Appelé quand un projet a changé de coffre : ses conversations sont écrites
2841
+ * dans le nouveau, et le fichier d'où elles viennent est réécrit sans elles.
2842
+ */
2843
+ async relocateAll() {
2844
+ await this.load();
2845
+ await this.save();
2846
+ }
2847
+ async save() {
2848
+ const write = this.saveQueue.then(() => this.saveInternal());
2849
+ this.saveQueue = write.catch(() => {
2850
+ });
2851
+ return write;
2852
+ }
2853
+ async list() {
2854
+ await this.load();
2855
+ return Array.from(this.conversations.values()).sort((a, b) => b.updatedAt - a.updatedAt);
2856
+ }
2857
+ /**
2858
+ * Les conversations d'un projet.
2859
+ *
2860
+ * Celle qui ne nomme aucun projet — écrite avant le champ, ou qu'une
2861
+ * migration n'a pas su attribuer — n'est rendue que dans les projets de son
2862
+ * propre coffre : elle reste atteignable là où elle a été écrite, sans se
2863
+ * déverser dans toutes les organisations de la machine.
2864
+ */
2865
+ async listForProject(ref) {
2866
+ const belongs = await this.matcherForProject(ref);
2867
+ return (await this.list()).filter(belongs);
2868
+ }
2869
+ /**
2870
+ * Le test d'appartenance à un projet, préparé une fois.
2871
+ *
2872
+ * Le flux temps réel en a besoin autant que la liste : une conversation qui
2873
+ * arrive par événement doit passer le même filtre, sans quoi elle se
2874
+ * réinvite dans un projet qui ne l'a jamais demandée.
2875
+ */
2876
+ async matcherForProject(ref) {
2877
+ await this.load();
2878
+ let projectRoot2 = null;
2931
2879
  try {
2932
- const res = await fetch(url.toString(), {
2933
- headers: { "Authorization": `Bearer ${passphrase}`, "Content-Type": "application/json" },
2934
- signal: AbortSignal.timeout(5e3)
2935
- });
2936
- if (!res.ok) {
2937
- return { success: false, error: `HTTP ${res.status} ${res.statusText}` };
2938
- }
2939
- const json = await res.json();
2940
- if (json.error) {
2941
- return { success: false, error: json.error.message ?? "Agent returned an error" };
2942
- }
2943
- return { success: true, uptime: json.result?.data?.uptime };
2944
- } catch (err) {
2945
- const msg = err instanceof Error ? err.message : "Unknown error";
2946
- return { success: false, error: msg };
2880
+ projectRoot2 = ref.orgId === void 0 ? rootOfProjectAmong(ref.id, null) : rootOfProjectRef({ id: ref.id, orgId: ref.orgId ?? null });
2881
+ } catch {
2882
+ projectRoot2 = null;
2947
2883
  }
2948
- }),
2949
- /** Register a new agent */
2950
- register: protectedProcedure.input(
2951
- z5.object({
2952
- name: z5.string().min(1).max(100),
2953
- url: z5.string().url().max(2048),
2954
- passphrase: z5.string().min(1).max(1024),
2955
- // À quelle organisation rattacher l'agent. Absent = la machine, ce que
2956
- // font l'agent local et tout client antérieur à ce champ.
2957
- orgId: z5.string().min(1).max(128).nullable().optional()
2958
- })
2959
- ).mutation(async ({ input }) => {
2960
- const config = await agentManager.register(
2961
- input.name,
2962
- input.url,
2963
- input.passphrase,
2964
- input.orgId ?? null
2965
- );
2966
- return { ...config, passphrase: "***" };
2967
- }),
2968
- /** Update an existing agent */
2969
- update: protectedProcedure.input(
2970
- z5.object({
2971
- id: z5.string().max(128),
2972
- name: z5.string().min(1).max(100).optional(),
2973
- url: z5.string().url().max(2048).optional(),
2974
- passphrase: z5.string().min(1).max(1024).optional(),
2975
- passthroughEnv: z5.array(
2976
- z5.string().regex(/^[A-Za-z_][A-Za-z0-9_]*$/).max(256)
2977
- ).max(100).optional()
2978
- })
2979
- ).mutation(async ({ input }) => {
2980
- try {
2981
- const { id, ...fields } = input;
2982
- const config = await agentManager.update(id, fields);
2983
- return { ...config, passphrase: "***" };
2984
- } catch (err) {
2985
- if (err instanceof Error) {
2986
- if (err.message.includes("not found")) {
2987
- throw new TRPCError5({ code: "NOT_FOUND", message: err.message });
2988
- }
2989
- if (err.message.includes("Cannot modify")) {
2990
- throw new TRPCError5({ code: "FORBIDDEN", message: err.message });
2991
- }
2992
- }
2993
- throw err;
2884
+ return (conv) => {
2885
+ if (conv.projectId) return conv.projectId === ref.id;
2886
+ if (!projectRoot2) return true;
2887
+ return this.roots.get(conv.id) === projectRoot2;
2888
+ };
2889
+ }
2890
+ async get(id) {
2891
+ await this.load();
2892
+ return this.conversations.get(id) ?? null;
2893
+ }
2894
+ async create(title, isCli = false, workflowId, workflowType, runner, kanbanBoardId, kanbanCardId, providerId, projectId, orgId) {
2895
+ await this.load();
2896
+ const now = Date.now();
2897
+ const conv = {
2898
+ id: randomUUID3(),
2899
+ projectId,
2900
+ title: title ?? "New conversation",
2901
+ messages: [],
2902
+ isCli,
2903
+ runner,
2904
+ providerId,
2905
+ workflowId,
2906
+ workflowType,
2907
+ kanbanBoardId,
2908
+ kanbanCardId,
2909
+ createdAt: now,
2910
+ updatedAt: now
2911
+ };
2912
+ this.conversations.set(conv.id, conv);
2913
+ if (projectId && orgId !== void 0) {
2914
+ this.roots.set(conv.id, vaultFor({ id: projectId, orgId }).root);
2994
2915
  }
2995
- }),
2996
- /** Get resolved passthrough env values from an agent */
2997
- getPassthroughEnv: protectedProcedure.input(z5.object({ id: z5.string().max(128) })).query(async ({ input }) => {
2998
- try {
2999
- return await agentManager.agentQuery(input.id, "config.getPassthroughEnv");
3000
- } catch {
3001
- return {};
2916
+ await this.save();
2917
+ this.events.emit("changed", { type: "created", conversation: this.snapshotConversation(conv) });
2918
+ return conv;
2919
+ }
2920
+ async update(id, patch, options) {
2921
+ await this.load();
2922
+ const conv = this.conversations.get(id);
2923
+ if (!conv) return null;
2924
+ if (patch.title !== void 0) conv.title = patch.title;
2925
+ if (patch.messages !== void 0) conv.messages = patch.messages;
2926
+ if (patch.contextUsage !== void 0) conv.contextUsage = patch.contextUsage;
2927
+ if (patch.workflowId !== void 0) conv.workflowId = patch.workflowId;
2928
+ if (patch.archived !== void 0) conv.archived = patch.archived;
2929
+ if (patch.codexThreadId !== void 0) conv.codexThreadId = patch.codexThreadId;
2930
+ if (options?.touch !== false) conv.updatedAt = Date.now();
2931
+ await this.save();
2932
+ if (!options?.silent) {
2933
+ this.events.emit("changed", { type: "updated", conversation: this.snapshotConversation(conv) });
3002
2934
  }
3003
- }),
3004
- /** Remove a registered agent */
3005
- remove: protectedProcedure.input(z5.object({ id: z5.string().max(128) })).mutation(async ({ input }) => {
3006
- try {
3007
- const removed = await agentManager.remove(input.id);
3008
- if (!removed) {
3009
- throw new TRPCError5({ code: "NOT_FOUND", message: `Agent ${input.id} not found` });
3010
- }
3011
- return { success: true };
3012
- } catch (err) {
3013
- if (err instanceof Error && err.message === "Cannot remove the local agent") {
3014
- throw new TRPCError5({ code: "FORBIDDEN", message: err.message });
3015
- }
3016
- throw err;
2935
+ return conv;
2936
+ }
2937
+ /**
2938
+ * Synchronously update the last assistant message content in memory, then schedule a save.
2939
+ * Used by activeSessionRegistry.persistNow() to avoid the async get() → .then() race.
2940
+ */
2941
+ updateLastAssistantContent(id, content, parts) {
2942
+ const conv = this.conversations.get(id);
2943
+ if (!conv) return;
2944
+ const lastMsg = conv.messages[conv.messages.length - 1];
2945
+ if (!lastMsg || lastMsg.role !== "assistant") return;
2946
+ lastMsg.content = content;
2947
+ if (parts !== void 0) lastMsg.parts = parts;
2948
+ conv.updatedAt = Date.now();
2949
+ this.save().catch(() => {
2950
+ });
2951
+ }
2952
+ async listByKanbanBoardId(boardId) {
2953
+ await this.load();
2954
+ return Array.from(this.conversations.values()).filter((c) => c.kanbanBoardId === boardId);
2955
+ }
2956
+ async listByKanbanCardId(cardId) {
2957
+ await this.load();
2958
+ return Array.from(this.conversations.values()).filter((c) => c.kanbanCardId === cardId);
2959
+ }
2960
+ async delete(id) {
2961
+ await this.load();
2962
+ const existed = this.conversations.delete(id);
2963
+ await this.save();
2964
+ if (existed) {
2965
+ this.events.emit("changed", { type: "deleted", id });
3017
2966
  }
3018
- }),
3019
- /** Subscribe to real-time agent status changes */
3020
- onStatus: protectedProcedure.subscription(async function* () {
3021
- for (const agent of agentManager.listAgents()) {
3022
- yield { agentId: agent.config.id, status: agent.status };
2967
+ }
2968
+ onChanged(handler) {
2969
+ this.events.on("changed", handler);
2970
+ }
2971
+ offChanged(handler) {
2972
+ this.events.off("changed", handler);
2973
+ }
2974
+ };
2975
+ var conversationStore = new ConversationStore();
2976
+
2977
+ // ../server/src/services/workspace-cwd.ts
2978
+ async function projectRoot(ref) {
2979
+ const project = await projectStore.get(ref);
2980
+ if (!project?.orgId) return null;
2981
+ return projectPathStore.get(project);
2982
+ }
2983
+ async function resolveWorkspaceCwd(target) {
2984
+ const launchDir = localVaultRootParent();
2985
+ if (target.serviceId) {
2986
+ const service = await serviceStore.get(target.serviceId);
2987
+ if (service) {
2988
+ const raw = (target.override ?? service.cwd ?? "").trim();
2989
+ const projects = await projectStore.list();
2990
+ const owner = projects.find((p) => p.serviceIds.includes(service.id));
2991
+ const root = owner ? await projectRoot(owner) : null;
2992
+ return root ? anchor(root, raw) : raw || ".";
3023
2993
  }
3024
- const queue = [];
3025
- let resolve7 = null;
3026
- const handler = (event) => {
3027
- if (queue.length >= SUBSCRIPTION_QUEUE_MAX) queue.shift();
3028
- queue.push(event);
3029
- if (resolve7) {
3030
- resolve7();
3031
- resolve7 = null;
3032
- }
3033
- };
3034
- agentManager.on("status", handler);
3035
- try {
3036
- while (true) {
3037
- if (queue.length > 0) {
3038
- yield queue.shift();
3039
- } else {
3040
- await new Promise((r) => {
3041
- resolve7 = r;
3042
- });
3043
- }
3044
- }
3045
- } finally {
3046
- agentManager.off("status", handler);
2994
+ }
2995
+ if (target.projectId) {
2996
+ const root = await projectRoot({ id: target.projectId, orgId: target.orgId });
2997
+ const raw = (target.override ?? "").trim();
2998
+ return root ? anchor(root, raw) : raw || launchDir;
2999
+ }
3000
+ return anchor(launchDir, (target.override ?? "").trim());
3001
+ }
3002
+ function anchor(base, raw) {
3003
+ if (!raw || raw === ".") return base;
3004
+ if (raw.startsWith("~") || raw.startsWith("$") || isAbsolute2(raw)) return raw;
3005
+ return join10(base, raw);
3006
+ }
3007
+ function localVaultRootParent() {
3008
+ return dirname5(localVaultRoot());
3009
+ }
3010
+ async function resolveAgentCwd(target) {
3011
+ if (target.serviceId) {
3012
+ const service = await serviceStore.get(target.serviceId);
3013
+ if (service) {
3014
+ const projects = await projectStore.list();
3015
+ const owner = projects.find((p) => p.serviceIds.includes(service.id));
3016
+ const root = owner ? await projectRoot(owner) : null;
3017
+ if (!root) return void 0;
3018
+ return anchor(root, (target.override ?? service.cwd ?? "").trim());
3047
3019
  }
3048
- })
3049
- });
3020
+ }
3021
+ if (target.projectId) {
3022
+ const root = await projectRoot({ id: target.projectId, orgId: target.orgId });
3023
+ if (!root) return void 0;
3024
+ return anchor(root, (target.override ?? "").trim());
3025
+ }
3026
+ return void 0;
3027
+ }
3028
+ async function resolveConversationCwd(conversationId) {
3029
+ const conv = await conversationStore.get(conversationId);
3030
+ const ref = conv ? await conversationStore.projectRefOf(conv) : null;
3031
+ if (!ref) return resolveAgentCwd({});
3032
+ return resolveAgentCwd({ projectId: ref.id, orgId: ref.orgId });
3033
+ }
3050
3034
 
3051
- // ../server/src/trpc/routers/process.ts
3052
- import { z as z6 } from "zod";
3053
- import { TRPCError as TRPCError6 } from "@trpc/server";
3054
- var SUBSCRIPTION_QUEUE_MAX2 = LOG_BUFFER_SIZE;
3055
- async function* forwardEvents(eventName, filter) {
3056
- const queue = [];
3057
- let resolve7 = null;
3058
- const handler = (data) => {
3059
- if (filter && !filter(data)) return;
3060
- if (queue.length >= SUBSCRIPTION_QUEUE_MAX2) queue.shift();
3061
- queue.push(data);
3062
- if (resolve7) {
3063
- resolve7();
3064
- resolve7 = null;
3035
+ // ../server/src/trpc/routers/service.ts
3036
+ async function resolveEffectiveAgentId(service) {
3037
+ const known = /* @__PURE__ */ new Set([
3038
+ ...agentManager.listAgents().map((a) => a.config.id),
3039
+ ...(await agentStore.list()).map((a) => a.id)
3040
+ ]);
3041
+ if (service.agentId && known.has(service.agentId)) return service.agentId;
3042
+ const projects = await projectStore.list();
3043
+ const project = projects.find((p) => p.serviceIds.includes(service.id));
3044
+ if (project?.activeEnvironmentId) {
3045
+ const activeEnv = await environmentStore.get(project.activeEnvironmentId);
3046
+ if (activeEnv?.agentId && known.has(activeEnv.agentId)) return activeEnv.agentId;
3047
+ }
3048
+ return agentManager.getDefaultAgentId();
3049
+ }
3050
+ async function resolveServiceVariables(service) {
3051
+ const projects = await projectStore.list();
3052
+ const project = projects.find((p) => p.serviceIds.includes(service.id));
3053
+ let resolvedService = service;
3054
+ const anchored = await resolveWorkspaceCwd({ serviceId: service.id });
3055
+ if (anchored !== (service.cwd ?? "").trim() && isAbsolute3(anchored)) {
3056
+ resolvedService = { ...resolvedService, resolvedCwd: anchored };
3057
+ }
3058
+ try {
3059
+ if (service.parserIds && service.parserIds.length > 0) {
3060
+ const settings = await settingsManager.getSettings();
3061
+ const allParsers = [...nativeParsers, ...settings.logParsers || []];
3062
+ const resolvedParsers = service.parserIds.map((id) => allParsers.find((p) => p.id === id)).filter((p) => p !== void 0);
3063
+ resolvedService = { ...resolvedService, resolvedLogParsers: resolvedParsers };
3065
3064
  }
3066
- };
3067
- agentManager.on(eventName, handler);
3065
+ } catch (err) {
3066
+ console.error("[service-router] Failed to resolve parsers for service", service.id + ":", err.message);
3067
+ }
3068
+ if (!project || !project.activeEnvironmentId) {
3069
+ return resolvedService;
3070
+ }
3068
3071
  try {
3069
- while (true) {
3070
- if (queue.length > 0) {
3071
- yield queue.shift();
3072
- } else {
3073
- await new Promise((r) => {
3074
- resolve7 = r;
3075
- });
3076
- }
3072
+ const resolved = await environmentResolver.resolveForService({
3073
+ serviceId: service.id,
3074
+ activeEnvironmentId: project.activeEnvironmentId
3075
+ });
3076
+ const serviceBuiltins = {
3077
+ HOST: env.HOST,
3078
+ HOST_LAN: "0.0.0.0"
3079
+ };
3080
+ const interpolatedEnv = {};
3081
+ for (const [key, value] of Object.entries(resolved.env)) {
3082
+ interpolatedEnv[key] = value.replace(
3083
+ /\{\{service\.([^}]+)\}\}/g,
3084
+ (match, k) => serviceBuiltins[k] !== void 0 ? serviceBuiltins[k] : match
3085
+ );
3077
3086
  }
3078
- } finally {
3079
- agentManager.off(eventName, handler);
3087
+ resolvedService = {
3088
+ ...resolvedService,
3089
+ env: interpolatedEnv,
3090
+ envSecrets: [...resolved.envSecrets]
3091
+ };
3092
+ } catch (err) {
3093
+ console.error("[service-router] Failed to resolve variables for service", service.id + ":", err.message);
3080
3094
  }
3095
+ return resolvedService;
3081
3096
  }
3082
- async function* forwardLogBatches(agentId, processId) {
3083
- const queue = [];
3084
- let resolve7 = null;
3085
- const handler = (data) => {
3086
- if (data.agentId !== agentId) return;
3087
- const batch = data.batch;
3088
- if (!Array.isArray(batch)) return;
3089
- const filtered = batch.filter((log) => log.processId === processId);
3090
- if (filtered.length === 0) return;
3091
- if (queue.length >= SUBSCRIPTION_QUEUE_MAX2) queue.shift();
3092
- queue.push(filtered);
3093
- if (resolve7) {
3094
- resolve7();
3095
- resolve7 = null;
3097
+ var DockerConfigInput = DockerConfigSchema;
3098
+ var serviceRouter = router({
3099
+ list: protectedProcedure.meta({ openapi: { method: "GET", path: "/services", tags: ["services"], summary: "List all services", protect: true } }).output(z4.any()).query(async ({ ctx }) => {
3100
+ const all = await serviceStore.list();
3101
+ const allowedIds = getAllowedServiceIds(ctx);
3102
+ const filtered = allowedIds ? all.filter((s) => allowedIds.has(s.id)) : all;
3103
+ return Promise.all(filtered.map(async (s) => ({
3104
+ ...s,
3105
+ effectiveAgentId: await resolveEffectiveAgentId(s)
3106
+ })));
3107
+ }),
3108
+ get: protectedProcedure.meta({ openapi: { method: "GET", path: "/services/{id}", tags: ["services"], summary: "Get a service by ID", protect: true } }).input(z4.object({ id: z4.string().max(128) })).output(z4.any()).query(async ({ input, ctx }) => {
3109
+ requireServiceAction(ctx, input.id, "service:read");
3110
+ const service = await serviceStore.get(input.id);
3111
+ if (!service) {
3112
+ throw new TRPCError4({ code: "NOT_FOUND", message: `Service ${input.id} not found` });
3096
3113
  }
3097
- };
3098
- agentManager.on("process:log", handler);
3099
- try {
3100
- while (true) {
3101
- if (queue.length > 0) {
3102
- yield queue.shift();
3103
- } else {
3104
- await new Promise((r) => {
3105
- resolve7 = r;
3114
+ return service;
3115
+ }),
3116
+ create: protectedProcedure.meta({ openapi: { method: "POST", path: "/services", tags: ["services"], summary: "Create a new service", protect: true } }).output(z4.any()).input(
3117
+ z4.object({
3118
+ agentId: z4.string().max(128).optional(),
3119
+ name: z4.string().min(1).max(200),
3120
+ commands: z4.array(ServiceCommandSchema).max(50).optional(),
3121
+ cwd: z4.string().max(4096).optional(),
3122
+ ports: z4.array(z4.number().int().min(1).max(65535)).max(20).optional(),
3123
+ groups: z4.array(z4.string().max(200)).max(20).optional(),
3124
+ description: z4.string().max(2e3).optional(),
3125
+ docsPath: z4.string().max(4096).optional(),
3126
+ url: z4.string().max(2048).optional(),
3127
+ localUrls: z4.array(z4.object({
3128
+ url: z4.string().max(253),
3129
+ port: z4.number().int().min(1).max(65535)
3130
+ })).max(20).optional(),
3131
+ openapiUrl: z4.string().max(2048).optional(),
3132
+ git: z4.object({ remote: z4.string().max(2048).optional(), home: z4.string().max(2048).optional() }).optional(),
3133
+ meta: z4.record(z4.string().max(256), z4.string().max(2e3)).optional().refine((obj) => !obj || Object.keys(obj).length <= 100, { message: "Too many meta keys (max 100)" }),
3134
+ runner: z4.enum(["native", "docker"]).optional().default("native"),
3135
+ dockerConfig: DockerConfigInput.optional(),
3136
+ healthCheck: HealthCheckSchema.optional(),
3137
+ autoRestart: z4.boolean().optional().default(false),
3138
+ restartStrategy: RestartStrategySchema.optional(),
3139
+ maxRestarts: z4.number().int().nonnegative().optional(),
3140
+ restartBackoffMs: z4.number().int().positive().optional().default(1e3),
3141
+ orgId: z4.string().max(128).nullable().optional(),
3142
+ shortcuts: z4.array(ServiceShortcutSchema).max(50).optional(),
3143
+ parserIds: z4.array(z4.string().max(128)).max(50).optional()
3144
+ })
3145
+ ).mutation(async ({ input }) => {
3146
+ if (input.agentId) {
3147
+ const status = agentManager.getStatus(input.agentId);
3148
+ if (!status) {
3149
+ throw new TRPCError4({
3150
+ code: "NOT_FOUND",
3151
+ message: `Agent ${input.agentId} not found`
3106
3152
  });
3107
3153
  }
3108
3154
  }
3109
- } finally {
3110
- agentManager.off("process:log", handler);
3111
- }
3112
- }
3113
- var processRouter = router({
3114
- /** List all processes across all connected agents */
3115
- list: protectedProcedure.meta({ openapi: { method: "GET", path: "/processes", tags: ["processes"], summary: "List all processes", protect: true } }).output(z6.any()).query(async () => {
3116
- const agents = agentManager.listAgents();
3117
- const results = [];
3118
- await Promise.allSettled(
3119
- agents.filter((a) => a.status.connected).map(async (a) => {
3155
+ if (input.runner === "docker") {
3156
+ throw new TRPCError4({ code: "BAD_REQUEST", message: 'Docker runner is not yet supported. Use "native" runner.' });
3157
+ }
3158
+ const service = await serviceStore.create({
3159
+ ...input,
3160
+ agentId: input.agentId
3161
+ });
3162
+ await auditService.log({ userId: "local", action: "create", entityType: "service", entityId: service.id, metadata: { name: input.name } });
3163
+ return service;
3164
+ }),
3165
+ update: protectedProcedure.meta({ openapi: { method: "PATCH", path: "/services/{id}", tags: ["services"], summary: "Update a service", protect: true } }).output(z4.any()).input(
3166
+ z4.object({
3167
+ id: z4.string().max(128),
3168
+ agentId: z4.string().max(128).nullable().optional(),
3169
+ // null = clear (inherit from env)
3170
+ name: z4.string().min(1).max(200).optional(),
3171
+ commands: z4.array(ServiceCommandSchema).max(50).optional(),
3172
+ cwd: z4.string().max(4096).nullable().optional(),
3173
+ ports: z4.array(z4.number().int().min(1).max(65535)).max(20).optional(),
3174
+ groups: z4.array(z4.string().max(200)).max(20).optional(),
3175
+ description: z4.string().max(2e3).optional(),
3176
+ docsPath: z4.string().max(4096).optional(),
3177
+ url: z4.string().max(2048).optional(),
3178
+ localUrls: z4.array(z4.object({
3179
+ url: z4.string().max(253),
3180
+ port: z4.number().int().min(1).max(65535)
3181
+ })).max(20).optional(),
3182
+ openapiUrl: z4.string().max(2048).optional(),
3183
+ git: z4.object({ remote: z4.string().max(2048).optional(), home: z4.string().max(2048).optional() }).nullable().optional(),
3184
+ meta: z4.record(z4.string().max(256), z4.string().max(2e3)).nullable().optional().refine((obj) => !obj || Object.keys(obj).length <= 100, { message: "Too many meta keys (max 100)" }),
3185
+ runner: z4.enum(["native", "docker"]).optional(),
3186
+ dockerConfig: DockerConfigInput.optional(),
3187
+ healthCheck: HealthCheckSchema.nullable().optional(),
3188
+ autoRestart: z4.boolean().optional(),
3189
+ clearLogsOnStart: z4.boolean().optional(),
3190
+ restartStrategy: RestartStrategySchema.optional(),
3191
+ maxRestarts: z4.number().int().nonnegative().optional(),
3192
+ restartBackoffMs: z4.number().int().positive().optional(),
3193
+ orgId: z4.string().max(128).nullable().optional(),
3194
+ shortcuts: z4.array(ServiceShortcutSchema).max(50).optional(),
3195
+ parserIds: z4.array(z4.string().max(128)).max(50).optional(),
3196
+ logSources: z4.array(LogSourceConfigSchema).max(20).optional()
3197
+ })
3198
+ ).mutation(async ({ input }) => {
3199
+ const existing = await serviceStore.get(input.id);
3200
+ if (!existing) {
3201
+ throw new TRPCError4({ code: "NOT_FOUND", message: `Service ${input.id} not found` });
3202
+ }
3203
+ let stoppedForAgentChange = false;
3204
+ if (input.agentId !== void 0 && input.agentId !== existing.agentId && existing.agentId) {
3205
+ const oldAgentStatus = agentManager.getStatus(existing.agentId);
3206
+ if (oldAgentStatus?.connected) {
3120
3207
  try {
3121
- const processes = await agentManager.agentQuery(a.config.id, "process.list");
3122
- for (const proc of processes) {
3123
- results.push({ ...proc, agentId: a.config.id });
3208
+ const processes = await agentManager.agentQuery(existing.agentId, "process.list");
3209
+ const proc = processes.find((p) => p.serviceId === input.id);
3210
+ if (proc && (proc.state === "running" || proc.state === "starting")) {
3211
+ await agentManager.agentMutation(existing.agentId, "process.stop", { id: input.id }, AGENT_STOP_FETCH_TIMEOUT);
3212
+ stoppedForAgentChange = true;
3124
3213
  }
3125
3214
  } catch {
3126
3215
  }
3127
- })
3128
- );
3129
- return results;
3130
- }),
3131
- /** Get a single process from a specific agent */
3132
- get: protectedProcedure.meta({ openapi: { method: "GET", path: "/processes/{agentId}/{id}", tags: ["processes"], summary: "Get a process by agent and ID", protect: true } }).input(z6.object({ agentId: z6.string().max(128), id: z6.string().max(128) })).output(z6.any()).query(async ({ input, ctx }) => {
3133
- requireServiceAction(ctx, input.id, "service:read");
3134
- const status = agentManager.getStatus(input.agentId);
3135
- if (!status) {
3136
- throw new TRPCError6({ code: "NOT_FOUND", message: `Agent ${input.agentId} not found` });
3137
- }
3138
- const proc = await agentManager.agentQuery(input.agentId, "process.get", { id: input.id });
3139
- return { ...proc, agentId: input.agentId };
3140
- }),
3141
- /** Start a process on an agent.
3142
- * Lazy deploy: the agent only learns about a service when it is first started
3143
- * (or restarted). This avoids pushing configs for every service at agent boot,
3144
- * which would be wasteful when many services are never started.
3145
- * The deploy call is idempotent — creates the process if missing, updates config if it exists. */
3146
- start: protectedProcedure.meta({ openapi: { method: "POST", path: "/processes/{agentId}/{processId}/start", tags: ["processes"], summary: "Start a process", protect: true } }).input(z6.object({ agentId: z6.string().max(128).optional(), processId: z6.string().max(128) })).output(z6.any()).mutation(async ({ input, ctx }) => {
3147
- requireServiceAction(ctx, input.processId, "service:start");
3148
- const service = await serviceStore.get(input.processId);
3149
- const effectiveAgentId = (service ? await resolveEffectiveAgentId(service) : null) ?? input.agentId ?? null;
3150
- if (!effectiveAgentId) {
3151
- throw new TRPCError6({ code: "BAD_REQUEST", message: "No agent assigned to this service. Set an agent on the service or on the active environment." });
3152
- }
3153
- if (service) {
3154
- try {
3155
- const resolved = await resolveServiceVariables(service);
3156
- await agentManager.agentMutation(effectiveAgentId, "process.deploy", resolved);
3157
- } catch (err) {
3158
- console.debug("[process-router] Failed to deploy before start for", input.processId + ":", err.message);
3159
3216
  }
3160
- }
3161
- if (service?.clearLogsOnStart) {
3162
- try {
3163
- await agentManager.agentMutation(effectiveAgentId, "process.clearLogBuffer", { id: input.processId });
3164
- agentManager.emit("process:log", { agentId: effectiveAgentId, batch: [{ processId: input.processId, __clearLogBuffer: true }] });
3165
- } catch (err) {
3166
- console.debug("[process-router] Failed to clear logs before start for", input.processId + ":", err.message);
3217
+ if (input.agentId) {
3218
+ const newAgentStatus = agentManager.getStatus(input.agentId);
3219
+ if (!newAgentStatus) {
3220
+ throw new TRPCError4({
3221
+ code: "NOT_FOUND",
3222
+ message: `Agent ${input.agentId} not found`
3223
+ });
3224
+ }
3167
3225
  }
3168
3226
  }
3169
- let result;
3170
- try {
3171
- result = await agentManager.agentMutation(effectiveAgentId, "process.start", { id: input.processId });
3172
- } catch (err) {
3173
- const err_ = err;
3174
- const isFetchError = err_?.cause?.code === "ECONNREFUSED" || err_?.message === "fetch failed" || err_?.name === "TypeError";
3175
- throw new TRPCError6({
3176
- code: "INTERNAL_SERVER_ERROR",
3177
- message: isFetchError ? `Cannot reach agent \u2014 make sure the agent is running` : err_?.message ?? "Failed to start process"
3178
- });
3179
- }
3180
- await auditService.log({ userId: "local", action: "start", entityType: "process", entityId: input.processId, metadata: { agentId: effectiveAgentId } });
3181
- return { ...result, agentId: effectiveAgentId };
3182
- }),
3183
- /** Stop a process on an agent */
3184
- stop: protectedProcedure.meta({ openapi: { method: "POST", path: "/processes/{agentId}/{processId}/stop", tags: ["processes"], summary: "Stop a process", protect: true } }).input(z6.object({ agentId: z6.string().max(128), processId: z6.string().max(128) })).output(z6.any()).mutation(async ({ input, ctx }) => {
3185
- requireServiceAction(ctx, input.processId, "service:stop");
3186
- const result = await agentManager.agentMutation(input.agentId, "process.stop", { id: input.processId }, AGENT_STOP_FETCH_TIMEOUT);
3187
- await auditService.log({ userId: "local", action: "stop", entityType: "process", entityId: input.processId, metadata: { agentId: input.agentId } });
3188
- return { ...result, agentId: input.agentId };
3189
- }),
3190
- /** Restart a process on an agent.
3191
- * Sends stop → deploy (latest resolved config) → start so that
3192
- * config/variable changes take effect. Same lazy deploy pattern as start. */
3193
- restart: protectedProcedure.meta({ openapi: { method: "POST", path: "/processes/{agentId}/{processId}/restart", tags: ["processes"], summary: "Restart a process", protect: true } }).input(z6.object({ agentId: z6.string().max(128).optional(), processId: z6.string().max(128) })).output(z6.any()).mutation(async ({ input, ctx }) => {
3194
- requireServiceAction(ctx, input.processId, "service:restart");
3195
- const service = await serviceStore.get(input.processId);
3196
- const effectiveAgentId = (service ? await resolveEffectiveAgentId(service) : null) ?? input.agentId ?? null;
3197
- if (!effectiveAgentId) {
3198
- throw new TRPCError6({ code: "BAD_REQUEST", message: "No agent assigned to this service. Set an agent on the service or on the active environment." });
3199
- }
3200
- try {
3201
- await agentManager.agentMutation(effectiveAgentId, "process.stop", { id: input.processId }, AGENT_STOP_FETCH_TIMEOUT);
3202
- } catch (err) {
3203
- console.debug("[process-router] Stop before restart ignored (process not found on agent?):", err.message);
3227
+ const service = await serviceStore.update(input);
3228
+ if (!service) {
3229
+ throw new TRPCError4({ code: "NOT_FOUND", message: `Service ${input.id} not found` });
3204
3230
  }
3205
- if (service) {
3206
- try {
3207
- const resolved = await resolveServiceVariables(service);
3208
- await agentManager.agentMutation(effectiveAgentId, "process.deploy", resolved);
3209
- } catch (err) {
3210
- console.debug("[process-router] Failed to deploy before restart for", input.processId + ":", err.message);
3211
- }
3231
+ if (input.name && input.name !== existing.name) {
3232
+ await environmentStore.renameByReference(input.id, input.name);
3212
3233
  }
3213
- if (service?.clearLogsOnStart) {
3214
- try {
3215
- await agentManager.agentMutation(effectiveAgentId, "process.clearLogBuffer", { id: input.processId });
3216
- agentManager.emit("process:log", { agentId: effectiveAgentId, batch: [{ processId: input.processId, __clearLogBuffer: true }] });
3217
- } catch (err) {
3218
- console.debug("[process-router] Failed to clear logs before restart for", input.processId + ":", err.message);
3234
+ if (service.agentId) {
3235
+ const agentStatus = agentManager.getStatus(service.agentId);
3236
+ if (agentStatus?.connected) {
3237
+ try {
3238
+ const resolvedService = await resolveServiceVariables(service);
3239
+ await agentManager.agentMutation(service.agentId, "process.updateConfig", {
3240
+ serviceId: service.id,
3241
+ config: resolvedService
3242
+ });
3243
+ } catch (err) {
3244
+ console.debug("[service-router] Failed to propagate config to agent", service.agentId + ":", err.message);
3245
+ }
3219
3246
  }
3220
3247
  }
3221
- const result = await agentManager.agentMutation(effectiveAgentId, "process.start", { id: input.processId });
3222
- await auditService.log({ userId: "local", action: "restart", entityType: "process", entityId: input.processId, metadata: { agentId: effectiveAgentId } });
3223
- return { ...result, agentId: effectiveAgentId };
3224
- }),
3225
- /** Get buffered logs for a process from an agent */
3226
- logs: protectedProcedure.meta({ openapi: { method: "GET", path: "/processes/{agentId}/{processId}/logs", tags: ["processes"], summary: "Get buffered logs for a process", protect: true } }).input(z6.object({ agentId: z6.string().max(128), processId: z6.string().max(128) })).output(z6.any()).query(async ({ input, ctx }) => {
3227
- requireServiceAction(ctx, input.processId, "logs:read");
3228
- return agentManager.agentQuery(input.agentId, "process.logs", { id: input.processId });
3248
+ await auditService.log({ userId: "local", action: "update", entityType: "service", entityId: input.id });
3249
+ return { ...service, effectiveAgentId: await resolveEffectiveAgentId(service), stoppedForAgentChange };
3229
3250
  }),
3230
- /** Clear buffered logs for a process on an agent */
3231
- clearLogBuffer: protectedProcedure.input(z6.object({ agentId: z6.string().max(128), processId: z6.string().max(128) })).mutation(async ({ input }) => {
3232
- await agentManager.agentMutation(input.agentId, "process.clearLogBuffer", { id: input.processId });
3233
- await auditService.log({ userId: "local", action: "clearLogBuffer", entityType: "process", entityId: input.processId, metadata: { agentId: input.agentId } });
3251
+ delete: protectedProcedure.meta({ openapi: { method: "DELETE", path: "/services/{id}", tags: ["services"], summary: "Delete a service", protect: true } }).input(z4.object({ id: z4.string().max(128) })).output(z4.object({ success: z4.literal(true) })).mutation(async ({ input }) => {
3252
+ const deleted = await serviceStore.delete(input.id);
3253
+ if (!deleted) {
3254
+ throw new TRPCError4({ code: "NOT_FOUND", message: `Service ${input.id} not found` });
3255
+ }
3256
+ await auditService.log({ userId: "local", action: "delete", entityType: "service", entityId: input.id });
3234
3257
  return { success: true };
3235
3258
  }),
3236
- /** Execute a shortcut command on an agent, streaming output into the process log */
3237
- executeShortcut: protectedProcedure.input(z6.object({
3238
- agentId: z6.string().max(128),
3239
- processId: z6.string().max(128),
3240
- shortcut: ServiceShortcutSchema
3241
- })).mutation(async ({ input }) => {
3242
- const service = await serviceStore.get(input.processId);
3243
- if (service) {
3244
- try {
3245
- const resolved = await resolveServiceVariables(service);
3246
- await agentManager.agentMutation(input.agentId, "process.deploy", resolved);
3247
- } catch (err) {
3248
- console.debug("[process-router] Failed to deploy before executeShortcut for", input.processId + ":", err.message);
3249
- }
3259
+ checkCwd: protectedProcedure.input(z4.object({ id: z4.string().max(128), checkReachability: z4.boolean().optional() })).query(async ({ input }) => {
3260
+ const service = await serviceStore.get(input.id);
3261
+ if (!service) {
3262
+ throw new TRPCError4({ code: "NOT_FOUND", message: `Service ${input.id} not found` });
3250
3263
  }
3251
- const result = await agentManager.agentMutation(
3252
- input.agentId,
3253
- "process.executeShortcut",
3254
- { id: input.processId, shortcut: input.shortcut },
3255
- AGENT_LONG_FETCH_TIMEOUT
3256
- );
3257
- await auditService.log({
3258
- userId: "local",
3259
- action: "executeShortcut",
3260
- entityType: "process",
3261
- entityId: input.processId,
3262
- metadata: { agentId: input.agentId, shortcutId: input.shortcut.id, shortcutLabel: input.shortcut.label }
3264
+ const effectiveAgentId = await resolveEffectiveAgentId(service);
3265
+ if (!effectiveAgentId) {
3266
+ throw new TRPCError4({ code: "BAD_REQUEST", message: "No agent configured for this service" });
3267
+ }
3268
+ const agentResult = await agentManager.agentQuery(effectiveAgentId, "git.checkCwd", {
3269
+ // Anchored on the project, so the check answers about the directory the
3270
+ // service will actually run in.
3271
+ cwd: await resolveWorkspaceCwd({ serviceId: service.id }),
3272
+ remote: service.git?.remote,
3273
+ checkReachability: input.checkReachability
3263
3274
  });
3264
- return result;
3265
- }),
3266
- /** Execute an ad-hoc command on an agent, streaming output into the process log */
3267
- executeAdHocCommand: protectedProcedure.input(z6.object({
3268
- agentId: z6.string().max(128),
3269
- processId: z6.string().max(128),
3270
- command: z6.string().min(1).max(4096),
3271
- cwd: z6.string().max(4096).optional(),
3272
- commandId: z6.string().max(128).optional()
3273
- })).mutation(async ({ input }) => {
3274
- const service = await serviceStore.get(input.processId);
3275
- if (service) {
3276
- try {
3277
- const resolved = await resolveServiceVariables(service);
3278
- await agentManager.agentMutation(input.agentId, "process.deploy", resolved);
3279
- } catch (err) {
3280
- console.debug("[process-router] Failed to deploy before executeAdHocCommand for", input.processId + ":", err.message);
3275
+ const { exists: exists4, actualRemote, unreachable, resolvedCwd, unresolvedVars } = agentResult;
3276
+ if (unresolvedVars.length > 0) {
3277
+ return { exists: false, canClone: false, unresolvedVars, resolvedCwd, gitMismatch: null };
3278
+ }
3279
+ const canClone = !!service.git?.remote && (!exists4 || actualRemote === null);
3280
+ let gitMismatch = null;
3281
+ if (exists4 && service.git?.remote && !canClone) {
3282
+ if (actualRemote === null) {
3283
+ gitMismatch = { actual: null, configured: service.git.remote };
3284
+ } else if (actualRemote !== service.git.remote) {
3285
+ gitMismatch = { actual: actualRemote, configured: service.git.remote };
3286
+ } else if (unreachable) {
3287
+ gitMismatch = { actual: actualRemote, configured: service.git.remote, unreachable: true };
3281
3288
  }
3282
3289
  }
3283
- const result = await agentManager.agentMutation(
3284
- input.agentId,
3285
- "process.executeAdHocCommand",
3286
- {
3287
- id: input.processId,
3288
- command: input.command,
3289
- // An explicit cwd wins; otherwise the service's directory, anchored on
3290
- // its project rather than on the server's launch directory.
3291
- cwd: input.cwd ?? await resolveAgentCwd({ serviceId: input.processId }),
3292
- commandId: input.commandId
3293
- },
3294
- AGENT_LONG_FETCH_TIMEOUT
3295
- );
3296
- await auditService.log({
3297
- userId: "local",
3298
- action: "executeAdHocCommand",
3299
- entityType: "process",
3300
- entityId: input.processId,
3301
- metadata: { agentId: input.agentId, command: input.command }
3302
- });
3303
- return result;
3290
+ return { exists: exists4, canClone, unresolvedVars: [], resolvedCwd, gitMismatch };
3304
3291
  }),
3305
- /** Cancel a running shortcut command on an agent */
3306
- cancelShortcut: protectedProcedure.input(z6.object({
3307
- agentId: z6.string().max(128),
3308
- processId: z6.string().max(128),
3309
- shortcutId: z6.string().max(128)
3310
- })).mutation(async ({ input }) => {
3311
- await agentManager.agentMutation(input.agentId, "process.cancelShortcut", { id: input.processId, shortcutId: input.shortcutId });
3292
+ cloneRepository: protectedProcedure.input(z4.object({ id: z4.string().max(128) })).mutation(async ({ input }) => {
3293
+ const service = await serviceStore.get(input.id);
3294
+ if (!service) {
3295
+ throw new TRPCError4({ code: "NOT_FOUND", message: `Service ${input.id} not found` });
3296
+ }
3297
+ if (!service.git?.remote) {
3298
+ throw new TRPCError4({ code: "BAD_REQUEST", message: "Service has no git remote configured" });
3299
+ }
3300
+ const effectiveAgentId = await resolveEffectiveAgentId(service);
3301
+ if (!effectiveAgentId) {
3302
+ throw new TRPCError4({ code: "BAD_REQUEST", message: "No agent configured for this service" });
3303
+ }
3304
+ await agentManager.agentMutation(effectiveAgentId, "git.clone", {
3305
+ remote: service.git.remote,
3306
+ // Clones WRITE: landing this in the launch directory instead of the
3307
+ // project's is how a repository ends up in the wrong place.
3308
+ cwd: await resolveWorkspaceCwd({ serviceId: service.id })
3309
+ }, 5 * 60 * 1e3);
3312
3310
  return { success: true };
3311
+ })
3312
+ });
3313
+
3314
+ // ../server/src/trpc/routers/agent.ts
3315
+ import { z as z5 } from "zod";
3316
+ import { TRPCError as TRPCError5 } from "@trpc/server";
3317
+ var SUBSCRIPTION_QUEUE_MAX = LOG_BUFFER_SIZE;
3318
+ var agentRouter = router({
3319
+ /** List all registered agents with their connection status */
3320
+ list: protectedProcedure.query(() => {
3321
+ return agentManager.listAgents();
3313
3322
  }),
3314
- /** Cancel a running ad-hoc command on an agent */
3315
- cancelAdHocCommand: protectedProcedure.input(z6.object({
3316
- agentId: z6.string().max(128),
3317
- processId: z6.string().max(128),
3318
- commandId: z6.string().max(128).optional()
3319
- })).mutation(async ({ input }) => {
3320
- await agentManager.agentMutation(input.agentId, "process.cancelAdHocCommand", { id: input.processId, commandId: input.commandId });
3321
- return { success: true };
3323
+ /** Get a single agent's config and status */
3324
+ get: protectedProcedure.input(z5.object({ id: z5.string().max(128) })).query(({ input }) => {
3325
+ const agent = agentManager.getAgent(input.id);
3326
+ if (!agent) {
3327
+ throw new TRPCError5({ code: "NOT_FOUND", message: `Agent ${input.id} not found` });
3328
+ }
3329
+ return {
3330
+ config: { ...agent.config, passphrase: "***" },
3331
+ status: agent.status
3332
+ };
3322
3333
  }),
3323
- /** Get current metrics for a process from an agent */
3324
- metrics: protectedProcedure.input(z6.object({ agentId: z6.string().max(128), processId: z6.string().max(128) })).query(async ({ input }) => {
3325
- return agentManager.agentQuery(input.agentId, "process.metrics", { id: input.processId });
3334
+ /** Test connectivity to an agent given url + passphrase */
3335
+ testConnection: protectedProcedure.input(
3336
+ z5.object({
3337
+ url: z5.string().url().max(2048),
3338
+ passphrase: z5.string().min(1).max(1024).optional(),
3339
+ agentId: z5.string().max(128).optional()
3340
+ })
3341
+ ).mutation(async ({ input }) => {
3342
+ let passphrase = input.passphrase;
3343
+ let targetUrl = input.url;
3344
+ if (!passphrase && input.agentId) {
3345
+ const agent = agentManager.getAgentConfig(input.agentId);
3346
+ if (!agent) {
3347
+ throw new TRPCError5({ code: "NOT_FOUND", message: "Agent not found" });
3348
+ }
3349
+ passphrase = agent.passphrase;
3350
+ targetUrl = agent.url;
3351
+ }
3352
+ if (!passphrase) {
3353
+ throw new TRPCError5({ code: "BAD_REQUEST", message: "Passphrase is required" });
3354
+ }
3355
+ const url = new URL("/api/trpc/health.check", targetUrl);
3356
+ try {
3357
+ const res = await fetch(url.toString(), {
3358
+ headers: { "Authorization": `Bearer ${passphrase}`, "Content-Type": "application/json" },
3359
+ signal: AbortSignal.timeout(5e3)
3360
+ });
3361
+ if (!res.ok) {
3362
+ return { success: false, error: `HTTP ${res.status} ${res.statusText}` };
3363
+ }
3364
+ const json = await res.json();
3365
+ if (json.error) {
3366
+ return { success: false, error: json.error.message ?? "Agent returned an error" };
3367
+ }
3368
+ return { success: true, uptime: json.result?.data?.uptime };
3369
+ } catch (err) {
3370
+ const msg = err instanceof Error ? err.message : "Unknown error";
3371
+ return { success: false, error: msg };
3372
+ }
3326
3373
  }),
3327
- /**
3328
- * Subscribe to real-time log stream for a process.
3329
- * Forwards batched events from the AgentWSBridge in real-time.
3330
- * Historical logs are fetched separately via process.logs query.
3331
- */
3332
- onLog: protectedProcedure.input(z6.object({ agentId: z6.string().max(128), processId: z6.string().max(128) })).subscription(async function* ({ input, ctx }) {
3333
- requireServiceAction(ctx, input.processId, "logs:read");
3334
- yield* forwardLogBatches(input.agentId, input.processId);
3374
+ /** Register a new agent */
3375
+ register: protectedProcedure.input(
3376
+ z5.object({
3377
+ name: z5.string().min(1).max(100),
3378
+ url: z5.string().url().max(2048),
3379
+ passphrase: z5.string().min(1).max(1024),
3380
+ // À quelle organisation rattacher l'agent. Absent = la machine, ce que
3381
+ // font l'agent local et tout client antérieur à ce champ.
3382
+ orgId: z5.string().min(1).max(128).nullable().optional()
3383
+ })
3384
+ ).mutation(async ({ input }) => {
3385
+ const config = await agentManager.register(
3386
+ input.name,
3387
+ input.url,
3388
+ input.passphrase,
3389
+ input.orgId ?? null
3390
+ );
3391
+ return { ...config, passphrase: "***" };
3335
3392
  }),
3336
- /**
3337
- * Subscribe to real-time status changes for processes.
3338
- * Input is optional — omit agentId/processId for global monitoring (dashboard).
3339
- * Forwards events from the AgentWSBridge in real-time.
3340
- */
3341
- onStatus: protectedProcedure.input(z6.object({
3342
- agentId: z6.string().max(128).optional(),
3343
- processId: z6.string().max(128).optional()
3344
- }).optional()).subscription(async function* ({ input, ctx }) {
3345
- const allowedIds = getAllowedServiceIds(ctx);
3346
- if (input?.processId) requireServiceAction(ctx, input.processId, "service:status");
3393
+ /** Update an existing agent */
3394
+ update: protectedProcedure.input(
3395
+ z5.object({
3396
+ id: z5.string().max(128),
3397
+ name: z5.string().min(1).max(100).optional(),
3398
+ url: z5.string().url().max(2048).optional(),
3399
+ passphrase: z5.string().min(1).max(1024).optional(),
3400
+ passthroughEnv: z5.array(
3401
+ z5.string().regex(/^[A-Za-z_][A-Za-z0-9_]*$/).max(256)
3402
+ ).max(100).optional()
3403
+ })
3404
+ ).mutation(async ({ input }) => {
3347
3405
  try {
3348
- const agents = agentManager.listAgents();
3349
- for (const agent of agents.filter((a) => a.status.connected)) {
3350
- if (input?.agentId && agent.config.id !== input.agentId) continue;
3351
- try {
3352
- const processes = await agentManager.agentQuery(agent.config.id, "process.list");
3353
- for (const proc of processes) {
3354
- if (input?.processId && proc.id !== input.processId) continue;
3355
- if (allowedIds && !allowedIds.has(proc.id)) continue;
3356
- yield { ...proc, agentId: agent.config.id };
3357
- }
3358
- } catch {
3406
+ const { id, ...fields } = input;
3407
+ const config = await agentManager.update(id, fields);
3408
+ return { ...config, passphrase: "***" };
3409
+ } catch (err) {
3410
+ if (err instanceof Error) {
3411
+ if (err.message.includes("not found")) {
3412
+ throw new TRPCError5({ code: "NOT_FOUND", message: err.message });
3413
+ }
3414
+ if (err.message.includes("Cannot modify")) {
3415
+ throw new TRPCError5({ code: "FORBIDDEN", message: err.message });
3359
3416
  }
3360
3417
  }
3361
- } catch {
3418
+ throw err;
3362
3419
  }
3363
- yield* forwardEvents("process:status", (d) => {
3364
- if (input?.agentId && d.agentId !== input.agentId) return false;
3365
- if (input?.processId && d.processId !== input.processId) return false;
3366
- if (allowedIds && !allowedIds.has(d.processId)) return false;
3367
- return true;
3368
- });
3369
- }),
3370
- /** Subscribe to real-time metrics for a process. */
3371
- onMetrics: protectedProcedure.input(z6.object({
3372
- agentId: z6.string().max(128).optional(),
3373
- processId: z6.string().max(128).optional()
3374
- }).optional()).subscription(async function* ({ input, ctx }) {
3375
- const allowedIds = getAllowedServiceIds(ctx);
3376
- if (input?.processId) requireServiceAction(ctx, input.processId, "service:status");
3377
- yield* forwardEvents("process:metrics", (d) => {
3378
- if (input?.agentId && d.agentId !== input.agentId) return false;
3379
- if (input?.processId && d.processId !== input.processId) return false;
3380
- if (allowedIds && !allowedIds.has(d.processId)) return false;
3381
- return true;
3382
- });
3383
3420
  }),
3384
- /** Subscribe to real-time health status changes. */
3385
- onHealth: protectedProcedure.input(z6.object({
3386
- agentId: z6.string().max(128).optional(),
3387
- processId: z6.string().max(128).optional()
3388
- }).optional()).subscription(async function* ({ input, ctx }) {
3389
- const allowedIds = getAllowedServiceIds(ctx);
3390
- if (input?.processId) requireServiceAction(ctx, input.processId, "service:healthcheck");
3391
- yield* forwardEvents("process:health", (d) => {
3392
- if (input?.agentId && d.agentId !== input.agentId) return false;
3393
- if (input?.processId && d.processId !== input.processId) return false;
3394
- if (allowedIds && !allowedIds.has(d.processId)) return false;
3395
- return true;
3396
- });
3397
- })
3398
- });
3399
-
3400
- // ../server/src/trpc/routers/audit.ts
3401
- import { z as z7 } from "zod";
3402
- var auditRouter = router({
3403
- list: protectedProcedure.input(z7.object({
3404
- action: z7.string().max(50).optional(),
3405
- entityType: z7.string().max(50).optional(),
3406
- entityId: z7.string().max(100).optional(),
3407
- limit: z7.number().int().positive().max(100).default(50),
3408
- offset: z7.number().int().nonnegative().default(0)
3409
- }).optional()).query(async ({ input }) => {
3410
- return auditService.list(input ?? {});
3411
- })
3412
- });
3413
-
3414
- // ../server/src/trpc/routers/setup.ts
3415
- import { z as z8 } from "zod";
3416
-
3417
- // ../server/src/services/cloud-session-store.ts
3418
- import { readFile as readFile8, writeFile as writeFile8, rename as rename8, mkdir as mkdir8, chmod as chmod7, unlink as unlink2 } from "fs/promises";
3419
- import { dirname as dirname5 } from "path";
3420
- import { randomBytes as randomBytes4, timingSafeEqual as timingSafeEqual2 } from "crypto";
3421
- var FILENAME4 = "cloud-session.json";
3422
- var STATE_TTL_MS = 5 * 60 * 1e3;
3423
- var CloudSessionStore = class {
3424
- session = null;
3425
- loaded = false;
3426
- /** Pending login states (CSRF binding between `start` and the callback). */
3427
- pending = /* @__PURE__ */ new Map();
3428
- /**
3429
- * Encrypted, so it lives beside the key that opens it — usually the machine
3430
- * root, but a launch directory owning its own key keeps both together.
3431
- */
3432
- getFilePath() {
3433
- return keyedFilePath(FILENAME4);
3434
- }
3435
- /** Where older versions kept it — read only, so an older Runeya keeps working. */
3436
- getLegacyFilePath() {
3437
- return legacyMachineFilePath(FILENAME4);
3438
- }
3439
- /**
3440
- * Read the machine file, falling back to the launch directory of old.
3441
- *
3442
- * The legacy file is left in place: the next save writes the new location,
3443
- * and a directory still opened by an older Runeya keeps working meanwhile.
3444
- */
3445
- async readFromDisk() {
3421
+ /** Get resolved passthrough env values from an agent */
3422
+ getPassthroughEnv: protectedProcedure.input(z5.object({ id: z5.string().max(128) })).query(async ({ input }) => {
3446
3423
  try {
3447
- return await readFile8(await this.getFilePath(), "utf-8");
3424
+ return await agentManager.agentQuery(input.id, "config.getPassthroughEnv");
3448
3425
  } catch {
3449
- return readFile8(this.getLegacyFilePath(), "utf-8");
3426
+ return {};
3450
3427
  }
3451
- }
3452
- async load() {
3453
- if (this.loaded) return;
3428
+ }),
3429
+ /** Remove a registered agent */
3430
+ remove: protectedProcedure.input(z5.object({ id: z5.string().max(128) })).mutation(async ({ input }) => {
3454
3431
  try {
3455
- const raw = await this.readFromDisk();
3456
- this.session = JSON.parse(raw);
3457
- } catch {
3432
+ const removed = await agentManager.remove(input.id);
3433
+ if (!removed) {
3434
+ throw new TRPCError5({ code: "NOT_FOUND", message: `Agent ${input.id} not found` });
3435
+ }
3436
+ return { success: true };
3437
+ } catch (err) {
3438
+ if (err instanceof Error && err.message === "Cannot remove the local agent") {
3439
+ throw new TRPCError5({ code: "FORBIDDEN", message: err.message });
3440
+ }
3441
+ throw err;
3458
3442
  }
3459
- this.loaded = true;
3460
- }
3461
- /**
3462
- * Mint a state value binding a login attempt to this server. Returned to the
3463
- * cloud page and echoed back on the callback, so a redirect we never
3464
- * initiated cannot install a token.
3465
- */
3466
- createState(returnTo) {
3467
- this.sweepStates();
3468
- const state = randomBytes4(32).toString("hex");
3469
- this.pending.set(state, { expiresAt: Date.now() + STATE_TTL_MS, returnTo });
3470
- return state;
3471
- }
3472
- /**
3473
- * Consume a state — single use, constant-time compare, TTL enforced. Returns
3474
- * the URL to send the browser back to, or null when the state is unknown or
3475
- * expired (i.e. a callback this server never initiated).
3476
- */
3477
- consumeState(candidate) {
3478
- this.sweepStates();
3479
- for (const [state, entry] of this.pending) {
3480
- const a = Buffer.from(state);
3481
- const b = Buffer.from(candidate);
3482
- if (a.length === b.length && timingSafeEqual2(a, b)) {
3483
- this.pending.delete(state);
3484
- return entry.expiresAt > Date.now() ? entry.returnTo : null;
3443
+ }),
3444
+ /** Subscribe to real-time agent status changes */
3445
+ onStatus: protectedProcedure.subscription(async function* () {
3446
+ for (const agent of agentManager.listAgents()) {
3447
+ yield { agentId: agent.config.id, status: agent.status };
3448
+ }
3449
+ const queue = [];
3450
+ let resolve7 = null;
3451
+ const handler = (event) => {
3452
+ if (queue.length >= SUBSCRIPTION_QUEUE_MAX) queue.shift();
3453
+ queue.push(event);
3454
+ if (resolve7) {
3455
+ resolve7();
3456
+ resolve7 = null;
3457
+ }
3458
+ };
3459
+ agentManager.on("status", handler);
3460
+ try {
3461
+ while (true) {
3462
+ if (queue.length > 0) {
3463
+ yield queue.shift();
3464
+ } else {
3465
+ await new Promise((r) => {
3466
+ resolve7 = r;
3467
+ });
3468
+ }
3469
+ }
3470
+ } finally {
3471
+ agentManager.off("status", handler);
3472
+ }
3473
+ })
3474
+ });
3475
+
3476
+ // ../server/src/trpc/routers/process.ts
3477
+ import { z as z6 } from "zod";
3478
+ import { TRPCError as TRPCError6 } from "@trpc/server";
3479
+ var SUBSCRIPTION_QUEUE_MAX2 = LOG_BUFFER_SIZE;
3480
+ async function* forwardEvents(eventName, filter) {
3481
+ const queue = [];
3482
+ let resolve7 = null;
3483
+ const handler = (data) => {
3484
+ if (filter && !filter(data)) return;
3485
+ if (queue.length >= SUBSCRIPTION_QUEUE_MAX2) queue.shift();
3486
+ queue.push(data);
3487
+ if (resolve7) {
3488
+ resolve7();
3489
+ resolve7 = null;
3490
+ }
3491
+ };
3492
+ agentManager.on(eventName, handler);
3493
+ try {
3494
+ while (true) {
3495
+ if (queue.length > 0) {
3496
+ yield queue.shift();
3497
+ } else {
3498
+ await new Promise((r) => {
3499
+ resolve7 = r;
3500
+ });
3485
3501
  }
3486
3502
  }
3487
- return null;
3503
+ } finally {
3504
+ agentManager.off(eventName, handler);
3488
3505
  }
3489
- sweepStates() {
3490
- const now = Date.now();
3491
- for (const [state, entry] of this.pending) {
3492
- if (entry.expiresAt <= now) this.pending.delete(state);
3506
+ }
3507
+ async function* forwardLogBatches(agentId, processId) {
3508
+ const queue = [];
3509
+ let resolve7 = null;
3510
+ const handler = (data) => {
3511
+ if (data.agentId !== agentId) return;
3512
+ const batch = data.batch;
3513
+ if (!Array.isArray(batch)) return;
3514
+ const filtered = batch.filter((log) => log.processId === processId);
3515
+ if (filtered.length === 0) return;
3516
+ if (queue.length >= SUBSCRIPTION_QUEUE_MAX2) queue.shift();
3517
+ queue.push(filtered);
3518
+ if (resolve7) {
3519
+ resolve7();
3520
+ resolve7 = null;
3493
3521
  }
3494
- }
3495
- async save() {
3496
- await mkdir8(dirname5(await this.getFilePath()), { recursive: true });
3497
- const filePath = await this.getFilePath();
3498
- const tmpPath = filePath + ".tmp";
3499
- await writeFile8(tmpPath, JSON.stringify(this.session, null, 2), "utf-8");
3500
- await rename8(tmpPath, filePath);
3501
- await chmod7(filePath, 384);
3502
- }
3503
- /** Store the session token obtained from the one-time-token exchange. */
3504
- async set(cloudUrl, token) {
3505
- await this.load();
3506
- this.session = { cloudUrl, encryptedToken: encryptValue(token) };
3507
- await this.save();
3508
- }
3509
- /** The decrypted session token, or null when not logged in. */
3510
- async getToken() {
3511
- await this.load();
3512
- if (!this.session) return null;
3513
- try {
3514
- return decryptValue(this.session.encryptedToken);
3515
- } catch {
3516
- return null;
3522
+ };
3523
+ agentManager.on("process:log", handler);
3524
+ try {
3525
+ while (true) {
3526
+ if (queue.length > 0) {
3527
+ yield queue.shift();
3528
+ } else {
3529
+ await new Promise((r) => {
3530
+ resolve7 = r;
3531
+ });
3532
+ }
3517
3533
  }
3534
+ } finally {
3535
+ agentManager.off("process:log", handler);
3518
3536
  }
3519
- /**
3520
- * The cloud this machine is signed into, or null when not linked.
3521
- *
3522
- * Needed to tell "an org I am not a member of" apart from "an org of another
3523
- * cloud instance entirely" — the ids of two clouds share no namespace, so
3524
- * comparing them without this would call every foreign-cloud project a
3525
- * permission problem.
3526
- */
3527
- async getCloudUrl() {
3528
- await this.load();
3529
- return this.session?.cloudUrl ?? null;
3530
- }
3531
- async isLinked() {
3532
- return await this.getToken() !== null;
3533
- }
3534
- /** Forget the session (sign-out). */
3535
- async clear() {
3536
- await this.load();
3537
- this.session = null;
3538
- this.pending.clear();
3537
+ }
3538
+ var processRouter = router({
3539
+ /** List all processes across all connected agents */
3540
+ list: protectedProcedure.meta({ openapi: { method: "GET", path: "/processes", tags: ["processes"], summary: "List all processes", protect: true } }).output(z6.any()).query(async () => {
3541
+ const agents = agentManager.listAgents();
3542
+ const results = [];
3543
+ await Promise.allSettled(
3544
+ agents.filter((a) => a.status.connected).map(async (a) => {
3545
+ try {
3546
+ const processes = await agentManager.agentQuery(a.config.id, "process.list");
3547
+ for (const proc of processes) {
3548
+ results.push({ ...proc, agentId: a.config.id });
3549
+ }
3550
+ } catch {
3551
+ }
3552
+ })
3553
+ );
3554
+ return results;
3555
+ }),
3556
+ /** Get a single process from a specific agent */
3557
+ get: protectedProcedure.meta({ openapi: { method: "GET", path: "/processes/{agentId}/{id}", tags: ["processes"], summary: "Get a process by agent and ID", protect: true } }).input(z6.object({ agentId: z6.string().max(128), id: z6.string().max(128) })).output(z6.any()).query(async ({ input, ctx }) => {
3558
+ requireServiceAction(ctx, input.id, "service:read");
3559
+ const status = agentManager.getStatus(input.agentId);
3560
+ if (!status) {
3561
+ throw new TRPCError6({ code: "NOT_FOUND", message: `Agent ${input.agentId} not found` });
3562
+ }
3563
+ const proc = await agentManager.agentQuery(input.agentId, "process.get", { id: input.id });
3564
+ return { ...proc, agentId: input.agentId };
3565
+ }),
3566
+ /** Start a process on an agent.
3567
+ * Lazy deploy: the agent only learns about a service when it is first started
3568
+ * (or restarted). This avoids pushing configs for every service at agent boot,
3569
+ * which would be wasteful when many services are never started.
3570
+ * The deploy call is idempotent — creates the process if missing, updates config if it exists. */
3571
+ start: protectedProcedure.meta({ openapi: { method: "POST", path: "/processes/{agentId}/{processId}/start", tags: ["processes"], summary: "Start a process", protect: true } }).input(z6.object({ agentId: z6.string().max(128).optional(), processId: z6.string().max(128) })).output(z6.any()).mutation(async ({ input, ctx }) => {
3572
+ requireServiceAction(ctx, input.processId, "service:start");
3573
+ const service = await serviceStore.get(input.processId);
3574
+ const effectiveAgentId = (service ? await resolveEffectiveAgentId(service) : null) ?? input.agentId ?? null;
3575
+ if (!effectiveAgentId) {
3576
+ throw new TRPCError6({ code: "BAD_REQUEST", message: "No agent assigned to this service. Set an agent on the service or on the active environment." });
3577
+ }
3578
+ if (service) {
3579
+ try {
3580
+ const resolved = await resolveServiceVariables(service);
3581
+ await agentManager.agentMutation(effectiveAgentId, "process.deploy", resolved);
3582
+ } catch (err) {
3583
+ console.debug("[process-router] Failed to deploy before start for", input.processId + ":", err.message);
3584
+ }
3585
+ }
3586
+ if (service?.clearLogsOnStart) {
3587
+ try {
3588
+ await agentManager.agentMutation(effectiveAgentId, "process.clearLogBuffer", { id: input.processId });
3589
+ agentManager.emit("process:log", { agentId: effectiveAgentId, batch: [{ processId: input.processId, __clearLogBuffer: true }] });
3590
+ } catch (err) {
3591
+ console.debug("[process-router] Failed to clear logs before start for", input.processId + ":", err.message);
3592
+ }
3593
+ }
3594
+ let result;
3539
3595
  try {
3540
- await unlink2(await this.getFilePath());
3541
- } catch {
3596
+ result = await agentManager.agentMutation(effectiveAgentId, "process.start", { id: input.processId });
3597
+ } catch (err) {
3598
+ const err_ = err;
3599
+ const isFetchError = err_?.cause?.code === "ECONNREFUSED" || err_?.message === "fetch failed" || err_?.name === "TypeError";
3600
+ throw new TRPCError6({
3601
+ code: "INTERNAL_SERVER_ERROR",
3602
+ message: isFetchError ? `Cannot reach agent \u2014 make sure the agent is running` : err_?.message ?? "Failed to start process"
3603
+ });
3604
+ }
3605
+ await auditService.log({ userId: "local", action: "start", entityType: "process", entityId: input.processId, metadata: { agentId: effectiveAgentId } });
3606
+ return { ...result, agentId: effectiveAgentId };
3607
+ }),
3608
+ /** Stop a process on an agent */
3609
+ stop: protectedProcedure.meta({ openapi: { method: "POST", path: "/processes/{agentId}/{processId}/stop", tags: ["processes"], summary: "Stop a process", protect: true } }).input(z6.object({ agentId: z6.string().max(128), processId: z6.string().max(128) })).output(z6.any()).mutation(async ({ input, ctx }) => {
3610
+ requireServiceAction(ctx, input.processId, "service:stop");
3611
+ const result = await agentManager.agentMutation(input.agentId, "process.stop", { id: input.processId }, AGENT_STOP_FETCH_TIMEOUT);
3612
+ await auditService.log({ userId: "local", action: "stop", entityType: "process", entityId: input.processId, metadata: { agentId: input.agentId } });
3613
+ return { ...result, agentId: input.agentId };
3614
+ }),
3615
+ /** Restart a process on an agent.
3616
+ * Sends stop → deploy (latest resolved config) → start so that
3617
+ * config/variable changes take effect. Same lazy deploy pattern as start. */
3618
+ restart: protectedProcedure.meta({ openapi: { method: "POST", path: "/processes/{agentId}/{processId}/restart", tags: ["processes"], summary: "Restart a process", protect: true } }).input(z6.object({ agentId: z6.string().max(128).optional(), processId: z6.string().max(128) })).output(z6.any()).mutation(async ({ input, ctx }) => {
3619
+ requireServiceAction(ctx, input.processId, "service:restart");
3620
+ const service = await serviceStore.get(input.processId);
3621
+ const effectiveAgentId = (service ? await resolveEffectiveAgentId(service) : null) ?? input.agentId ?? null;
3622
+ if (!effectiveAgentId) {
3623
+ throw new TRPCError6({ code: "BAD_REQUEST", message: "No agent assigned to this service. Set an agent on the service or on the active environment." });
3542
3624
  }
3543
- }
3544
- };
3545
- var cloudSessionStore = new CloudSessionStore();
3546
-
3547
- // ../server/src/services/cloud-api.ts
3548
- var DEFAULT_CLOUD_API_URL = "https://api.runeya.dev";
3549
- var DEFAULT_CLOUD_APP_URL = "https://runeya.dev";
3550
- function getCloudApiUrl() {
3551
- return (env.CLOUD_URL ?? DEFAULT_CLOUD_API_URL).replace(/\/+$/, "");
3552
- }
3553
- function getCloudAppUrl() {
3554
- return (env.CLOUD_APP_URL ?? DEFAULT_CLOUD_APP_URL).replace(/\/+$/, "");
3555
- }
3556
- var CloudNotLinkedError = class extends Error {
3557
- code = "CLOUD_NOT_LINKED";
3558
- constructor() {
3559
- super("Not linked to the Runeya cloud.");
3560
- this.name = "CloudNotLinkedError";
3561
- }
3562
- };
3563
- async function cloudFetch(path, init = {}) {
3564
- const token = await cloudSessionStore.getToken();
3565
- if (!token) throw new CloudNotLinkedError();
3566
- return fetch(`${getCloudApiUrl()}${path}`, {
3567
- method: init.method ?? "GET",
3568
- headers: {
3569
- Authorization: `Bearer ${token}`,
3570
- // Le cloud refuse (426) les apps trop anciennes pour son schéma courant.
3571
- [APP_VERSION_HEADER]: appVersion,
3572
- ...init.body !== void 0 ? { "Content-Type": "application/json" } : {}
3573
- },
3574
- ...init.body !== void 0 ? { body: JSON.stringify(init.body) } : {},
3575
- // 15s suits a JSON call; attachment downloads pass a longer budget.
3576
- signal: AbortSignal.timeout(init.timeoutMs ?? 15e3)
3577
- });
3578
- }
3579
-
3580
- // ../server/src/services/conversation-store.ts
3581
- import { randomUUID as randomUUID3 } from "crypto";
3582
- import { readFile as readFile9, writeFile as writeFile9, rename as rename9, mkdir as mkdir9, chmod as chmod8 } from "fs/promises";
3583
- import { dirname as dirname6, join as join10 } from "path";
3584
- import { EventEmitter as EventEmitter2 } from "events";
3585
- var FILENAME5 = "conversations.private.json";
3586
- async function localProjectId() {
3587
- const projects = await projectStore.list().catch(() => []);
3588
- return projects.find((p) => !p.orgId)?.id ?? null;
3589
- }
3590
- var ConversationStore = class {
3591
- conversations = /* @__PURE__ */ new Map();
3592
- /** Which vault each conversation was read from, until its project is known. */
3593
- roots = /* @__PURE__ */ new Map();
3594
- /**
3595
- * Les conversations qu'on n'a pas su déchiffrer, par coffre, sous leur forme
3596
- * de disque.
3597
- *
3598
- * Une entrée illisible était simplement ignorée. Tant qu'aucune de ses
3599
- * voisines ne se chargeait, le fichier n'était jamais réécrit et la donnée
3600
- * survivait par accident ; dès qu'une seule s'ouvrait, la sauvegarde suivante
3601
- * réécrivait le fichier avec elle seule — et effaçait les autres. Elles sont
3602
- * donc gardées ici, telles quelles, et réémises à l'identique.
3603
- */
3604
- unreadable = /* @__PURE__ */ new Map();
3605
- loaded = false;
3606
- events = new EventEmitter2().setMaxListeners(0);
3607
- saveQueue = Promise.resolve();
3608
- snapshotConversation(conv) {
3609
- return structuredClone(conv);
3610
- }
3611
- /**
3612
- * Encrypted, so it lives beside the key that opens it — usually the machine
3613
- * root, but a launch directory owning its own key keeps both together.
3614
- */
3615
- getFilePath() {
3616
- return keyedFilePath(FILENAME5);
3617
- }
3618
- /** Where older versions kept it — read only, so an older Runeya keeps working. */
3619
- getLegacyFilePath() {
3620
- return legacyMachineFilePath(FILENAME5);
3621
- }
3622
- /**
3623
- * Read the machine file, falling back to the launch directory of old.
3624
- *
3625
- * The legacy file is left in place: the next save writes the new location,
3626
- * and a directory still opened by an older Runeya keeps working meanwhile.
3627
- */
3628
- async readFromDisk() {
3629
3625
  try {
3630
- return await readFile9(await this.getFilePath(), "utf-8");
3631
- } catch {
3632
- return readFile9(this.getLegacyFilePath(), "utf-8");
3626
+ await agentManager.agentMutation(effectiveAgentId, "process.stop", { id: input.processId }, AGENT_STOP_FETCH_TIMEOUT);
3627
+ } catch (err) {
3628
+ console.debug("[process-router] Stop before restart ignored (process not found on agent?):", err.message);
3633
3629
  }
3634
- }
3635
- async ensureDir() {
3636
- await mkdir9(dirname6(await this.getFilePath()), { recursive: true });
3637
- }
3638
- encryptMessages(messages, vault) {
3639
- return encryptValue(JSON.stringify(messages), vault);
3640
- }
3641
- decryptMessages(encrypted, vault) {
3642
- const json = decryptValue(encrypted, vault);
3643
- return JSON.parse(json);
3644
- }
3630
+ if (service) {
3631
+ try {
3632
+ const resolved = await resolveServiceVariables(service);
3633
+ await agentManager.agentMutation(effectiveAgentId, "process.deploy", resolved);
3634
+ } catch (err) {
3635
+ console.debug("[process-router] Failed to deploy before restart for", input.processId + ":", err.message);
3636
+ }
3637
+ }
3638
+ if (service?.clearLogsOnStart) {
3639
+ try {
3640
+ await agentManager.agentMutation(effectiveAgentId, "process.clearLogBuffer", { id: input.processId });
3641
+ agentManager.emit("process:log", { agentId: effectiveAgentId, batch: [{ processId: input.processId, __clearLogBuffer: true }] });
3642
+ } catch (err) {
3643
+ console.debug("[process-router] Failed to clear logs before restart for", input.processId + ":", err.message);
3644
+ }
3645
+ }
3646
+ const result = await agentManager.agentMutation(effectiveAgentId, "process.start", { id: input.processId });
3647
+ await auditService.log({ userId: "local", action: "restart", entityType: "process", entityId: input.processId, metadata: { agentId: effectiveAgentId } });
3648
+ return { ...result, agentId: effectiveAgentId };
3649
+ }),
3650
+ /** Get buffered logs for a process from an agent */
3651
+ logs: protectedProcedure.meta({ openapi: { method: "GET", path: "/processes/{agentId}/{processId}/logs", tags: ["processes"], summary: "Get buffered logs for a process", protect: true } }).input(z6.object({ agentId: z6.string().max(128), processId: z6.string().max(128) })).output(z6.any()).query(async ({ input, ctx }) => {
3652
+ requireServiceAction(ctx, input.processId, "logs:read");
3653
+ return agentManager.agentQuery(input.agentId, "process.logs", { id: input.processId });
3654
+ }),
3655
+ /** Clear buffered logs for a process on an agent */
3656
+ clearLogBuffer: protectedProcedure.input(z6.object({ agentId: z6.string().max(128), processId: z6.string().max(128) })).mutation(async ({ input }) => {
3657
+ await agentManager.agentMutation(input.agentId, "process.clearLogBuffer", { id: input.processId });
3658
+ await auditService.log({ userId: "local", action: "clearLogBuffer", entityType: "process", entityId: input.processId, metadata: { agentId: input.agentId } });
3659
+ return { success: true };
3660
+ }),
3661
+ /** Execute a shortcut command on an agent, streaming output into the process log */
3662
+ executeShortcut: protectedProcedure.input(z6.object({
3663
+ agentId: z6.string().max(128),
3664
+ processId: z6.string().max(128),
3665
+ shortcut: ServiceShortcutSchema
3666
+ })).mutation(async ({ input }) => {
3667
+ const service = await serviceStore.get(input.processId);
3668
+ if (service) {
3669
+ try {
3670
+ const resolved = await resolveServiceVariables(service);
3671
+ await agentManager.agentMutation(input.agentId, "process.deploy", resolved);
3672
+ } catch (err) {
3673
+ console.debug("[process-router] Failed to deploy before executeShortcut for", input.processId + ":", err.message);
3674
+ }
3675
+ }
3676
+ const result = await agentManager.agentMutation(
3677
+ input.agentId,
3678
+ "process.executeShortcut",
3679
+ { id: input.processId, shortcut: input.shortcut },
3680
+ AGENT_LONG_FETCH_TIMEOUT
3681
+ );
3682
+ await auditService.log({
3683
+ userId: "local",
3684
+ action: "executeShortcut",
3685
+ entityType: "process",
3686
+ entityId: input.processId,
3687
+ metadata: { agentId: input.agentId, shortcutId: input.shortcut.id, shortcutLabel: input.shortcut.label }
3688
+ });
3689
+ return result;
3690
+ }),
3691
+ /** Execute an ad-hoc command on an agent, streaming output into the process log */
3692
+ executeAdHocCommand: protectedProcedure.input(z6.object({
3693
+ agentId: z6.string().max(128),
3694
+ processId: z6.string().max(128),
3695
+ command: z6.string().min(1).max(4096),
3696
+ cwd: z6.string().max(4096).optional(),
3697
+ commandId: z6.string().max(128).optional()
3698
+ })).mutation(async ({ input }) => {
3699
+ const service = await serviceStore.get(input.processId);
3700
+ if (service) {
3701
+ try {
3702
+ const resolved = await resolveServiceVariables(service);
3703
+ await agentManager.agentMutation(input.agentId, "process.deploy", resolved);
3704
+ } catch (err) {
3705
+ console.debug("[process-router] Failed to deploy before executeAdHocCommand for", input.processId + ":", err.message);
3706
+ }
3707
+ }
3708
+ const result = await agentManager.agentMutation(
3709
+ input.agentId,
3710
+ "process.executeAdHocCommand",
3711
+ {
3712
+ id: input.processId,
3713
+ command: input.command,
3714
+ // An explicit cwd wins; otherwise the service's directory, anchored on
3715
+ // its project rather than on the server's launch directory.
3716
+ cwd: input.cwd ?? await resolveAgentCwd({ serviceId: input.processId }),
3717
+ commandId: input.commandId
3718
+ },
3719
+ AGENT_LONG_FETCH_TIMEOUT
3720
+ );
3721
+ await auditService.log({
3722
+ userId: "local",
3723
+ action: "executeAdHocCommand",
3724
+ entityType: "process",
3725
+ entityId: input.processId,
3726
+ metadata: { agentId: input.agentId, command: input.command }
3727
+ });
3728
+ return result;
3729
+ }),
3730
+ /** Cancel a running shortcut command on an agent */
3731
+ cancelShortcut: protectedProcedure.input(z6.object({
3732
+ agentId: z6.string().max(128),
3733
+ processId: z6.string().max(128),
3734
+ shortcutId: z6.string().max(128)
3735
+ })).mutation(async ({ input }) => {
3736
+ await agentManager.agentMutation(input.agentId, "process.cancelShortcut", { id: input.processId, shortcutId: input.shortcutId });
3737
+ return { success: true };
3738
+ }),
3739
+ /** Cancel a running ad-hoc command on an agent */
3740
+ cancelAdHocCommand: protectedProcedure.input(z6.object({
3741
+ agentId: z6.string().max(128),
3742
+ processId: z6.string().max(128),
3743
+ commandId: z6.string().max(128).optional()
3744
+ })).mutation(async ({ input }) => {
3745
+ await agentManager.agentMutation(input.agentId, "process.cancelAdHocCommand", { id: input.processId, commandId: input.commandId });
3746
+ return { success: true };
3747
+ }),
3748
+ /** Get current metrics for a process from an agent */
3749
+ metrics: protectedProcedure.input(z6.object({ agentId: z6.string().max(128), processId: z6.string().max(128) })).query(async ({ input }) => {
3750
+ return agentManager.agentQuery(input.agentId, "process.metrics", { id: input.processId });
3751
+ }),
3645
3752
  /**
3646
- * Refuser d'écrire dans le coffre d'une organisation sans nommer sa clé.
3647
- *
3648
- * `getEncryptionKey(undefined)` rend la clé machine, et c'est le bon défaut
3649
- * pour le coffre local. Appliqué à un fichier rangé sous `orgs/<org>/`, il
3650
- * produit un chiffré que ce coffre n'emploie pas — lisible par personne à la
3651
- * relecture, et impossible à distinguer d'une corruption.
3652
- *
3653
- * Le cas a été observé plusieurs fois sans qu'on parvienne à nommer le
3654
- * chemin fautif : les garde-fous en place traitent le coffre fermé, le
3655
- * coffre mal résolu et la copie périmée, aucun ne couvrait « coffre non
3656
- * précisé ». Ces écritures-là n'ont jamais fonctionné — elles échouaient en
3657
- * silence, ce qui est pire. On les saute désormais en le disant, avec la
3658
- * pile d'appels qui manquait pour remonter au chemin fautif.
3753
+ * Subscribe to real-time log stream for a process.
3754
+ * Forwards batched events from the AgentWSBridge in real-time.
3755
+ * Historical logs are fetched separately via process.logs query.
3659
3756
  */
3660
- isVaultNamed(root, vault) {
3661
- const orgId = orgIdOfRoot(root);
3662
- if (!orgId || vault) return true;
3663
- console.error(
3664
- `[conversation-store] \xC9criture ignor\xE9e dans le coffre de l'organisation ${orgId} (${root}) : aucune cl\xE9 d'organisation nomm\xE9e, le contenu serait chiffr\xE9 par la cl\xE9 machine et illisible \xE0 la relecture.
3665
- ${new Error("trace").stack}`
3666
- );
3667
- return false;
3668
- }
3669
- toDisk(conv, vault) {
3670
- return {
3671
- id: conv.id,
3672
- projectId: conv.projectId,
3673
- title: conv.title,
3674
- encryptedMessages: this.encryptMessages(conv.messages, vault),
3675
- isCli: conv.isCli,
3676
- runner: conv.runner,
3677
- providerId: conv.providerId,
3678
- codexThreadId: conv.codexThreadId,
3679
- contextUsage: conv.contextUsage,
3680
- workflowId: conv.workflowId,
3681
- workflowType: conv.workflowType,
3682
- kanbanBoardId: conv.kanbanBoardId,
3683
- kanbanCardId: conv.kanbanCardId,
3684
- archived: conv.archived,
3685
- createdAt: conv.createdAt,
3686
- updatedAt: conv.updatedAt
3687
- };
3688
- }
3689
- fromDisk(disk, vault) {
3690
- const workflowType = (disk.workflowType === "scenario" ? "scenario" : void 0) ?? (disk.scenarioId ? "scenario" : void 0);
3691
- return {
3692
- id: disk.id,
3693
- projectId: disk.projectId,
3694
- title: disk.title,
3695
- messages: this.decryptMessages(disk.encryptedMessages, vault),
3696
- isCli: disk.isCli ?? disk.isClaudeCode ?? false,
3697
- runner: disk.runner,
3698
- providerId: disk.providerId,
3699
- codexThreadId: disk.codexThreadId,
3700
- contextUsage: disk.contextUsage,
3701
- workflowId: disk.workflowId ?? disk.scenarioId,
3702
- workflowType,
3703
- kanbanBoardId: disk.kanbanBoardId,
3704
- kanbanCardId: disk.kanbanCardId,
3705
- archived: disk.archived,
3706
- createdAt: disk.createdAt,
3707
- updatedAt: disk.updatedAt
3708
- };
3709
- }
3757
+ onLog: protectedProcedure.input(z6.object({ agentId: z6.string().max(128), processId: z6.string().max(128) })).subscription(async function* ({ input, ctx }) {
3758
+ requireServiceAction(ctx, input.processId, "logs:read");
3759
+ yield* forwardLogBatches(input.agentId, input.processId);
3760
+ }),
3710
3761
  /**
3711
- * Read every project vault, then the homes older versions used.
3712
- *
3713
- * A conversation found in a legacy file is attributed to the local project of
3714
- * that directory — which is what it was about, back when one directory meant
3715
- * one project. With no local project to point at, it is left unattributed
3716
- * rather than filed under a project picked at random.
3762
+ * Subscribe to real-time status changes for processes.
3763
+ * Input is optional — omit agentId/processId for global monitoring (dashboard).
3764
+ * Forwards events from the AgentWSBridge in real-time.
3717
3765
  */
3718
- async load() {
3719
- if (this.loaded) return;
3720
- if (!getEncryptionKey()) {
3721
- return;
3722
- }
3723
- this.loaded = true;
3724
- this.unreadable.clear();
3725
- await ensureProjectsLoaded();
3726
- const vaults = await allVaultRoots();
3727
- const legacyPaths = [this.getLegacyFilePath(), await keyedFilePath(FILENAME5)];
3728
- const sources = [
3729
- ...legacyPaths.map((path) => ({ path, legacy: true })),
3730
- ...vaults.map((root) => ({ path: join10(root, FILENAME5), legacy: false }))
3731
- ];
3732
- for (const { path, legacy } of sources) {
3733
- let data;
3734
- try {
3735
- data = JSON.parse(await readFile9(path, "utf-8"));
3736
- } catch {
3737
- continue;
3738
- }
3739
- const inherited = legacy ? await localProjectId() : null;
3740
- const root = dirname6(path);
3741
- await rememberFile(path);
3742
- for (const item of data) {
3766
+ onStatus: protectedProcedure.input(z6.object({
3767
+ agentId: z6.string().max(128).optional(),
3768
+ processId: z6.string().max(128).optional()
3769
+ }).optional()).subscription(async function* ({ input, ctx }) {
3770
+ const allowedIds = getAllowedServiceIds(ctx);
3771
+ if (input?.processId) requireServiceAction(ctx, input.processId, "service:status");
3772
+ try {
3773
+ const agents = agentManager.listAgents();
3774
+ for (const agent of agents.filter((a) => a.status.connected)) {
3775
+ if (input?.agentId && agent.config.id !== input.agentId) continue;
3743
3776
  try {
3744
- const conv = this.fromDisk(item, vaultIdOfRoot(root));
3745
- if (!conv.projectId && inherited) conv.projectId = inherited;
3746
- this.conversations.set(item.id, conv);
3747
- this.roots.set(item.id, root);
3777
+ const processes = await agentManager.agentQuery(agent.config.id, "process.list");
3778
+ for (const proc of processes) {
3779
+ if (input?.processId && proc.id !== input.processId) continue;
3780
+ if (allowedIds && !allowedIds.has(proc.id)) continue;
3781
+ yield { ...proc, agentId: agent.config.id };
3782
+ }
3748
3783
  } catch {
3749
- const bucket = this.unreadable.get(root) ?? [];
3750
- bucket.push(item);
3751
- this.unreadable.set(root, bucket);
3752
3784
  }
3753
3785
  }
3786
+ } catch {
3754
3787
  }
3755
- }
3788
+ yield* forwardEvents("process:status", (d) => {
3789
+ if (input?.agentId && d.agentId !== input.agentId) return false;
3790
+ if (input?.processId && d.processId !== input.processId) return false;
3791
+ if (allowedIds && !allowedIds.has(d.processId)) return false;
3792
+ return true;
3793
+ });
3794
+ }),
3795
+ /** Subscribe to real-time metrics for a process. */
3796
+ onMetrics: protectedProcedure.input(z6.object({
3797
+ agentId: z6.string().max(128).optional(),
3798
+ processId: z6.string().max(128).optional()
3799
+ }).optional()).subscription(async function* ({ input, ctx }) {
3800
+ const allowedIds = getAllowedServiceIds(ctx);
3801
+ if (input?.processId) requireServiceAction(ctx, input.processId, "service:status");
3802
+ yield* forwardEvents("process:metrics", (d) => {
3803
+ if (input?.agentId && d.agentId !== input.agentId) return false;
3804
+ if (input?.processId && d.processId !== input.processId) return false;
3805
+ if (allowedIds && !allowedIds.has(d.processId)) return false;
3806
+ return true;
3807
+ });
3808
+ }),
3809
+ /** Subscribe to real-time health status changes. */
3810
+ onHealth: protectedProcedure.input(z6.object({
3811
+ agentId: z6.string().max(128).optional(),
3812
+ processId: z6.string().max(128).optional()
3813
+ }).optional()).subscription(async function* ({ input, ctx }) {
3814
+ const allowedIds = getAllowedServiceIds(ctx);
3815
+ if (input?.processId) requireServiceAction(ctx, input.processId, "service:healthcheck");
3816
+ yield* forwardEvents("process:health", (d) => {
3817
+ if (input?.agentId && d.agentId !== input.agentId) return false;
3818
+ if (input?.processId && d.processId !== input.processId) return false;
3819
+ if (allowedIds && !allowedIds.has(d.processId)) return false;
3820
+ return true;
3821
+ });
3822
+ })
3823
+ });
3824
+
3825
+ // ../server/src/trpc/routers/audit.ts
3826
+ import { z as z7 } from "zod";
3827
+ var auditRouter = router({
3828
+ list: protectedProcedure.input(z7.object({
3829
+ action: z7.string().max(50).optional(),
3830
+ entityType: z7.string().max(50).optional(),
3831
+ entityId: z7.string().max(100).optional(),
3832
+ limit: z7.number().int().positive().max(100).default(50),
3833
+ offset: z7.number().int().nonnegative().default(0)
3834
+ }).optional()).query(async ({ input }) => {
3835
+ return auditService.list(input ?? {});
3836
+ })
3837
+ });
3838
+
3839
+ // ../server/src/trpc/routers/setup.ts
3840
+ import { z as z8 } from "zod";
3841
+
3842
+ // ../server/src/services/cloud-session-store.ts
3843
+ import { readFile as readFile9, writeFile as writeFile9, rename as rename9, mkdir as mkdir9, chmod as chmod8, unlink as unlink2 } from "fs/promises";
3844
+ import { dirname as dirname6 } from "path";
3845
+ import { randomBytes as randomBytes4, timingSafeEqual as timingSafeEqual2 } from "crypto";
3846
+ var FILENAME5 = "cloud-session.json";
3847
+ var STATE_TTL_MS = 5 * 60 * 1e3;
3848
+ var CloudSessionStore = class {
3849
+ session = null;
3850
+ loaded = false;
3851
+ /** Pending login states (CSRF binding between `start` and the callback). */
3852
+ pending = /* @__PURE__ */ new Map();
3756
3853
  /**
3757
- * Oublier ce qui a été lu, pour tout relire sous la clé courante.
3758
- *
3759
- * Appelé après l'adoption d'une clé d'organisation : le disque vient d'être
3760
- * re-chiffré sous les pieds du store, si bien que sa mémoire décrit un état
3761
- * qui n'existe plus. La laisser en place n'affichait pas seulement des
3762
- * comptes périmés — la sauvegarde suivante réécrivait l'ancien chiffré
3763
- * par-dessus le nouveau, et défaisait l'adoption.
3854
+ * Encrypted, so it lives beside the key that opens it usually the machine
3855
+ * root, but a launch directory owning its own key keeps both together.
3764
3856
  */
3765
- resetCache() {
3766
- this.conversations.clear();
3767
- this.roots.clear();
3768
- this.unreadable.clear();
3769
- this.loaded = false;
3770
- this.events.emit("changed", { type: "reloaded" });
3857
+ getFilePath() {
3858
+ return keyedFilePath(FILENAME5);
3771
3859
  }
3772
- /**
3773
- * Combien de conversations ce coffre garde sans savoir les lire, par
3774
- * organisation. La machine elle-même est rangée sous `null`.
3775
- */
3776
- async unreadableByVault() {
3777
- await this.load();
3778
- const out = /* @__PURE__ */ new Map();
3779
- for (const [root, items] of this.unreadable) {
3780
- const vault = vaultIdOfRoot(root) ?? null;
3781
- out.set(vault, (out.get(vault) ?? 0) + items.length);
3782
- }
3783
- return out;
3860
+ /** Where older versions kept it — read only, so an older Runeya keeps working. */
3861
+ getLegacyFilePath() {
3862
+ return legacyMachineFilePath(FILENAME5);
3784
3863
  }
3785
3864
  /**
3786
- * Write each vault that holds or held a conversation.
3865
+ * Read the machine file, falling back to the launch directory of old.
3787
3866
  *
3788
- * A vault emptied by this save is still written, as an empty list: a
3789
- * conversation that found its project must not survive in the file it was
3790
- * read from.
3867
+ * The legacy file is left in place: the next save writes the new location,
3868
+ * and a directory still opened by an older Runeya keeps working meanwhile.
3791
3869
  */
3792
- async saveInternal() {
3793
- if (!getEncryptionKey()) return;
3794
- const byRoot = /* @__PURE__ */ new Map();
3795
- for (const root of this.roots.values()) byRoot.set(root, []);
3796
- for (const conv of this.conversations.values()) {
3797
- const root = await this.rootFor(conv);
3798
- this.roots.set(conv.id, root);
3799
- const bucket = byRoot.get(root) ?? [];
3800
- const vault = vaultIdOfRoot(root);
3801
- if (!this.isVaultNamed(root, vault)) continue;
3802
- bucket.push(this.toDisk(conv, vault));
3803
- byRoot.set(root, bucket);
3804
- }
3805
- for (const [root, items] of this.unreadable) {
3806
- byRoot.set(root, [...items, ...byRoot.get(root) ?? []]);
3807
- }
3808
- const stale = /* @__PURE__ */ new Set();
3809
- for (const root of byRoot.keys()) {
3810
- const path = join10(root, FILENAME5);
3811
- if (wasRead(path) && await hasChangedSinceRead(path)) stale.add(root);
3812
- }
3813
- for (const [root, data] of byRoot) {
3814
- const filePath = join10(root, FILENAME5);
3815
- if (stale.has(root)) {
3816
- console.warn(
3817
- `[conversation-store] ${filePath} a chang\xE9 depuis sa lecture \u2014 \xE9criture ignor\xE9e pour ne pas \xE9craser une r\xE9paration faite en dehors du serveur.`
3818
- );
3819
- continue;
3820
- }
3821
- await mkdir9(root, { recursive: true });
3822
- const tmpPath = `${filePath}.tmp`;
3823
- await writeFile9(tmpPath, JSON.stringify(data, null, 2), "utf-8");
3824
- await rename9(tmpPath, filePath);
3825
- await chmod8(filePath, 384);
3826
- await rememberFile(filePath);
3870
+ async readFromDisk() {
3871
+ try {
3872
+ return await readFile9(await this.getFilePath(), "utf-8");
3873
+ } catch {
3874
+ return readFile9(this.getLegacyFilePath(), "utf-8");
3827
3875
  }
3828
3876
  }
3829
- /**
3830
- * The vault a conversation is written to: its project's.
3831
- *
3832
- * One that names no project stays where it was read, falling back to the
3833
- * key's own directory — never lost for want of an owner.
3834
- */
3835
- async rootFor(conv) {
3836
- const known = this.roots.get(conv.id) ?? null;
3837
- if (conv.projectId) return rootOfProjectAmong(conv.projectId, known);
3838
- return known ?? dirname6(await keyedFilePath(FILENAME5));
3839
- }
3840
- /**
3841
- * Le projet d'une conversation, organisation comprise.
3842
- *
3843
- * Ce que la conversation ne dit pas d'elle-même : elle ne nomme que l'id.
3844
- * Le coffre d'où elle vient fournit l'organisation, et c'est ce couple qu'il
3845
- * faut pour retrouver le répertoire de travail du projet.
3846
- */
3847
- async projectRefOf(conv) {
3848
- if (!conv.projectId) return null;
3849
- const known = this.roots.get(conv.id) ?? null;
3850
- const ref = refOfProjectAmong(conv.projectId, known);
3851
- return { id: ref.id, orgId: ref.orgId ?? null };
3877
+ async load() {
3878
+ if (this.loaded) return;
3879
+ try {
3880
+ const raw = await this.readFromDisk();
3881
+ this.session = JSON.parse(raw);
3882
+ } catch {
3883
+ }
3884
+ this.loaded = true;
3852
3885
  }
3853
3886
  /**
3854
- * Réécrire chaque coffre, en déplaçant ce qui appartient désormais ailleurs.
3855
- *
3856
- * Appelé quand un projet a changé de coffre : ses conversations sont écrites
3857
- * dans le nouveau, et le fichier d'où elles viennent est réécrit sans elles.
3887
+ * Mint a state value binding a login attempt to this server. Returned to the
3888
+ * cloud page and echoed back on the callback, so a redirect we never
3889
+ * initiated cannot install a token.
3858
3890
  */
3859
- async relocateAll() {
3860
- await this.load();
3861
- await this.save();
3862
- }
3863
- async save() {
3864
- const write = this.saveQueue.then(() => this.saveInternal());
3865
- this.saveQueue = write.catch(() => {
3866
- });
3867
- return write;
3868
- }
3869
- async list() {
3870
- await this.load();
3871
- return Array.from(this.conversations.values()).sort((a, b) => b.updatedAt - a.updatedAt);
3891
+ createState(returnTo) {
3892
+ this.sweepStates();
3893
+ const state = randomBytes4(32).toString("hex");
3894
+ this.pending.set(state, { expiresAt: Date.now() + STATE_TTL_MS, returnTo });
3895
+ return state;
3872
3896
  }
3873
3897
  /**
3874
- * Les conversations d'un projet.
3875
- *
3876
- * Celle qui ne nomme aucun projet écrite avant le champ, ou qu'une
3877
- * migration n'a pas su attribuer — n'est rendue que dans les projets de son
3878
- * propre coffre : elle reste atteignable là où elle a été écrite, sans se
3879
- * déverser dans toutes les organisations de la machine.
3898
+ * Consume a state — single use, constant-time compare, TTL enforced. Returns
3899
+ * the URL to send the browser back to, or null when the state is unknown or
3900
+ * expired (i.e. a callback this server never initiated).
3880
3901
  */
3881
- async listForProject(ref) {
3882
- const belongs = await this.matcherForProject(ref);
3883
- return (await this.list()).filter(belongs);
3902
+ consumeState(candidate) {
3903
+ this.sweepStates();
3904
+ for (const [state, entry] of this.pending) {
3905
+ const a = Buffer.from(state);
3906
+ const b = Buffer.from(candidate);
3907
+ if (a.length === b.length && timingSafeEqual2(a, b)) {
3908
+ this.pending.delete(state);
3909
+ return entry.expiresAt > Date.now() ? entry.returnTo : null;
3910
+ }
3911
+ }
3912
+ return null;
3884
3913
  }
3885
- /**
3886
- * Le test d'appartenance à un projet, préparé une fois.
3887
- *
3888
- * Le flux temps réel en a besoin autant que la liste : une conversation qui
3889
- * arrive par événement doit passer le même filtre, sans quoi elle se
3890
- * réinvite dans un projet qui ne l'a jamais demandée.
3891
- */
3892
- async matcherForProject(ref) {
3893
- await this.load();
3894
- let projectRoot2 = null;
3895
- try {
3896
- projectRoot2 = ref.orgId === void 0 ? rootOfProjectAmong(ref.id, null) : rootOfProjectRef({ id: ref.id, orgId: ref.orgId ?? null });
3897
- } catch {
3898
- projectRoot2 = null;
3914
+ sweepStates() {
3915
+ const now = Date.now();
3916
+ for (const [state, entry] of this.pending) {
3917
+ if (entry.expiresAt <= now) this.pending.delete(state);
3899
3918
  }
3900
- return (conv) => {
3901
- if (conv.projectId) return conv.projectId === ref.id;
3902
- if (!projectRoot2) return true;
3903
- return this.roots.get(conv.id) === projectRoot2;
3904
- };
3905
3919
  }
3906
- async get(id) {
3907
- await this.load();
3908
- return this.conversations.get(id) ?? null;
3920
+ async save() {
3921
+ await mkdir9(dirname6(await this.getFilePath()), { recursive: true });
3922
+ const filePath = await this.getFilePath();
3923
+ const tmpPath = filePath + ".tmp";
3924
+ await writeFile9(tmpPath, JSON.stringify(this.session, null, 2), "utf-8");
3925
+ await rename9(tmpPath, filePath);
3926
+ await chmod8(filePath, 384);
3909
3927
  }
3910
- async create(title, isCli = false, workflowId, workflowType, runner, kanbanBoardId, kanbanCardId, providerId, projectId, orgId) {
3928
+ /** Store the session token obtained from the one-time-token exchange. */
3929
+ async set(cloudUrl, token) {
3911
3930
  await this.load();
3912
- const now = Date.now();
3913
- const conv = {
3914
- id: randomUUID3(),
3915
- projectId,
3916
- title: title ?? "New conversation",
3917
- messages: [],
3918
- isCli,
3919
- runner,
3920
- providerId,
3921
- workflowId,
3922
- workflowType,
3923
- kanbanBoardId,
3924
- kanbanCardId,
3925
- createdAt: now,
3926
- updatedAt: now
3927
- };
3928
- this.conversations.set(conv.id, conv);
3929
- if (projectId && orgId !== void 0) {
3930
- this.roots.set(conv.id, vaultFor({ id: projectId, orgId }).root);
3931
- }
3931
+ this.session = { cloudUrl, encryptedToken: encryptValue(token) };
3932
3932
  await this.save();
3933
- this.events.emit("changed", { type: "created", conversation: this.snapshotConversation(conv) });
3934
- return conv;
3935
3933
  }
3936
- async update(id, patch, options) {
3934
+ /** The decrypted session token, or null when not logged in. */
3935
+ async getToken() {
3937
3936
  await this.load();
3938
- const conv = this.conversations.get(id);
3939
- if (!conv) return null;
3940
- if (patch.title !== void 0) conv.title = patch.title;
3941
- if (patch.messages !== void 0) conv.messages = patch.messages;
3942
- if (patch.contextUsage !== void 0) conv.contextUsage = patch.contextUsage;
3943
- if (patch.workflowId !== void 0) conv.workflowId = patch.workflowId;
3944
- if (patch.archived !== void 0) conv.archived = patch.archived;
3945
- if (patch.codexThreadId !== void 0) conv.codexThreadId = patch.codexThreadId;
3946
- if (options?.touch !== false) conv.updatedAt = Date.now();
3947
- await this.save();
3948
- if (!options?.silent) {
3949
- this.events.emit("changed", { type: "updated", conversation: this.snapshotConversation(conv) });
3937
+ if (!this.session) return null;
3938
+ try {
3939
+ return decryptValue(this.session.encryptedToken);
3940
+ } catch {
3941
+ return null;
3950
3942
  }
3951
- return conv;
3952
3943
  }
3953
3944
  /**
3954
- * Synchronously update the last assistant message content in memory, then schedule a save.
3955
- * Used by activeSessionRegistry.persistNow() to avoid the async get() → .then() race.
3945
+ * The cloud this machine is signed into, or null when not linked.
3946
+ *
3947
+ * Needed to tell "an org I am not a member of" apart from "an org of another
3948
+ * cloud instance entirely" — the ids of two clouds share no namespace, so
3949
+ * comparing them without this would call every foreign-cloud project a
3950
+ * permission problem.
3956
3951
  */
3957
- updateLastAssistantContent(id, content, parts) {
3958
- const conv = this.conversations.get(id);
3959
- if (!conv) return;
3960
- const lastMsg = conv.messages[conv.messages.length - 1];
3961
- if (!lastMsg || lastMsg.role !== "assistant") return;
3962
- lastMsg.content = content;
3963
- if (parts !== void 0) lastMsg.parts = parts;
3964
- conv.updatedAt = Date.now();
3965
- this.save().catch(() => {
3966
- });
3967
- }
3968
- async listByKanbanBoardId(boardId) {
3952
+ async getCloudUrl() {
3969
3953
  await this.load();
3970
- return Array.from(this.conversations.values()).filter((c) => c.kanbanBoardId === boardId);
3954
+ return this.session?.cloudUrl ?? null;
3971
3955
  }
3972
- async listByKanbanCardId(cardId) {
3973
- await this.load();
3974
- return Array.from(this.conversations.values()).filter((c) => c.kanbanCardId === cardId);
3956
+ async isLinked() {
3957
+ return await this.getToken() !== null;
3975
3958
  }
3976
- async delete(id) {
3959
+ /** Forget the session (sign-out). */
3960
+ async clear() {
3977
3961
  await this.load();
3978
- const existed = this.conversations.delete(id);
3979
- await this.save();
3980
- if (existed) {
3981
- this.events.emit("changed", { type: "deleted", id });
3962
+ this.session = null;
3963
+ this.pending.clear();
3964
+ try {
3965
+ await unlink2(await this.getFilePath());
3966
+ } catch {
3982
3967
  }
3983
3968
  }
3984
- onChanged(handler) {
3985
- this.events.on("changed", handler);
3986
- }
3987
- offChanged(handler) {
3988
- this.events.off("changed", handler);
3969
+ };
3970
+ var cloudSessionStore = new CloudSessionStore();
3971
+
3972
+ // ../server/src/services/cloud-api.ts
3973
+ var DEFAULT_CLOUD_API_URL = "https://api.runeya.dev";
3974
+ var DEFAULT_CLOUD_APP_URL = "https://runeya.dev";
3975
+ function getCloudApiUrl() {
3976
+ return (env.CLOUD_URL ?? DEFAULT_CLOUD_API_URL).replace(/\/+$/, "");
3977
+ }
3978
+ function getCloudAppUrl() {
3979
+ return (env.CLOUD_APP_URL ?? DEFAULT_CLOUD_APP_URL).replace(/\/+$/, "");
3980
+ }
3981
+ var CloudNotLinkedError = class extends Error {
3982
+ code = "CLOUD_NOT_LINKED";
3983
+ constructor() {
3984
+ super("Not linked to the Runeya cloud.");
3985
+ this.name = "CloudNotLinkedError";
3989
3986
  }
3990
3987
  };
3991
- var conversationStore = new ConversationStore();
3988
+ async function cloudFetch(path, init = {}) {
3989
+ const token = await cloudSessionStore.getToken();
3990
+ if (!token) throw new CloudNotLinkedError();
3991
+ return fetch(`${getCloudApiUrl()}${path}`, {
3992
+ method: init.method ?? "GET",
3993
+ headers: {
3994
+ Authorization: `Bearer ${token}`,
3995
+ // Le cloud refuse (426) les apps trop anciennes pour son schéma courant.
3996
+ [APP_VERSION_HEADER]: appVersion,
3997
+ ...init.body !== void 0 ? { "Content-Type": "application/json" } : {}
3998
+ },
3999
+ ...init.body !== void 0 ? { body: JSON.stringify(init.body) } : {},
4000
+ // 15s suits a JSON call; attachment downloads pass a longer budget.
4001
+ signal: AbortSignal.timeout(init.timeoutMs ?? 15e3)
4002
+ });
4003
+ }
3992
4004
 
3993
4005
  // ../server/src/trpc/routers/setup.ts
3994
4006
  var setupRouter = router({
@@ -4844,20 +4856,20 @@ var settingsRouter = router({
4844
4856
  });
4845
4857
  if (config?.wildcardDomain?.trim()) {
4846
4858
  const sanitisedDomain = config.wildcardDomain.replace(/^\*\./, "").replace(/[^a-z0-9.-]/gi, "");
4847
- const probe2 = `probe.${sanitisedDomain}`;
4859
+ const probe = `probe.${sanitisedDomain}`;
4848
4860
  try {
4849
- const addresses = await dns.resolve4(probe2);
4861
+ const addresses = await dns.resolve4(probe);
4850
4862
  const pointsToLocalhost = addresses.some((a) => a === "127.0.0.1");
4851
4863
  checks.push({
4852
4864
  id: "dns_resolution",
4853
4865
  ok: pointsToLocalhost,
4854
- message: pointsToLocalhost ? `${probe2} \u2192 127.0.0.1 \u2713` : `${probe2} r\xE9sout vers ${addresses.join(", ")} (attendu : 127.0.0.1)`
4866
+ message: pointsToLocalhost ? `${probe} \u2192 127.0.0.1 \u2713` : `${probe} r\xE9sout vers ${addresses.join(", ")} (attendu : 127.0.0.1)`
4855
4867
  });
4856
4868
  } catch {
4857
4869
  checks.push({
4858
4870
  id: "dns_resolution",
4859
4871
  ok: false,
4860
- message: `${probe2} ne r\xE9sout pas \u2014 v\xE9rifiez le DNS wildcard`
4872
+ message: `${probe} ne r\xE9sout pas \u2014 v\xE9rifiez le DNS wildcard`
4861
4873
  });
4862
4874
  }
4863
4875
  }
@@ -7951,7 +7963,7 @@ function tailTranscript(opts) {
7951
7963
 
7952
7964
  // ../server/src/services/ai-runners/claude-pty-runner.ts
7953
7965
  async function getAgentManager() {
7954
- const { agentManager: agentManager2 } = await import("./agent-manager-74WZPMVQ.js");
7966
+ const { agentManager: agentManager2 } = await import("./agent-manager-UUSQK6S2.js");
7955
7967
  return agentManager2;
7956
7968
  }
7957
7969
  function buildPtyArgs(params, sessionId, opts) {
@@ -8076,6 +8088,15 @@ async function runClaudeViaPty(params) {
8076
8088
  });
8077
8089
  } catch {
8078
8090
  }
8091
+ if (skipTokenInstructions && entry.type === "assistant" && entry.message?.stop_reason === "end_turn") {
8092
+ settle({
8093
+ output: state.assistantText,
8094
+ tokenStatus: "none",
8095
+ tokenMessage: "",
8096
+ actualSessionId: sessionId
8097
+ });
8098
+ return;
8099
+ }
8079
8100
  if (!skipTokenInstructions) {
8080
8101
  const { status, message, branchChoice } = parseEndToken(state.assistantText);
8081
8102
  if (status !== "none") {
@@ -8094,11 +8115,21 @@ async function runClaudeViaPty(params) {
8094
8115
  cli: "claude",
8095
8116
  args,
8096
8117
  env: params.injectedEnv && Object.keys(params.injectedEnv).length > 0 ? params.injectedEnv : void 0,
8118
+ // Le répertoire que l'appelant a résolu — celui du projet de la
8119
+ // conversation. Sans lui, le PTY naissait dans le répertoire de l'agent,
8120
+ // c'est-à-dire là où Runeya a été lancé : une carte kanban du projet A
8121
+ // faisait travailler le CLI dans le code de Runeya.
8122
+ cwd: params.cwd || void 0,
8097
8123
  cols: 120,
8098
8124
  rows: 30,
8099
8125
  sessionId,
8100
8126
  enableMcp: !!params.mcpConfig,
8101
- mcpApiBaseUrl: params.apiBaseUrl || void 0
8127
+ mcpApiBaseUrl: params.apiBaseUrl || void 0,
8128
+ // Sans lui, l'agent lance le CLI et son serveur MCP sans RUNEYA_API_TOKEN :
8129
+ // toutes les tools répondent « No auth token available », et `$RUNEYA_API_TOKEN`
8130
+ // ne résout à rien dans le shell du CLI. Le jeton est déjà là — la file le
8131
+ // signe elle-même, faute de requête HTTP d'où le tirer.
8132
+ mcpApiToken: params.apiToken || void 0
8102
8133
  }).then(({ spawnId: id }) => {
8103
8134
  spawnId = id;
8104
8135
  if (signal.aborted) {
@@ -8673,7 +8704,7 @@ async function shutdownCodexAppServer() {
8673
8704
  }
8674
8705
  }
8675
8706
  async function getAgentManager2() {
8676
- const { agentManager: agentManager2 } = await import("./agent-manager-74WZPMVQ.js");
8707
+ const { agentManager: agentManager2 } = await import("./agent-manager-UUSQK6S2.js");
8677
8708
  return agentManager2;
8678
8709
  }
8679
8710
  function parseRuneyaCurlCommand(command) {
@@ -9359,7 +9390,10 @@ async function* drainSession(session, signal, isReconnect) {
9359
9390
  }
9360
9391
  async function buildCliSystemContext(conv, jwt) {
9361
9392
  const allServices = await serviceStore.list();
9362
- const resolvedServices = await Promise.all(allServices.map(resolveServiceVariables));
9393
+ const ref = conv ? await conversationStore.projectRefOf(conv) : null;
9394
+ const project = ref ? await projectStore.get(ref) : null;
9395
+ const ownServices = project ? allServices.filter((svc) => project.serviceIds.includes(svc.id)) : allServices;
9396
+ const resolvedServices = await Promise.all(ownServices.map(resolveServiceVariables));
9363
9397
  let systemContext = generateAdminLlmsTxt(resolvedServices, jwt);
9364
9398
  if (conv?.kanbanBoardId) {
9365
9399
  const kanbanBoard = await kanbanStore.get(conv.kanbanBoardId);
@@ -9529,9 +9563,7 @@ var StoredMessageSchema = z16.object({
9529
9563
  parts: z16.array(MessagePartSchema).max(200).optional()
9530
9564
  });
9531
9565
  async function conversationCwd(conv) {
9532
- const ref = await conversationStore.projectRefOf(conv);
9533
- if (!ref) return resolveAgentCwd({});
9534
- return resolveAgentCwd({ projectId: ref.id, orgId: ref.orgId });
9566
+ return resolveConversationCwd(conv.id);
9535
9567
  }
9536
9568
  var chatRouter = router({
9537
9569
  conversations: router({
@@ -10219,9 +10251,13 @@ ${apiDocs}` : systemContext;
10219
10251
  cli,
10220
10252
  args,
10221
10253
  env: injectedEnv,
10222
- // An explicit cwd wins; otherwise the service's directory, then the
10223
- // project's never the directory the server was launched in.
10224
- cwd: input.cwd ?? await resolveAgentCwd({
10254
+ // An explicit cwd wins; then le projet de la conversation, s'il y en
10255
+ // a une : c'est elle que ce terminal poursuit, et son projet n'est
10256
+ // pas forcément celui ouvert dans l'onglet — ouvrir le terminal
10257
+ // d'une carte kanban depuis ailleurs lançait le CLI dans le
10258
+ // répertoire de lancement de Runeya. Sinon le service, puis le
10259
+ // projet actif — jamais le répertoire du serveur.
10260
+ cwd: input.cwd ?? (input.sessionId ? await resolveConversationCwd(input.sessionId) : void 0) ?? await resolveAgentCwd({
10225
10261
  serviceId: input.serviceId,
10226
10262
  projectId: input.projectId,
10227
10263
  orgId: input.orgId
@@ -11588,9 +11624,11 @@ async function runNodeCall(conversationId, node, systemContext, userMessage, run
11588
11624
  const result = await state.runner.run({
11589
11625
  agentId: runOverrides?.agentId ?? agentManager.getDefaultAgentId() ?? void 0,
11590
11626
  // Without this the CLI would run wherever the server was launched, whatever
11591
- // project the scenario belongs to. No project: left unset, so the runner
11592
- // keeps its own default rather than being sent to this machine's path.
11593
- cwd: await resolveAgentCwd({ projectId: runOverrides?.projectId, orgId: runOverrides?.orgId }),
11627
+ // project the scenario belongs to. Read from the conversation, so un lancement
11628
+ // sans requête HTTP la file kanban est ancré comme les autres. No project:
11629
+ // left unset, so the runner keeps its own default rather than being sent to
11630
+ // this machine's path.
11631
+ cwd: await resolveConversationCwd(conversationId),
11594
11632
  sessionId: state.sessionId,
11595
11633
  prompt: currentPrompt,
11596
11634
  isResume: state.isResume,
@@ -12468,7 +12506,6 @@ var scenarioRouter = router({
12468
12506
  runnerOverride: input.runnerOverride,
12469
12507
  modelOverride: input.modelOverride,
12470
12508
  agentId: input.agentId,
12471
- projectId: input.projectId,
12472
12509
  orgId: input.orgId
12473
12510
  }, !!input.resume);
12474
12511
  await new Promise((r) => setTimeout(r, 10));
@@ -12664,7 +12701,8 @@ var debugRouter = router({
12664
12701
  });
12665
12702
 
12666
12703
  // ../server/src/trpc/routers/diagnostics.ts
12667
- import { z as z21 } from "zod";
12704
+ import { z as z20 } from "zod";
12705
+ import { TRPCError as TRPCError18 } from "@trpc/server";
12668
12706
 
12669
12707
  // ../server/src/services/log-buffer.ts
12670
12708
  import { EventEmitter as EventEmitter4 } from "events";
@@ -12732,132 +12770,10 @@ function onEntry(listener) {
12732
12770
  return () => emitter.off("entry", listener);
12733
12771
  }
12734
12772
 
12735
- // ../server/src/services/instance-registry.ts
12736
- import { mkdir as mkdir16, readdir as readdir5, readFile as readFile15, writeFile as writeFile14, rm as rm3 } from "fs/promises";
12737
- import { join as join19 } from "path";
12738
- import { z as z20 } from "zod";
12739
- var InstanceFileSchema = z20.object({
12740
- pid: z20.number().int().positive(),
12741
- port: z20.number().int().min(1).max(65535),
12742
- agentPort: z20.number().int().min(1).max(65535),
12743
- cwd: z20.string().max(4096),
12744
- dataDir: z20.string().max(4096),
12745
- version: z20.string().max(64),
12746
- fromDesktop: z20.boolean(),
12747
- startedAt: z20.number().int().nonnegative()
12748
- });
12749
- function registryDir() {
12750
- return join19(machineRoot(), "instances");
12751
- }
12752
- function fileFor(pid) {
12753
- return join19(registryDir(), `${pid}.json`);
12754
- }
12755
- function selfRecord() {
12756
- return {
12757
- pid: process.pid,
12758
- port: env.PORT,
12759
- agentPort: env.RUNEYA_AGENT_PORT,
12760
- cwd: process.cwd(),
12761
- dataDir: env.DATA_DIR,
12762
- version: process.env["RUNEYA_VERSION"] ?? "unknown",
12763
- fromDesktop: env.FROM_DESKTOP,
12764
- startedAt: Date.now()
12765
- };
12766
- }
12767
- async function registerInstance() {
12768
- try {
12769
- await mkdir16(registryDir(), { recursive: true });
12770
- await writeFile14(fileFor(process.pid), JSON.stringify(selfRecord(), null, 2));
12771
- } catch (err) {
12772
- console.log("[instances] Could not register this instance:", err.message);
12773
- }
12774
- }
12775
- async function unregisterInstance() {
12776
- await rm3(fileFor(process.pid), { force: true }).catch(() => {
12777
- });
12778
- }
12779
- function isProcessAlive(pid) {
12780
- try {
12781
- process.kill(pid, 0);
12782
- return true;
12783
- } catch (err) {
12784
- return err.code === "EPERM";
12785
- }
12786
- }
12787
- async function probe(port) {
12788
- try {
12789
- const res = await fetch(`http://127.0.0.1:${port}/api/trpc/health.check`, {
12790
- signal: AbortSignal.timeout(1500)
12791
- });
12792
- if (!res.ok) return { ok: false, uptimeMs: null, version: null };
12793
- const body = await res.json();
12794
- const data = body.result?.data;
12795
- return { ok: true, uptimeMs: data?.uptime ?? null, version: data?.version ?? null };
12796
- } catch {
12797
- return { ok: false, uptimeMs: null, version: null };
12798
- }
12799
- }
12800
- async function listInstances() {
12801
- let files;
12802
- try {
12803
- files = await readdir5(registryDir());
12804
- } catch {
12805
- return [];
12806
- }
12807
- const found = await Promise.all(
12808
- files.filter((f) => f.endsWith(".json")).map(async (name) => {
12809
- const path = join19(registryDir(), name);
12810
- let record;
12811
- try {
12812
- const parsed = InstanceFileSchema.safeParse(JSON.parse(await readFile15(path, "utf8")));
12813
- if (!parsed.success) {
12814
- await rm3(path, { force: true }).catch(() => {
12815
- });
12816
- return null;
12817
- }
12818
- record = parsed.data;
12819
- } catch {
12820
- return null;
12821
- }
12822
- if (!isProcessAlive(record.pid)) {
12823
- await rm3(path, { force: true }).catch(() => {
12824
- });
12825
- return null;
12826
- }
12827
- const { ok, uptimeMs, version } = await probe(record.port);
12828
- if (!ok) {
12829
- await rm3(path, { force: true }).catch(() => {
12830
- });
12831
- return null;
12832
- }
12833
- return {
12834
- ...record,
12835
- // Trust the live answer over the file: the file was written at boot
12836
- // and an upgraded instance would keep claiming its old version.
12837
- version: version ?? record.version,
12838
- reachable: true,
12839
- current: record.pid === process.pid,
12840
- uptimeMs
12841
- };
12842
- })
12843
- );
12844
- return dedupeByPort(found.filter((i) => i !== null)).sort((a, b) => Number(b.current) - Number(a.current) || a.port - b.port);
12845
- }
12846
- function dedupeByPort(instances) {
12847
- const byPort = /* @__PURE__ */ new Map();
12848
- for (const instance of instances) {
12849
- const kept = byPort.get(instance.port);
12850
- if (!kept || instance.current || !kept.current && instance.startedAt > kept.startedAt) {
12851
- byPort.set(instance.port, instance);
12852
- }
12853
- }
12854
- return [...byPort.values()];
12855
- }
12856
-
12857
12773
  // ../server/src/trpc/routers/diagnostics.ts
12858
12774
  var diagnosticsRouter = router({
12859
12775
  /** Backlog for a client that just opened the panel. */
12860
- recentLogs: protectedProcedure.input(z21.object({ limit: z21.number().int().min(1).max(2e3).default(500) }).optional()).query(({ input }) => recentEntries(input?.limit ?? 500)),
12776
+ recentLogs: protectedProcedure.input(z20.object({ limit: z20.number().int().min(1).max(2e3).default(500) }).optional()).query(({ input }) => recentEntries(input?.limit ?? 500)),
12861
12777
  /**
12862
12778
  * Live log stream.
12863
12779
  *
@@ -12866,7 +12782,7 @@ var diagnosticsRouter = router({
12866
12782
  * silently loses lines, which is precisely what someone watching a boot
12867
12783
  * failure cannot afford.
12868
12784
  */
12869
- streamLogs: protectedProcedure.input(z21.object({ afterSeq: z21.number().int().nonnegative().default(0) }).optional()).subscription(async function* ({ input, signal }) {
12785
+ streamLogs: protectedProcedure.input(z20.object({ afterSeq: z20.number().int().nonnegative().default(0) }).optional()).subscription(async function* ({ input, signal }) {
12870
12786
  for (const entry of entriesSince(input?.afterSeq ?? 0)) yield entry;
12871
12787
  const queue = [];
12872
12788
  let wake = null;
@@ -12891,11 +12807,33 @@ var diagnosticsRouter = router({
12891
12807
  }
12892
12808
  }),
12893
12809
  /** Every Runeya answering on this machine, this one included. */
12894
- instances: protectedProcedure.query(() => listInstances())
12810
+ instances: protectedProcedure.query(() => listInstances()),
12811
+ /**
12812
+ * Stop another Runeya on this machine — its agent first, then its server.
12813
+ *
12814
+ * The pid is a *selector*, not an instruction: it is looked up in the list
12815
+ * `listInstances()` has just vouched for, and anything absent from it is
12816
+ * refused. That is what keeps this from being a way to signal arbitrary
12817
+ * processes with the server's privileges.
12818
+ */
12819
+ killInstance: protectedProcedure.input(z20.object({ pid: z20.number().int().positive() })).mutation(async ({ input }) => {
12820
+ try {
12821
+ return await killInstance(input.pid);
12822
+ } catch (err) {
12823
+ const reason = err.message;
12824
+ if (reason === "instance-not-found") {
12825
+ throw new TRPCError18({ code: "NOT_FOUND", message: "No live Runeya instance with this pid." });
12826
+ }
12827
+ if (reason === "instance-is-current") {
12828
+ throw new TRPCError18({ code: "BAD_REQUEST", message: "This instance cannot stop itself." });
12829
+ }
12830
+ throw new TRPCError18({ code: "INTERNAL_SERVER_ERROR", message: reason });
12831
+ }
12832
+ })
12895
12833
  });
12896
12834
 
12897
12835
  // ../server/src/trpc/routers/kanban-queue.ts
12898
- import { z as z22 } from "zod";
12836
+ import { z as z21 } from "zod";
12899
12837
 
12900
12838
  // ../server/src/services/kanban-queue.ts
12901
12839
  import { randomUUID as randomUUID10 } from "crypto";
@@ -13083,36 +13021,36 @@ var kanbanQueue = new KanbanQueueService();
13083
13021
  // ../server/src/trpc/routers/kanban-queue.ts
13084
13022
  var kanbanQueueRouter = router({
13085
13023
  getState: protectedProcedure.query(() => kanbanQueue.getState()),
13086
- add: protectedProcedure.input(z22.object({
13087
- boardId: z22.string().max(128),
13088
- cardId: z22.string().max(128),
13089
- runner: z22.enum(["claude-code", "codex"]).optional(),
13090
- model: z22.string().max(128).optional(),
13091
- scenarioId: z22.string().max(128).optional(),
13024
+ add: protectedProcedure.input(z21.object({
13025
+ boardId: z21.string().max(128),
13026
+ cardId: z21.string().max(128),
13027
+ runner: z21.enum(["claude-code", "codex"]).optional(),
13028
+ model: z21.string().max(128).optional(),
13029
+ scenarioId: z21.string().max(128).optional(),
13092
13030
  // Le projet ouvert quand la carte a été mise en file : la conversation
13093
13031
  // qui en naîtra lui appartient, plutôt que de rester orpheline et de
13094
13032
  // s'afficher dans tous les projets.
13095
- projectId: z22.string().max(128).optional(),
13096
- orgId: z22.string().max(128).nullable().optional()
13033
+ projectId: z21.string().max(128).optional(),
13034
+ orgId: z21.string().max(128).nullable().optional()
13097
13035
  })).mutation(({ input }) => kanbanQueue.add(input)),
13098
- addColumn: protectedProcedure.input(z22.object({
13099
- boardId: z22.string().max(128),
13100
- columnId: z22.string().max(128),
13101
- runner: z22.enum(["claude-code", "codex"]).optional(),
13102
- model: z22.string().max(128).optional(),
13103
- scenarioId: z22.string().max(128).optional(),
13104
- projectId: z22.string().max(128).optional(),
13105
- orgId: z22.string().max(128).nullable().optional()
13036
+ addColumn: protectedProcedure.input(z21.object({
13037
+ boardId: z21.string().max(128),
13038
+ columnId: z21.string().max(128),
13039
+ runner: z21.enum(["claude-code", "codex"]).optional(),
13040
+ model: z21.string().max(128).optional(),
13041
+ scenarioId: z21.string().max(128).optional(),
13042
+ projectId: z21.string().max(128).optional(),
13043
+ orgId: z21.string().max(128).nullable().optional()
13106
13044
  })).mutation(async ({ input }) => {
13107
13045
  const { boardId, columnId, ...options } = input;
13108
13046
  return kanbanQueue.addColumn(boardId, columnId, options);
13109
13047
  }),
13110
- remove: protectedProcedure.input(z22.object({ itemId: z22.string().max(128) })).mutation(({ input }) => ({ removed: kanbanQueue.remove(input.itemId) })),
13048
+ remove: protectedProcedure.input(z21.object({ itemId: z21.string().max(128) })).mutation(({ input }) => ({ removed: kanbanQueue.remove(input.itemId) })),
13111
13049
  stopAll: protectedProcedure.mutation(() => {
13112
13050
  kanbanQueue.stopAll();
13113
13051
  return { ok: true };
13114
13052
  }),
13115
- setMaxConcurrent: protectedProcedure.input(z22.object({ max: z22.number().int().min(1).max(20) })).mutation(({ input }) => {
13053
+ setMaxConcurrent: protectedProcedure.input(z21.object({ max: z21.number().int().min(1).max(20) })).mutation(({ input }) => {
13116
13054
  kanbanQueue.setMaxConcurrent(input.max);
13117
13055
  return kanbanQueue.getState();
13118
13056
  }),
@@ -13146,12 +13084,12 @@ var kanbanQueueRouter = router({
13146
13084
  });
13147
13085
 
13148
13086
  // ../server/src/trpc/routers/kanban.ts
13149
- import { mkdir as mkdir17, readdir as readdir6, rm as rm4, stat as stat5, writeFile as writeFile15 } from "fs/promises";
13087
+ import { mkdir as mkdir16, readdir as readdir5, rm as rm3, stat as stat5, writeFile as writeFile14 } from "fs/promises";
13150
13088
  import { createHash } from "crypto";
13151
13089
  import { tmpdir } from "os";
13152
- import { join as join20 } from "path";
13153
- import { z as z23 } from "zod";
13154
- import { TRPCError as TRPCError18 } from "@trpc/server";
13090
+ import { join as join19 } from "path";
13091
+ import { z as z22 } from "zod";
13092
+ import { TRPCError as TRPCError19 } from "@trpc/server";
13155
13093
  function summarizeCard(card, boardId, columnTitle) {
13156
13094
  return {
13157
13095
  boardId,
@@ -13200,37 +13138,37 @@ function safeDecode(value) {
13200
13138
  }
13201
13139
  var MAX_ATTACHMENT_BYTES = 55 * 1024 * 1024;
13202
13140
  function attachmentCacheDir() {
13203
- return join20(tmpdir(), "runeya-kanban-attachments");
13141
+ return join19(tmpdir(), "runeya-kanban-attachments");
13204
13142
  }
13205
13143
  var ATTACHMENT_CACHE_TTL_MS = 24 * 60 * 60 * 1e3;
13206
13144
  async function purgeStaleAttachments(dir, ttlMs = ATTACHMENT_CACHE_TTL_MS) {
13207
13145
  try {
13208
13146
  const cutoff = Date.now() - ttlMs;
13209
- for (const name of await readdir6(dir)) {
13210
- const path = join20(dir, name);
13147
+ for (const name of await readdir5(dir)) {
13148
+ const path = join19(dir, name);
13211
13149
  const info = await stat5(path).catch(() => null);
13212
- if (info && info.mtimeMs < cutoff) await rm4(path, { force: true });
13150
+ if (info && info.mtimeMs < cutoff) await rm3(path, { force: true });
13213
13151
  }
13214
13152
  } catch {
13215
13153
  }
13216
13154
  }
13217
13155
  function attachmentCacheDirs() {
13218
- return [attachmentCacheDir(), join20(machineRoot(), "kanban-attachments")];
13156
+ return [attachmentCacheDir(), join19(machineRoot(), "kanban-attachments")];
13219
13157
  }
13220
13158
  async function storeAttachment(id, name, body, dirs = attachmentCacheDirs()) {
13221
13159
  const refusals = [];
13222
13160
  for (const dir of dirs) {
13223
13161
  try {
13224
- await mkdir17(dir, { recursive: true });
13225
- const path = join20(dir, safeLocalName(id, name));
13226
- await writeFile15(path, body);
13162
+ await mkdir16(dir, { recursive: true });
13163
+ const path = join19(dir, safeLocalName(id, name));
13164
+ await writeFile14(path, body);
13227
13165
  void purgeStaleAttachments(dir);
13228
13166
  return path;
13229
13167
  } catch (err) {
13230
13168
  refusals.push(`${dir} (${err instanceof Error ? err.message : String(err)})`);
13231
13169
  }
13232
13170
  }
13233
- throw new TRPCError18({
13171
+ throw new TRPCError19({
13234
13172
  code: "INTERNAL_SERVER_ERROR",
13235
13173
  message: `Impossible d'\xE9crire la pi\xE8ce jointe sur cette machine. Emplacements essay\xE9s : ${refusals.join(" ; ")}. V\xE9rifiez les droits d'\xE9criture, ou l'espace disque.`
13236
13174
  });
@@ -13246,9 +13184,9 @@ var kanbanRouter = router({
13246
13184
  * card graph, so listing them in full would be megabytes to answer "which
13247
13185
  * boards exist?". Use `getBoard` for the cards of one.
13248
13186
  */
13249
- listBoards: protectedProcedure.input(z23.object({
13250
- includeArchived: z23.boolean().optional(),
13251
- organizationId: z23.string().max(128).optional()
13187
+ listBoards: protectedProcedure.input(z22.object({
13188
+ includeArchived: z22.boolean().optional(),
13189
+ organizationId: z22.string().max(128).optional()
13252
13190
  }).optional()).query(async ({ input }) => {
13253
13191
  const { organizationId, boards } = await kanbanStore.list(
13254
13192
  input?.includeArchived ?? false,
@@ -13274,7 +13212,7 @@ var kanbanRouter = router({
13274
13212
  };
13275
13213
  }),
13276
13214
  /** One board: its columns, and the cards in each. */
13277
- getBoard: protectedProcedure.input(z23.object({ boardId: z23.string().max(128) })).query(async ({ input }) => {
13215
+ getBoard: protectedProcedure.input(z22.object({ boardId: z22.string().max(128) })).query(async ({ input }) => {
13278
13216
  const board = await kanbanStore.get(input.boardId);
13279
13217
  if (!board) return { board: null };
13280
13218
  const { links } = await kanbanStore.cardLinks(input.boardId);
@@ -13313,11 +13251,11 @@ var kanbanRouter = router({
13313
13251
  * organization — or one board when `boardId` is given. Archived boards are
13314
13252
  * left out: they are what the team decided to stop looking at.
13315
13253
  */
13316
- searchCards: protectedProcedure.input(z23.object({
13317
- query: z23.string().min(1).max(256),
13318
- boardId: z23.string().max(128).optional(),
13319
- limit: z23.number().int().min(1).max(100).optional(),
13320
- organizationId: z23.string().max(128).optional()
13254
+ searchCards: protectedProcedure.input(z22.object({
13255
+ query: z22.string().min(1).max(256),
13256
+ boardId: z22.string().max(128).optional(),
13257
+ limit: z22.number().int().min(1).max(100).optional(),
13258
+ organizationId: z22.string().max(128).optional()
13321
13259
  })).query(async ({ input }) => {
13322
13260
  const needle = normalize(input.query);
13323
13261
  const ids = input.boardId ? [input.boardId] : (await kanbanStore.list(false, input.organizationId)).boards.map((b) => b.id);
@@ -13336,10 +13274,10 @@ var kanbanRouter = router({
13336
13274
  * qu'un `commentCount`, et rien n'en donnait le texte. Cette route est la
13337
13275
  * relecture, pour une seule carte : c'est le journal qui pèse, pas le reste.
13338
13276
  */
13339
- getCardComments: protectedProcedure.input(z23.object({
13340
- boardId: z23.string().max(128),
13341
- cardId: z23.string().max(128),
13342
- limit: z23.number().int().min(1).max(200).optional()
13277
+ getCardComments: protectedProcedure.input(z22.object({
13278
+ boardId: z22.string().max(128),
13279
+ cardId: z22.string().max(128),
13280
+ limit: z22.number().int().min(1).max(200).optional()
13343
13281
  })).query(async ({ input }) => {
13344
13282
  const history = await kanbanStore.cardHistory(input.boardId, input.cardId);
13345
13283
  const comments = history.filter((e) => e.type === "comment");
@@ -13368,18 +13306,18 @@ var kanbanRouter = router({
13368
13306
  * `getBoard` en donne déjà l'essentiel carte par carte ; cette route sert
13369
13307
  * quand c'est l'arbre lui-même qu'on veut, dates et auteurs compris.
13370
13308
  */
13371
- getCardLinks: protectedProcedure.input(z23.object({ boardId: z23.string().max(128) })).query(async ({ input }) => kanbanStore.cardLinks(input.boardId)),
13309
+ getCardLinks: protectedProcedure.input(z22.object({ boardId: z22.string().max(128) })).query(async ({ input }) => kanbanStore.cardLinks(input.boardId)),
13372
13310
  /**
13373
13311
  * Rattacher une carte à une carte parente, ou l'en détacher.
13374
13312
  *
13375
13313
  * Sans `parentCardId`, le lien est retiré. Le parent peut vivre sur un autre
13376
13314
  * tableau : `parentBoardId` le désigne alors, sinon c'est celui de la carte.
13377
13315
  */
13378
- setCardParent: protectedProcedure.input(z23.object({
13379
- boardId: z23.string().max(128),
13380
- cardId: z23.string().max(128),
13381
- parentCardId: z23.string().max(128).optional(),
13382
- parentBoardId: z23.string().max(128).optional()
13316
+ setCardParent: protectedProcedure.input(z22.object({
13317
+ boardId: z22.string().max(128),
13318
+ cardId: z22.string().max(128),
13319
+ parentCardId: z22.string().max(128).optional(),
13320
+ parentBoardId: z22.string().max(128).optional()
13383
13321
  })).mutation(async ({ input }) => {
13384
13322
  const ok = await kanbanStore.setCardParent(
13385
13323
  input.boardId,
@@ -13387,35 +13325,35 @@ var kanbanRouter = router({
13387
13325
  input.parentCardId ? { boardId: input.parentBoardId ?? input.boardId, cardId: input.parentCardId } : null
13388
13326
  );
13389
13327
  if (!ok) {
13390
- throw new TRPCError18({ code: "BAD_REQUEST", message: "The cloud refused the link (unknown card, or a cycle)." });
13328
+ throw new TRPCError19({ code: "BAD_REQUEST", message: "The cloud refused the link (unknown card, or a cycle)." });
13391
13329
  }
13392
13330
  return { ok: true };
13393
13331
  }),
13394
- moveCard: protectedProcedure.input(z23.object({
13395
- boardId: z23.string().max(128),
13396
- cardId: z23.string().max(128),
13397
- targetColumn: z23.string().max(256)
13332
+ moveCard: protectedProcedure.input(z22.object({
13333
+ boardId: z22.string().max(128),
13334
+ cardId: z22.string().max(128),
13335
+ targetColumn: z22.string().max(256)
13398
13336
  })).mutation(async ({ input }) => {
13399
13337
  await executeKanbanCommands(input.boardId, input.cardId, [
13400
13338
  { type: "move_card", columnTitle: input.targetColumn }
13401
13339
  ]);
13402
13340
  return { ok: true };
13403
13341
  }),
13404
- addComment: protectedProcedure.input(z23.object({
13405
- boardId: z23.string().max(128),
13406
- cardId: z23.string().max(128),
13407
- comment: z23.string().max(2e4)
13342
+ addComment: protectedProcedure.input(z22.object({
13343
+ boardId: z22.string().max(128),
13344
+ cardId: z22.string().max(128),
13345
+ comment: z22.string().max(2e4)
13408
13346
  })).mutation(async ({ input }) => {
13409
13347
  await executeKanbanCommands(input.boardId, input.cardId, [
13410
13348
  { type: "add_comment", comment: input.comment }
13411
13349
  ]);
13412
13350
  return { ok: true };
13413
13351
  }),
13414
- addCard: protectedProcedure.input(z23.object({
13415
- boardId: z23.string().max(128),
13416
- column: z23.string().max(256),
13417
- title: z23.string().max(512),
13418
- content: z23.string().max(2e4).optional()
13352
+ addCard: protectedProcedure.input(z22.object({
13353
+ boardId: z22.string().max(128),
13354
+ column: z22.string().max(256),
13355
+ title: z22.string().max(512),
13356
+ content: z22.string().max(2e4).optional()
13419
13357
  })).mutation(async ({ input }) => {
13420
13358
  await executeKanbanBoardCommands(input.boardId, [
13421
13359
  { type: "add_card", columnTitle: input.column, cardTitle: input.title, cardContent: input.content }
@@ -13427,28 +13365,28 @@ var kanbanRouter = router({
13427
13365
  * agent can open it with its own file tools. The bytes never go through the
13428
13366
  * prompt: the card context lists the attachments, this returns a path.
13429
13367
  */
13430
- downloadAttachment: protectedProcedure.input(z23.object({ attachmentId: z23.string().max(128) })).mutation(async ({ input }) => {
13368
+ downloadAttachment: protectedProcedure.input(z22.object({ attachmentId: z22.string().max(128) })).mutation(async ({ input }) => {
13431
13369
  const id = input.attachmentId;
13432
13370
  if (!ATTACHMENT_ID_REGEX.test(id)) {
13433
- throw new TRPCError18({ code: "BAD_REQUEST", message: "Invalid attachment id" });
13371
+ throw new TRPCError19({ code: "BAD_REQUEST", message: "Invalid attachment id" });
13434
13372
  }
13435
13373
  const res = await cloudFetch2(`/api/kanban/file/${id}`, { timeoutMs: 12e4 });
13436
13374
  if (res.status === 404) {
13437
- throw new TRPCError18({ code: "NOT_FOUND", message: `Attachment ${id} not found` });
13375
+ throw new TRPCError19({ code: "NOT_FOUND", message: `Attachment ${id} not found` });
13438
13376
  }
13439
13377
  if (!res.ok) {
13440
- throw new TRPCError18({
13378
+ throw new TRPCError19({
13441
13379
  code: res.status === 403 ? "FORBIDDEN" : "INTERNAL_SERVER_ERROR",
13442
13380
  message: `Cloud refused the attachment (HTTP ${res.status})`
13443
13381
  });
13444
13382
  }
13445
13383
  const declared = Number(res.headers.get("content-length") ?? 0);
13446
13384
  if (declared > MAX_ATTACHMENT_BYTES) {
13447
- throw new TRPCError18({ code: "PAYLOAD_TOO_LARGE", message: "Attachment too large to download" });
13385
+ throw new TRPCError19({ code: "PAYLOAD_TOO_LARGE", message: "Attachment too large to download" });
13448
13386
  }
13449
13387
  const body = Buffer.from(await res.arrayBuffer());
13450
13388
  if (body.length > MAX_ATTACHMENT_BYTES) {
13451
- throw new TRPCError18({ code: "PAYLOAD_TOO_LARGE", message: "Attachment too large to download" });
13389
+ throw new TRPCError19({ code: "PAYLOAD_TOO_LARGE", message: "Attachment too large to download" });
13452
13390
  }
13453
13391
  const contentType = res.headers.get("content-type") ?? "application/octet-stream";
13454
13392
  const disposition = res.headers.get("content-disposition") ?? "";
@@ -13460,20 +13398,21 @@ var kanbanRouter = router({
13460
13398
  });
13461
13399
 
13462
13400
  // ../server/src/trpc/routers/terminal.ts
13463
- import { z as z24 } from "zod";
13464
- import { TRPCError as TRPCError19 } from "@trpc/server";
13401
+ import { z as z23 } from "zod";
13402
+ import { TRPCError as TRPCError20 } from "@trpc/server";
13465
13403
  var terminalRouter = router({
13466
13404
  /** Create a PTY shell session on the target agent */
13467
- create: protectedProcedure.input(z24.object({
13468
- agentId: z24.string().max(128),
13469
- cwd: z24.string().max(512).optional(),
13405
+ create: protectedProcedure.input(z23.object({
13406
+ agentId: z23.string().max(128),
13407
+ cwd: z23.string().max(512).optional(),
13470
13408
  /** Anchors the terminal when no explicit cwd is given. */
13471
- serviceId: z24.string().max(128).optional(),
13472
- projectId: z24.string().max(128).optional(),
13409
+ serviceId: z23.string().max(128).optional(),
13410
+ projectId: z23.string().max(128).optional(),
13473
13411
  /** L'organisation du projet : un id seul peut en nommer un par organisation. */
13474
- orgId: z24.string().max(128).nullable().optional(),
13475
- cols: z24.number().int().min(1).max(512).optional(),
13476
- rows: z24.number().int().min(1).max(256).optional()
13412
+ orgId: z23.string().max(128).nullable().optional(),
13413
+ cols: z23.number().int().min(1).max(512).optional(),
13414
+ rows: z23.number().int().min(1).max(256).optional(),
13415
+ title: z23.string().max(128).optional()
13477
13416
  })).mutation(async ({ input }) => {
13478
13417
  const result = await agentManager.agentMutation(
13479
13418
  input.agentId,
@@ -13487,16 +13426,37 @@ var terminalRouter = router({
13487
13426
  orgId: input.orgId
13488
13427
  }),
13489
13428
  cols: input.cols,
13490
- rows: input.rows
13429
+ rows: input.rows,
13430
+ title: input.title
13491
13431
  }
13492
13432
  );
13493
13433
  return result;
13494
13434
  }),
13435
+ /** Les sessions PTY encore vivantes sur l'agent (onglets à retrouver) */
13436
+ list: protectedProcedure.input(z23.object({ agentId: z23.string().max(128) })).query(async ({ input }) => {
13437
+ return agentManager.agentQuery(
13438
+ input.agentId,
13439
+ "terminal.list"
13440
+ );
13441
+ }),
13442
+ /** Renommer un onglet */
13443
+ rename: protectedProcedure.input(z23.object({
13444
+ agentId: z23.string().max(128),
13445
+ sessionId: z23.string().max(128),
13446
+ title: z23.string().max(128)
13447
+ })).mutation(async ({ input }) => {
13448
+ await agentManager.agentMutation(
13449
+ input.agentId,
13450
+ "terminal.rename",
13451
+ { sessionId: input.sessionId, title: input.title }
13452
+ );
13453
+ return { ok: true };
13454
+ }),
13495
13455
  /** Send keystrokes / data to a PTY session */
13496
- write: protectedProcedure.input(z24.object({
13497
- agentId: z24.string().max(128),
13498
- sessionId: z24.string().max(128),
13499
- data: z24.string().max(65536)
13456
+ write: protectedProcedure.input(z23.object({
13457
+ agentId: z23.string().max(128),
13458
+ sessionId: z23.string().max(128),
13459
+ data: z23.string().max(65536)
13500
13460
  })).mutation(async ({ input }) => {
13501
13461
  await agentManager.agentMutation(
13502
13462
  input.agentId,
@@ -13506,11 +13466,11 @@ var terminalRouter = router({
13506
13466
  return { ok: true };
13507
13467
  }),
13508
13468
  /** Resize the PTY */
13509
- resize: protectedProcedure.input(z24.object({
13510
- agentId: z24.string().max(128),
13511
- sessionId: z24.string().max(128),
13512
- cols: z24.number().int().min(1).max(512),
13513
- rows: z24.number().int().min(1).max(256)
13469
+ resize: protectedProcedure.input(z23.object({
13470
+ agentId: z23.string().max(128),
13471
+ sessionId: z23.string().max(128),
13472
+ cols: z23.number().int().min(1).max(512),
13473
+ rows: z23.number().int().min(1).max(256)
13514
13474
  })).mutation(async ({ input }) => {
13515
13475
  await agentManager.agentMutation(
13516
13476
  input.agentId,
@@ -13520,9 +13480,9 @@ var terminalRouter = router({
13520
13480
  return { ok: true };
13521
13481
  }),
13522
13482
  /** Destroy a PTY session */
13523
- destroy: protectedProcedure.input(z24.object({
13524
- agentId: z24.string().max(128),
13525
- sessionId: z24.string().max(128)
13483
+ destroy: protectedProcedure.input(z23.object({
13484
+ agentId: z23.string().max(128),
13485
+ sessionId: z23.string().max(128)
13526
13486
  })).mutation(async ({ input }) => {
13527
13487
  await agentManager.agentMutation(
13528
13488
  input.agentId,
@@ -13532,9 +13492,11 @@ var terminalRouter = router({
13532
13492
  return { ok: true };
13533
13493
  }),
13534
13494
  /** Stream PTY output — proxied via AgentWSBridge dynamic subscription */
13535
- onData: protectedProcedure.input(z24.object({
13536
- agentId: z24.string().max(128),
13537
- sessionId: z24.string().max(128)
13495
+ onData: protectedProcedure.input(z23.object({
13496
+ agentId: z23.string().max(128),
13497
+ sessionId: z23.string().max(128),
13498
+ /** Rejouer l'historique de la session avant le flux temps réel. */
13499
+ replay: z23.boolean().optional()
13538
13500
  })).subscription(async function* ({ input }) {
13539
13501
  const queue = [];
13540
13502
  let resolve7 = null;
@@ -13544,7 +13506,7 @@ var terminalRouter = router({
13544
13506
  unsubscribe = agentManager.agentSubscribe(
13545
13507
  input.agentId,
13546
13508
  "terminal.onData",
13547
- { sessionId: input.sessionId },
13509
+ { sessionId: input.sessionId, replay: input.replay },
13548
13510
  (data) => {
13549
13511
  if (typeof data === "string") queue.push(data);
13550
13512
  if (resolve7) {
@@ -13561,7 +13523,7 @@ var terminalRouter = router({
13561
13523
  }
13562
13524
  );
13563
13525
  } catch (err) {
13564
- throw new TRPCError19({
13526
+ throw new TRPCError20({
13565
13527
  code: "BAD_REQUEST",
13566
13528
  message: `Cannot subscribe to terminal: ${err.message}`
13567
13529
  });
@@ -13583,11 +13545,11 @@ var terminalRouter = router({
13583
13545
  });
13584
13546
 
13585
13547
  // ../server/src/trpc/routers/utility.ts
13586
- import { z as z25 } from "zod";
13548
+ import { z as z24 } from "zod";
13587
13549
  var MAX_WAIT_MS = 5 * 60 * 1e3;
13588
13550
  var utilityRouter = router({
13589
- wait: protectedProcedure.input(z25.object({
13590
- ms: z25.number().int().min(0).max(MAX_WAIT_MS)
13551
+ wait: protectedProcedure.input(z24.object({
13552
+ ms: z24.number().int().min(0).max(MAX_WAIT_MS)
13591
13553
  })).mutation(async ({ input }) => {
13592
13554
  await new Promise((resolve7) => setTimeout(resolve7, input.ms));
13593
13555
  return { waited: input.ms };
@@ -13595,15 +13557,15 @@ var utilityRouter = router({
13595
13557
  });
13596
13558
 
13597
13559
  // ../server/src/trpc/routers/wiki.ts
13598
- import { z as z26 } from "zod";
13599
- import { TRPCError as TRPCError20 } from "@trpc/server";
13600
- import { readFile as readFile16, readdir as readdir7, stat as stat6, realpath, writeFile as writeFile16, mkdir as mkdir18, rename as rename12, rm as rm5 } from "fs/promises";
13601
- import { resolve as resolvePath, join as join21, relative, sep as sep3, dirname as dirname9 } from "path";
13602
- var SCOPE_SCHEMA = z26.union([
13603
- z26.literal("global"),
13604
- z26.string().regex(/^service:[A-Za-z0-9_-]{1,128}$/, "Invalid service scope")
13560
+ import { z as z25 } from "zod";
13561
+ import { TRPCError as TRPCError21 } from "@trpc/server";
13562
+ import { readFile as readFile15, readdir as readdir6, stat as stat6, realpath, writeFile as writeFile15, mkdir as mkdir17, rename as rename12, rm as rm4 } from "fs/promises";
13563
+ import { resolve as resolvePath, join as join20, relative, sep as sep3, dirname as dirname9 } from "path";
13564
+ var SCOPE_SCHEMA = z25.union([
13565
+ z25.literal("global"),
13566
+ z25.string().regex(/^service:[A-Za-z0-9_-]{1,128}$/, "Invalid service scope")
13605
13567
  ]);
13606
- var REL_PATH_SCHEMA = z26.string().min(1).max(512).regex(/^[A-Za-z0-9_./-]+$/).refine((p) => !p.includes("..") && !p.startsWith("/"), {
13568
+ var REL_PATH_SCHEMA = z25.string().min(1).max(512).regex(/^[A-Za-z0-9_./-]+$/).refine((p) => !p.includes("..") && !p.startsWith("/"), {
13607
13569
  message: "Path must be a relative path without traversal"
13608
13570
  });
13609
13571
  var MAX_FILES = 500;
@@ -13623,36 +13585,36 @@ var IGNORED_DIRS = /* @__PURE__ */ new Set([
13623
13585
  "target",
13624
13586
  "out"
13625
13587
  ]);
13626
- var DocsEntrySchema = z26.lazy(() => z26.union([
13627
- z26.object({ type: z26.literal("file"), name: z26.string(), path: z26.string() }),
13628
- z26.object({ type: z26.literal("dir"), name: z26.string(), path: z26.string(), children: z26.array(DocsEntrySchema) })
13588
+ var DocsEntrySchema = z25.lazy(() => z25.union([
13589
+ z25.object({ type: z25.literal("file"), name: z25.string(), path: z25.string() }),
13590
+ z25.object({ type: z25.literal("dir"), name: z25.string(), path: z25.string(), children: z25.array(DocsEntrySchema) })
13629
13591
  ]));
13630
13592
  async function resolveScope(scope) {
13631
13593
  if (scope === "global") {
13632
13594
  const settings = await settingsManager.getSettings();
13633
13595
  const sub = (settings.globalDocsPath?.trim() || "wiki").replace(/^[\\/]+|[\\/]+$/g, "");
13634
13596
  if (sub.includes("..") || sub.includes("\0")) {
13635
- throw new TRPCError20({ code: "BAD_REQUEST", message: "Sous-chemin invalide" });
13597
+ throw new TRPCError21({ code: "BAD_REQUEST", message: "Sous-chemin invalide" });
13636
13598
  }
13637
13599
  const dataDirAbs = resolvePath(env.DATA_DIR);
13638
- await mkdir18(dataDirAbs, { recursive: true });
13600
+ await mkdir17(dataDirAbs, { recursive: true });
13639
13601
  const base = dirname9(await realpath(dataDirAbs));
13640
13602
  const target = resolvePath(base, sub);
13641
13603
  if (target !== base && !(target + sep3).startsWith(base + sep3)) {
13642
- throw new TRPCError20({ code: "FORBIDDEN", message: "Hors du dossier de donn\xE9es du serveur" });
13604
+ throw new TRPCError21({ code: "FORBIDDEN", message: "Hors du dossier de donn\xE9es du serveur" });
13643
13605
  }
13644
- await mkdir18(target, { recursive: true });
13606
+ await mkdir17(target, { recursive: true });
13645
13607
  const root = await realpath(target);
13646
13608
  if (root !== base && !(root + sep3).startsWith(base + sep3)) {
13647
- throw new TRPCError20({ code: "FORBIDDEN", message: "Le dossier wiki r\xE9sout hors du dossier de donn\xE9es" });
13609
+ throw new TRPCError21({ code: "FORBIDDEN", message: "Le dossier wiki r\xE9sout hors du dossier de donn\xE9es" });
13648
13610
  }
13649
13611
  return { mode: "local", root };
13650
13612
  }
13651
13613
  const serviceId = scope.slice("service:".length);
13652
13614
  const service = await serviceStore.get(serviceId);
13653
- if (!service) throw new TRPCError20({ code: "NOT_FOUND", message: "Service introuvable" });
13615
+ if (!service) throw new TRPCError21({ code: "NOT_FOUND", message: "Service introuvable" });
13654
13616
  const agentId = await resolveEffectiveAgentId(service);
13655
- if (!agentId) throw new TRPCError20({ code: "BAD_REQUEST", message: "Aucun agent configur\xE9 pour ce service" });
13617
+ if (!agentId) throw new TRPCError21({ code: "BAD_REQUEST", message: "Aucun agent configur\xE9 pour ce service" });
13656
13618
  const cwd = await resolveWorkspaceCwd({ serviceId, override: service.docsPath?.trim() || void 0 });
13657
13619
  return { mode: "agent", agentId, cwd };
13658
13620
  }
@@ -13660,7 +13622,7 @@ async function walkLocal(root, dir, depth, counter) {
13660
13622
  if (depth > MAX_DEPTH || counter.n >= MAX_FILES) return [];
13661
13623
  let entries2;
13662
13624
  try {
13663
- entries2 = await readdir7(dir, { withFileTypes: true });
13625
+ entries2 = await readdir6(dir, { withFileTypes: true });
13664
13626
  } catch {
13665
13627
  return [];
13666
13628
  }
@@ -13668,7 +13630,7 @@ async function walkLocal(root, dir, depth, counter) {
13668
13630
  for (const e of entries2) {
13669
13631
  if (counter.n >= MAX_FILES) break;
13670
13632
  if (e.name.startsWith(".") && e.name !== ".") continue;
13671
- const abs = join21(dir, e.name);
13633
+ const abs = join20(dir, e.name);
13672
13634
  const rel = relative(root, abs).split(sep3).join("/");
13673
13635
  if (e.isDirectory()) {
13674
13636
  if (IGNORED_DIRS.has(e.name)) continue;
@@ -13687,7 +13649,7 @@ async function walkLocal(root, dir, depth, counter) {
13687
13649
  }
13688
13650
  async function resolveSafeLocal(root, relPath, requireExist) {
13689
13651
  if (relPath.includes("..") || relPath.startsWith("/")) {
13690
- throw new TRPCError20({ code: "BAD_REQUEST", message: "Chemin invalide" });
13652
+ throw new TRPCError21({ code: "BAD_REQUEST", message: "Chemin invalide" });
13691
13653
  }
13692
13654
  const target = resolvePath(root, relPath);
13693
13655
  if (requireExist) {
@@ -13695,10 +13657,10 @@ async function resolveSafeLocal(root, relPath, requireExist) {
13695
13657
  try {
13696
13658
  real = await realpath(target);
13697
13659
  } catch {
13698
- throw new TRPCError20({ code: "NOT_FOUND", message: "Chemin introuvable" });
13660
+ throw new TRPCError21({ code: "NOT_FOUND", message: "Chemin introuvable" });
13699
13661
  }
13700
13662
  if (real !== root && !(real + sep3).startsWith(root + sep3)) {
13701
- throw new TRPCError20({ code: "FORBIDDEN", message: "Hors du dossier autoris\xE9" });
13663
+ throw new TRPCError21({ code: "FORBIDDEN", message: "Hors du dossier autoris\xE9" });
13702
13664
  }
13703
13665
  return real;
13704
13666
  }
@@ -13710,12 +13672,12 @@ async function resolveSafeLocal(root, relPath, requireExist) {
13710
13672
  realParent = parent;
13711
13673
  }
13712
13674
  if (realParent !== root && !(realParent + sep3).startsWith(root + sep3)) {
13713
- throw new TRPCError20({ code: "FORBIDDEN", message: "Hors du dossier autoris\xE9" });
13675
+ throw new TRPCError21({ code: "FORBIDDEN", message: "Hors du dossier autoris\xE9" });
13714
13676
  }
13715
13677
  return resolvePath(realParent, target.slice(parent.length).replace(/^[\\/]+/, ""));
13716
13678
  }
13717
13679
  var wikiRouter = router({
13718
- list: protectedProcedure.input(z26.object({ scope: SCOPE_SCHEMA })).output(z26.object({ root: z26.string(), entries: z26.array(DocsEntrySchema), truncated: z26.boolean() })).query(async ({ input }) => {
13680
+ list: protectedProcedure.input(z25.object({ scope: SCOPE_SCHEMA })).output(z25.object({ root: z25.string(), entries: z25.array(DocsEntrySchema), truncated: z25.boolean() })).query(async ({ input }) => {
13719
13681
  const target = await resolveScope(input.scope);
13720
13682
  if (target.mode === "agent") {
13721
13683
  return agentManager.agentQuery(target.agentId, "wiki.listRepoMarkdown", { cwd: target.cwd });
@@ -13724,9 +13686,9 @@ var wikiRouter = router({
13724
13686
  const entries2 = await walkLocal(target.root, target.root, 0, counter);
13725
13687
  return { root: target.root, entries: entries2, truncated: counter.n >= MAX_FILES };
13726
13688
  }),
13727
- read: protectedProcedure.input(z26.object({ scope: SCOPE_SCHEMA, path: REL_PATH_SCHEMA })).output(z26.object({ path: z26.string(), content: z26.string() })).query(async ({ input }) => {
13689
+ read: protectedProcedure.input(z25.object({ scope: SCOPE_SCHEMA, path: REL_PATH_SCHEMA })).output(z25.object({ path: z25.string(), content: z25.string() })).query(async ({ input }) => {
13728
13690
  if (!/\.(md|markdown)$/i.test(input.path)) {
13729
- throw new TRPCError20({ code: "BAD_REQUEST", message: "Seuls les .md/.markdown sont autoris\xE9s" });
13691
+ throw new TRPCError21({ code: "BAD_REQUEST", message: "Seuls les .md/.markdown sont autoris\xE9s" });
13730
13692
  }
13731
13693
  const target = await resolveScope(input.scope);
13732
13694
  if (target.mode === "agent") {
@@ -13734,27 +13696,27 @@ var wikiRouter = router({
13734
13696
  }
13735
13697
  const file = await resolveSafeLocal(target.root, input.path, true);
13736
13698
  const s = await stat6(file);
13737
- if (!s.isFile()) throw new TRPCError20({ code: "BAD_REQUEST", message: "Pas un fichier" });
13738
- if (s.size > MAX_FILE_BYTES) throw new TRPCError20({ code: "PAYLOAD_TOO_LARGE", message: "Fichier trop volumineux" });
13739
- const content = await readFile16(file, "utf-8");
13699
+ if (!s.isFile()) throw new TRPCError21({ code: "BAD_REQUEST", message: "Pas un fichier" });
13700
+ if (s.size > MAX_FILE_BYTES) throw new TRPCError21({ code: "PAYLOAD_TOO_LARGE", message: "Fichier trop volumineux" });
13701
+ const content = await readFile15(file, "utf-8");
13740
13702
  return { path: input.path, content };
13741
13703
  }),
13742
- write: protectedProcedure.input(z26.object({ scope: SCOPE_SCHEMA, path: REL_PATH_SCHEMA, content: z26.string().max(5e6) })).mutation(async ({ input }) => {
13704
+ write: protectedProcedure.input(z25.object({ scope: SCOPE_SCHEMA, path: REL_PATH_SCHEMA, content: z25.string().max(5e6) })).mutation(async ({ input }) => {
13743
13705
  if (!/\.(md|markdown)$/i.test(input.path)) {
13744
- throw new TRPCError20({ code: "BAD_REQUEST", message: "Seuls les .md/.markdown sont autoris\xE9s" });
13706
+ throw new TRPCError21({ code: "BAD_REQUEST", message: "Seuls les .md/.markdown sont autoris\xE9s" });
13745
13707
  }
13746
13708
  const target = await resolveScope(input.scope);
13747
13709
  if (target.mode === "agent") {
13748
13710
  return agentManager.agentMutation(target.agentId, "wiki.writeMarkdown", { cwd: target.cwd, path: input.path, content: input.content });
13749
13711
  }
13750
13712
  const file = await resolveSafeLocal(target.root, input.path, false);
13751
- await mkdir18(dirname9(file), { recursive: true });
13752
- await writeFile16(file, input.content, "utf-8");
13713
+ await mkdir17(dirname9(file), { recursive: true });
13714
+ await writeFile15(file, input.content, "utf-8");
13753
13715
  return { path: input.path, ok: true };
13754
13716
  }),
13755
- createFile: protectedProcedure.input(z26.object({ scope: SCOPE_SCHEMA, path: REL_PATH_SCHEMA })).mutation(async ({ input }) => {
13717
+ createFile: protectedProcedure.input(z25.object({ scope: SCOPE_SCHEMA, path: REL_PATH_SCHEMA })).mutation(async ({ input }) => {
13756
13718
  if (!/\.(md|markdown)$/i.test(input.path)) {
13757
- throw new TRPCError20({ code: "BAD_REQUEST", message: "Seuls les .md/.markdown sont autoris\xE9s" });
13719
+ throw new TRPCError21({ code: "BAD_REQUEST", message: "Seuls les .md/.markdown sont autoris\xE9s" });
13758
13720
  }
13759
13721
  const target = await resolveScope(input.scope);
13760
13722
  if (target.mode === "agent") {
@@ -13763,24 +13725,24 @@ var wikiRouter = router({
13763
13725
  const file = await resolveSafeLocal(target.root, input.path, false);
13764
13726
  try {
13765
13727
  await stat6(file);
13766
- throw new TRPCError20({ code: "CONFLICT", message: "Fichier d\xE9j\xE0 existant" });
13728
+ throw new TRPCError21({ code: "CONFLICT", message: "Fichier d\xE9j\xE0 existant" });
13767
13729
  } catch (err) {
13768
- if (err instanceof TRPCError20) throw err;
13730
+ if (err instanceof TRPCError21) throw err;
13769
13731
  }
13770
- await mkdir18(dirname9(file), { recursive: true });
13771
- await writeFile16(file, "", "utf-8");
13732
+ await mkdir17(dirname9(file), { recursive: true });
13733
+ await writeFile15(file, "", "utf-8");
13772
13734
  return { path: input.path, ok: true };
13773
13735
  }),
13774
- createDir: protectedProcedure.input(z26.object({ scope: SCOPE_SCHEMA, path: REL_PATH_SCHEMA })).mutation(async ({ input }) => {
13736
+ createDir: protectedProcedure.input(z25.object({ scope: SCOPE_SCHEMA, path: REL_PATH_SCHEMA })).mutation(async ({ input }) => {
13775
13737
  const target = await resolveScope(input.scope);
13776
13738
  if (target.mode === "agent") {
13777
13739
  return agentManager.agentMutation(target.agentId, "wiki.createDir", { cwd: target.cwd, path: input.path });
13778
13740
  }
13779
13741
  const dir = await resolveSafeLocal(target.root, input.path, false);
13780
- await mkdir18(dir, { recursive: true });
13742
+ await mkdir17(dir, { recursive: true });
13781
13743
  return { path: input.path, ok: true };
13782
13744
  }),
13783
- rename: protectedProcedure.input(z26.object({ scope: SCOPE_SCHEMA, fromPath: REL_PATH_SCHEMA, toPath: REL_PATH_SCHEMA })).mutation(async ({ input }) => {
13745
+ rename: protectedProcedure.input(z25.object({ scope: SCOPE_SCHEMA, fromPath: REL_PATH_SCHEMA, toPath: REL_PATH_SCHEMA })).mutation(async ({ input }) => {
13784
13746
  const target = await resolveScope(input.scope);
13785
13747
  if (target.mode === "agent") {
13786
13748
  return agentManager.agentMutation(target.agentId, "wiki.renamePath", { cwd: target.cwd, fromPath: input.fromPath, toPath: input.toPath });
@@ -13789,35 +13751,35 @@ var wikiRouter = router({
13789
13751
  const to = await resolveSafeLocal(target.root, input.toPath, false);
13790
13752
  try {
13791
13753
  await stat6(to);
13792
- throw new TRPCError20({ code: "CONFLICT", message: "La cible existe d\xE9j\xE0" });
13754
+ throw new TRPCError21({ code: "CONFLICT", message: "La cible existe d\xE9j\xE0" });
13793
13755
  } catch (err) {
13794
- if (err instanceof TRPCError20) throw err;
13756
+ if (err instanceof TRPCError21) throw err;
13795
13757
  }
13796
- await mkdir18(dirname9(to), { recursive: true });
13758
+ await mkdir17(dirname9(to), { recursive: true });
13797
13759
  await rename12(from, to);
13798
13760
  return { fromPath: input.fromPath, toPath: input.toPath, ok: true };
13799
13761
  }),
13800
- ensureRoot: protectedProcedure.input(z26.object({ scope: SCOPE_SCHEMA })).mutation(async ({ input }) => {
13762
+ ensureRoot: protectedProcedure.input(z25.object({ scope: SCOPE_SCHEMA })).mutation(async ({ input }) => {
13801
13763
  if (input.scope === "global") {
13802
13764
  await resolveScope("global");
13803
13765
  return { ok: true };
13804
13766
  }
13805
13767
  const serviceId = input.scope.slice("service:".length);
13806
13768
  const service = await serviceStore.get(serviceId);
13807
- if (!service) throw new TRPCError20({ code: "NOT_FOUND", message: "Service introuvable" });
13769
+ if (!service) throw new TRPCError21({ code: "NOT_FOUND", message: "Service introuvable" });
13808
13770
  const agentId = await resolveEffectiveAgentId(service);
13809
- if (!agentId) throw new TRPCError20({ code: "BAD_REQUEST", message: "Aucun agent configur\xE9" });
13771
+ if (!agentId) throw new TRPCError21({ code: "BAD_REQUEST", message: "Aucun agent configur\xE9" });
13810
13772
  const cwd = await resolveWorkspaceCwd({ serviceId, override: service.docsPath?.trim() || void 0 });
13811
13773
  return agentManager.agentMutation(agentId, "wiki.ensureRoot", { cwd });
13812
13774
  }),
13813
- delete: protectedProcedure.input(z26.object({ scope: SCOPE_SCHEMA, path: REL_PATH_SCHEMA })).mutation(async ({ input }) => {
13775
+ delete: protectedProcedure.input(z25.object({ scope: SCOPE_SCHEMA, path: REL_PATH_SCHEMA })).mutation(async ({ input }) => {
13814
13776
  const target = await resolveScope(input.scope);
13815
13777
  if (target.mode === "agent") {
13816
13778
  return agentManager.agentMutation(target.agentId, "wiki.deletePath", { cwd: target.cwd, path: input.path });
13817
13779
  }
13818
13780
  const fp = await resolveSafeLocal(target.root, input.path, true);
13819
- if (fp === target.root) throw new TRPCError20({ code: "FORBIDDEN", message: "Impossible de supprimer la racine" });
13820
- await rm5(fp, { recursive: true, force: true });
13781
+ if (fp === target.root) throw new TRPCError21({ code: "FORBIDDEN", message: "Impossible de supprimer la racine" });
13782
+ await rm4(fp, { recursive: true, force: true });
13821
13783
  return { path: input.path, ok: true };
13822
13784
  })
13823
13785
  });
@@ -13836,14 +13798,14 @@ var configRouter = router({
13836
13798
  });
13837
13799
 
13838
13800
  // ../server/src/trpc/routers/cloud-sync.ts
13839
- import { z as z27 } from "zod";
13840
- import { TRPCError as TRPCError21 } from "@trpc/server";
13801
+ import { z as z26 } from "zod";
13802
+ import { TRPCError as TRPCError22 } from "@trpc/server";
13841
13803
 
13842
13804
  // ../server/src/services/org-sync-scope.ts
13843
13805
  import { access as access2 } from "fs/promises";
13844
- import { join as join22 } from "path";
13806
+ import { join as join21 } from "path";
13845
13807
  async function isMaterialized(root) {
13846
- return access2(join22(root, "project.json")).then(() => true, () => false);
13808
+ return access2(join21(root, "project.json")).then(() => true, () => false);
13847
13809
  }
13848
13810
  async function orgsToSync() {
13849
13811
  const cloudUrl = getCloudApiUrl();
@@ -13905,16 +13867,16 @@ async function reconcileOrgCanary(orgId) {
13905
13867
 
13906
13868
  // ../server/src/services/cloud-sync-bridge.ts
13907
13869
  import { randomUUID as randomUUID11 } from "crypto";
13908
- import { mkdir as mkdir20, rm as rm6 } from "fs/promises";
13870
+ import { mkdir as mkdir19, rm as rm5 } from "fs/promises";
13909
13871
  import * as Y from "yjs";
13910
13872
 
13911
13873
  // ../server/src/services/org-lock.ts
13912
- import { open as open2, readFile as readFile17, unlink as unlink4, mkdir as mkdir19 } from "fs/promises";
13874
+ import { open as open2, readFile as readFile16, unlink as unlink4, mkdir as mkdir18 } from "fs/promises";
13913
13875
  import { readFileSync, unlinkSync } from "fs";
13914
- import { join as join23 } from "path";
13876
+ import { join as join22 } from "path";
13915
13877
  var held = /* @__PURE__ */ new Set();
13916
13878
  function lockPath(orgId) {
13917
- return join23(orgVaultRoot(orgId), "org.lock");
13879
+ return join22(orgVaultRoot(orgId), "org.lock");
13918
13880
  }
13919
13881
  function isRunning(pid) {
13920
13882
  if (!Number.isInteger(pid) || pid <= 0) return false;
@@ -13927,7 +13889,7 @@ function isRunning(pid) {
13927
13889
  }
13928
13890
  async function readLock(path) {
13929
13891
  try {
13930
- const parsed = JSON.parse(await readFile17(path, "utf-8"));
13892
+ const parsed = JSON.parse(await readFile16(path, "utf-8"));
13931
13893
  return typeof parsed?.pid === "number" ? parsed : null;
13932
13894
  } catch {
13933
13895
  return null;
@@ -13935,7 +13897,7 @@ async function readLock(path) {
13935
13897
  }
13936
13898
  async function acquireOrgLock(orgId) {
13937
13899
  const path = lockPath(orgId);
13938
- await mkdir19(orgVaultRoot(orgId), { recursive: true });
13900
+ await mkdir18(orgVaultRoot(orgId), { recursive: true });
13939
13901
  for (let attempt = 0; attempt < 2; attempt++) {
13940
13902
  try {
13941
13903
  const handle = await open2(path, "wx");
@@ -14346,7 +14308,7 @@ var OrgSyncConnection = class {
14346
14308
  }
14347
14309
  const isLocal = existing?.orgId === this.link.orgId;
14348
14310
  if (!inDoc && !isLocal) return false;
14349
- await mkdir20(projectVaultRoot(this.link.orgId, projectId), { recursive: true });
14311
+ await mkdir19(projectVaultRoot(this.link.orgId, projectId), { recursive: true });
14350
14312
  this.tracked.add(projectId);
14351
14313
  if (inDoc) await this.pullFromDoc([projectId]);
14352
14314
  else await this.reconcileLocalToDoc();
@@ -14362,7 +14324,7 @@ var OrgSyncConnection = class {
14362
14324
  async unfollowProject(projectId) {
14363
14325
  this.tracked.delete(projectId);
14364
14326
  await projectStore.forgetLocally({ id: projectId, orgId: this.link.orgId });
14365
- await rm6(projectVaultRoot(this.link.orgId, projectId), { recursive: true, force: true }).catch(() => {
14327
+ await rm5(projectVaultRoot(this.link.orgId, projectId), { recursive: true, force: true }).catch(() => {
14366
14328
  });
14367
14329
  }
14368
14330
  /**
@@ -14722,8 +14684,8 @@ var CloudSyncBridge = class {
14722
14684
  var cloudSyncBridge = new CloudSyncBridge();
14723
14685
 
14724
14686
  // ../server/src/services/key-adoption.ts
14725
- import { readdir as readdir8, readFile as readFile18, writeFile as writeFile17, mkdir as mkdir21, copyFile, rename as rename13, stat as stat7 } from "fs/promises";
14726
- import { join as join24 } from "path";
14687
+ import { readdir as readdir7, readFile as readFile17, writeFile as writeFile16, mkdir as mkdir20, copyFile, rename as rename13, stat as stat7 } from "fs/promises";
14688
+ import { join as join23 } from "path";
14727
14689
  import { createCipheriv as createCipheriv2, createDecipheriv as createDecipheriv2, randomBytes as randomBytes5 } from "crypto";
14728
14690
  var ALGORITHM = "aes-256-gcm";
14729
14691
  function isAgentTriplet(value) {
@@ -14780,7 +14742,7 @@ async function rootsOfOrg(orgId) {
14780
14742
  }
14781
14743
  async function jsonFilesIn(root) {
14782
14744
  try {
14783
- const entries2 = await readdir8(root, { withFileTypes: true });
14745
+ const entries2 = await readdir7(root, { withFileTypes: true });
14784
14746
  return entries2.filter((e) => e.isFile() && e.name.endsWith(".json")).map((e) => e.name);
14785
14747
  } catch {
14786
14748
  return [];
@@ -14803,10 +14765,10 @@ async function adoptOrgKey(orgId, newKey, masterPassword, now) {
14803
14765
  const touchedRoots = /* @__PURE__ */ new Set();
14804
14766
  for (const vaultRoot of await rootsOfOrg(orgId)) {
14805
14767
  for (const name of await jsonFilesIn(vaultRoot)) {
14806
- const path = join24(vaultRoot, name);
14768
+ const path = join23(vaultRoot, name);
14807
14769
  let parsed;
14808
14770
  try {
14809
- parsed = JSON.parse(await readFile18(path, "utf-8"));
14771
+ parsed = JSON.parse(await readFile17(path, "utf-8"));
14810
14772
  } catch {
14811
14773
  continue;
14812
14774
  }
@@ -14820,21 +14782,21 @@ async function adoptOrgKey(orgId, newKey, masterPassword, now) {
14820
14782
  const stamp = now.toISOString().replace(/[:.]/g, "-").substring(0, 19);
14821
14783
  const backups = [];
14822
14784
  for (const vaultRoot of touchedRoots) {
14823
- const backupDir = join24(vaultRoot, `backup-key-${stamp}`);
14824
- await mkdir21(backupDir, { recursive: true });
14785
+ const backupDir = join23(vaultRoot, `backup-key-${stamp}`);
14786
+ await mkdir20(backupDir, { recursive: true });
14825
14787
  for (const name of await jsonFilesIn(vaultRoot)) {
14826
- await copyFile(join24(vaultRoot, name), join24(backupDir, name));
14788
+ await copyFile(join23(vaultRoot, name), join23(backupDir, name));
14827
14789
  }
14828
14790
  for (const name of [".encryption.key.enc", ".runeya-verify"]) {
14829
- if (await exists(join24(vaultRoot, name))) {
14830
- await copyFile(join24(vaultRoot, name), join24(backupDir, name));
14791
+ if (await exists(join23(vaultRoot, name))) {
14792
+ await copyFile(join23(vaultRoot, name), join23(backupDir, name));
14831
14793
  }
14832
14794
  }
14833
14795
  backups.push(backupDir);
14834
14796
  }
14835
14797
  for (const item of pending) {
14836
14798
  const tmp = item.path + ".tmp";
14837
- await writeFile17(tmp, item.content, "utf-8");
14799
+ await writeFile16(tmp, item.content, "utf-8");
14838
14800
  await rename13(tmp, item.path);
14839
14801
  }
14840
14802
  await installOrgKey(orgId, newKey, masterPassword);
@@ -14866,7 +14828,7 @@ async function assertOrgReadable(orgId) {
14866
14828
  await assertOrgVaultReadable(orgId);
14867
14829
  } catch (err) {
14868
14830
  if (err instanceof OrgVaultUnreadableError) {
14869
- throw new TRPCError21({ code: "FORBIDDEN", message: err.message });
14831
+ throw new TRPCError22({ code: "FORBIDDEN", message: err.message });
14870
14832
  }
14871
14833
  throw err;
14872
14834
  }
@@ -14876,10 +14838,10 @@ async function assertOrgKey(orgId) {
14876
14838
  await reconcileOrgCanary(orgId);
14877
14839
  } catch (err) {
14878
14840
  if (err instanceof OrgKeyMismatchError) {
14879
- throw new TRPCError21({ code: "FORBIDDEN", message: err.message });
14841
+ throw new TRPCError22({ code: "FORBIDDEN", message: err.message });
14880
14842
  }
14881
14843
  if (err instanceof CanaryUnavailableError) {
14882
- throw new TRPCError21({ code: "PRECONDITION_FAILED", message: err.message });
14844
+ throw new TRPCError22({ code: "PRECONDITION_FAILED", message: err.message });
14883
14845
  }
14884
14846
  throw err;
14885
14847
  }
@@ -14896,10 +14858,10 @@ var cloudSyncRouter = router({
14896
14858
  * "open this project" is the whole gesture.
14897
14859
  */
14898
14860
  trackProject: protectedProcedure.input(
14899
- z27.object({
14900
- orgId: z27.string().min(1).max(128),
14901
- cloudUrl: z27.string().url().max(2048),
14902
- projectId: z27.string().min(1).max(128)
14861
+ z26.object({
14862
+ orgId: z26.string().min(1).max(128),
14863
+ cloudUrl: z26.string().url().max(2048),
14864
+ projectId: z26.string().min(1).max(128)
14903
14865
  })
14904
14866
  ).mutation(async ({ input }) => {
14905
14867
  await assertOrgReadable(input.orgId);
@@ -14914,7 +14876,7 @@ var cloudSyncRouter = router({
14914
14876
  return { pulled, state: syncState(input.orgId) };
14915
14877
  } catch (err) {
14916
14878
  if (err instanceof ProjectHeldElsewhereError) {
14917
- throw new TRPCError21({ code: "CONFLICT", message: err.code, cause: err });
14879
+ throw new TRPCError22({ code: "CONFLICT", message: err.code, cause: err });
14918
14880
  }
14919
14881
  throw err;
14920
14882
  }
@@ -14933,18 +14895,18 @@ var cloudSyncRouter = router({
14933
14895
  * fusionné.
14934
14896
  */
14935
14897
  publishProject: protectedProcedure.input(
14936
- z27.object({
14937
- orgId: z27.string().min(1).max(128),
14938
- cloudUrl: z27.string().url().max(2048),
14939
- projectId: z27.string().min(1).max(128)
14898
+ z26.object({
14899
+ orgId: z26.string().min(1).max(128),
14900
+ cloudUrl: z26.string().url().max(2048),
14901
+ projectId: z26.string().min(1).max(128)
14940
14902
  })
14941
14903
  ).mutation(async ({ input }) => {
14942
14904
  const local = await projectStore.get({ id: input.projectId, orgId: null });
14943
14905
  if (!local) {
14944
- throw new TRPCError21({ code: "NOT_FOUND", message: "PROJECT_NOT_LOCAL" });
14906
+ throw new TRPCError22({ code: "NOT_FOUND", message: "PROJECT_NOT_LOCAL" });
14945
14907
  }
14946
14908
  if (await projectStore.get({ id: input.projectId, orgId: input.orgId })) {
14947
- throw new TRPCError21({ code: "CONFLICT", message: "PROJECT_ALREADY_IN_ORG" });
14909
+ throw new TRPCError22({ code: "CONFLICT", message: "PROJECT_ALREADY_IN_ORG" });
14948
14910
  }
14949
14911
  await assertOrgReadable(input.orgId);
14950
14912
  const connected = cloudSyncBridge.states().some((s) => s.orgId === input.orgId);
@@ -14963,7 +14925,7 @@ var cloudSyncRouter = router({
14963
14925
  await cloudSyncBridge.followProject(input.orgId, input.projectId);
14964
14926
  } catch (err) {
14965
14927
  if (err instanceof ProjectHeldElsewhereError) {
14966
- throw new TRPCError21({ code: "CONFLICT", message: err.code, cause: err });
14928
+ throw new TRPCError22({ code: "CONFLICT", message: err.code, cause: err });
14967
14929
  }
14968
14930
  throw err;
14969
14931
  }
@@ -14977,16 +14939,16 @@ var cloudSyncRouter = router({
14977
14939
  * publié. Le cloud, lui, ne peut pas répondre — il n'a que du chiffré sous
14978
14940
  * les yeux — donc le canari est rapatrié et jugé ici.
14979
14941
  */
14980
- checkOrgKey: protectedProcedure.input(z27.object({
14981
- orgId: z27.string().min(1).max(128),
14982
- keyHex: z27.string().length(64).regex(/^[0-9a-f]+$/i)
14942
+ checkOrgKey: protectedProcedure.input(z26.object({
14943
+ orgId: z26.string().min(1).max(128),
14944
+ keyHex: z26.string().length(64).regex(/^[0-9a-f]+$/i)
14983
14945
  })).query(async ({ input }) => {
14984
14946
  const res = await cloudFetch2(
14985
14947
  `/api/auth/organization/canary?organizationId=${encodeURIComponent(input.orgId)}`,
14986
14948
  { method: "GET" }
14987
14949
  );
14988
14950
  if (!res.ok) {
14989
- throw new TRPCError21({ code: "BAD_GATEWAY", message: await cloudMessage(res) || `CANARY_READ_FAILED (${res.status})` });
14951
+ throw new TRPCError22({ code: "BAD_GATEWAY", message: await cloudMessage(res) || `CANARY_READ_FAILED (${res.status})` });
14990
14952
  }
14991
14953
  const { canary } = await res.json();
14992
14954
  if (typeof canary !== "string") return { matches: false, published: false };
@@ -15002,10 +14964,10 @@ var cloudSyncRouter = router({
15002
14964
  * Bornée au coffre de l'organisation et à ceux de ses projets : les autres
15003
14965
  * organisations et la machine gardent leur clé.
15004
14966
  */
15005
- adoptOrgKey: protectedProcedure.input(z27.object({
15006
- orgId: z27.string().min(1).max(128),
15007
- keyHex: z27.string().length(64).regex(/^[0-9a-f]+$/i),
15008
- masterPassword: z27.string().min(1).max(1024)
14967
+ adoptOrgKey: protectedProcedure.input(z26.object({
14968
+ orgId: z26.string().min(1).max(128),
14969
+ keyHex: z26.string().length(64).regex(/^[0-9a-f]+$/i),
14970
+ masterPassword: z26.string().min(1).max(1024)
15009
14971
  })).mutation(async ({ input }) => {
15010
14972
  const report = await adoptOrgKey(
15011
14973
  input.orgId,
@@ -15029,10 +14991,10 @@ var cloudSyncRouter = router({
15029
14991
  * Elle ne re-chiffre rien : ce qui a déjà été synchronisé sous l'ancienne clé
15030
14992
  * reste illisible. L'interface le dit avant de la déclencher.
15031
14993
  */
15032
- resetOrgKey: protectedProcedure.input(z27.object({ orgId: z27.string().min(1).max(128) })).mutation(async ({ input }) => {
14994
+ resetOrgKey: protectedProcedure.input(z26.object({ orgId: z26.string().min(1).max(128) })).mutation(async ({ input }) => {
15033
14995
  const canary = await readVerifyFile(await keyRoot());
15034
14996
  if (!canary) {
15035
- throw new TRPCError21({
14997
+ throw new TRPCError22({
15036
14998
  code: "PRECONDITION_FAILED",
15037
14999
  message: "Cette machine n'a pas encore de cl\xE9 de chiffrement."
15038
15000
  });
@@ -15043,7 +15005,7 @@ var cloudSyncRouter = router({
15043
15005
  });
15044
15006
  if (!res.ok) {
15045
15007
  const detail = await cloudMessage(res);
15046
- throw new TRPCError21({
15008
+ throw new TRPCError22({
15047
15009
  code: res.status === 403 ? "FORBIDDEN" : "BAD_GATEWAY",
15048
15010
  message: detail || `ORG_KEY_RESET_FAILED (${res.status})`
15049
15011
  });
@@ -15059,9 +15021,9 @@ var cloudSyncRouter = router({
15059
15021
  * every other member and for this one on another machine.
15060
15022
  */
15061
15023
  untrackProject: protectedProcedure.input(
15062
- z27.object({
15063
- orgId: z27.string().min(1).max(128),
15064
- projectId: z27.string().min(1).max(128)
15024
+ z26.object({
15025
+ orgId: z26.string().min(1).max(128),
15026
+ projectId: z26.string().min(1).max(128)
15065
15027
  })
15066
15028
  ).mutation(async ({ input }) => {
15067
15029
  await cloudSyncBridge.unfollowProject(input.orgId, input.projectId);
@@ -15077,9 +15039,9 @@ var cloudSyncRouter = router({
15077
15039
  * membre, ce qui évite d'effacer les fichiers si le cloud a refusé l'ordre.
15078
15040
  */
15079
15041
  deleteProjectFromOrg: protectedProcedure.input(
15080
- z27.object({
15081
- orgId: z27.string().min(1).max(128),
15082
- projectId: z27.string().min(1).max(128)
15042
+ z26.object({
15043
+ orgId: z26.string().min(1).max(128),
15044
+ projectId: z26.string().min(1).max(128)
15083
15045
  })
15084
15046
  ).mutation(async ({ input }) => {
15085
15047
  let res;
@@ -15089,8 +15051,8 @@ var cloudSyncRouter = router({
15089
15051
  body: { organizationId: input.orgId, projectId: input.projectId }
15090
15052
  });
15091
15053
  } catch (err) {
15092
- if (err instanceof TRPCError21 && err.code === "PRECONDITION_FAILED") {
15093
- throw new TRPCError21({ code: "PRECONDITION_FAILED", message: "CLOUD_NOT_LINKED" });
15054
+ if (err instanceof TRPCError22 && err.code === "PRECONDITION_FAILED") {
15055
+ throw new TRPCError22({ code: "PRECONDITION_FAILED", message: "CLOUD_NOT_LINKED" });
15094
15056
  }
15095
15057
  throw err;
15096
15058
  }
@@ -15098,12 +15060,12 @@ var cloudSyncRouter = router({
15098
15060
  const detail = await cloudMessage(res);
15099
15061
  if (res.status === 403) {
15100
15062
  const code = /not a member/i.test(detail) ? "PROJECT_NOT_MEMBER" : "PROJECT_DELETE_FORBIDDEN";
15101
- throw new TRPCError21({ code: "FORBIDDEN", message: code, cause: new Error(detail) });
15063
+ throw new TRPCError22({ code: "FORBIDDEN", message: code, cause: new Error(detail) });
15102
15064
  }
15103
15065
  if (res.status === 404) {
15104
- throw new TRPCError21({ code: "NOT_FOUND", message: "PROJECT_NOT_IN_ORG", cause: new Error(detail) });
15066
+ throw new TRPCError22({ code: "NOT_FOUND", message: "PROJECT_NOT_IN_ORG", cause: new Error(detail) });
15105
15067
  }
15106
- throw new TRPCError21({
15068
+ throw new TRPCError22({
15107
15069
  code: "BAD_GATEWAY",
15108
15070
  message: detail || `PROJECT_DELETE_FAILED (${res.status})`
15109
15071
  });
@@ -15123,10 +15085,10 @@ var cloudSyncRouter = router({
15123
15085
  * afficher. Le nom est donc recopié dans le tampon du coffre au passage, là
15124
15086
  * où il est déjà lu au démarrage suivant.
15125
15087
  */
15126
- rememberOrgNames: protectedProcedure.input(z27.object({
15127
- orgs: z27.array(z27.object({
15128
- orgId: z27.string().min(1).max(128),
15129
- name: z27.string().min(1).max(100)
15088
+ rememberOrgNames: protectedProcedure.input(z26.object({
15089
+ orgs: z26.array(z26.object({
15090
+ orgId: z26.string().min(1).max(128),
15091
+ name: z26.string().min(1).max(100)
15130
15092
  })).max(200)
15131
15093
  })).mutation(async ({ input }) => {
15132
15094
  for (const { orgId, name } of input.orgs) await rememberOrgName(orgId, name);
@@ -15160,15 +15122,15 @@ var cloudSyncRouter = router({
15160
15122
  });
15161
15123
 
15162
15124
  // ../server/src/trpc/routers/filesystem.ts
15163
- import { readdir as readdir9, stat as stat8 } from "fs/promises";
15125
+ import { readdir as readdir8, stat as stat8 } from "fs/promises";
15164
15126
  import { homedir as homedir5 } from "os";
15165
- import { isAbsolute as isAbsolute4, join as join25, resolve as resolve5, dirname as dirname10, basename as basename2 } from "path";
15166
- import { z as z28 } from "zod";
15127
+ import { isAbsolute as isAbsolute4, join as join24, resolve as resolve5, dirname as dirname10, basename as basename2 } from "path";
15128
+ import { z as z27 } from "zod";
15167
15129
  var MAX_ENTRIES2 = 500;
15168
- var PATH_SCHEMA = z28.string().min(1).max(4096);
15130
+ var PATH_SCHEMA = z27.string().min(1).max(4096);
15169
15131
  function safeResolve(input) {
15170
15132
  if (input.includes("\0")) throw new Error("Chemin invalide");
15171
- const expanded = input.startsWith("~") ? join25(homedir5(), input.slice(1)) : input;
15133
+ const expanded = input.startsWith("~") ? join24(homedir5(), input.slice(1)) : input;
15172
15134
  if (!isAbsolute4(expanded)) throw new Error("Le chemin doit \xEAtre absolu");
15173
15135
  return resolve5(expanded);
15174
15136
  }
@@ -15189,7 +15151,7 @@ var filesystemRouter = router({
15189
15151
  * Hidden entries are skipped: they are noise for choosing a workspace, and
15190
15152
  * `.runeya` itself would invite placing a project inside another's data.
15191
15153
  */
15192
- listDirectories: protectedProcedure.input(z28.object({ path: PATH_SCHEMA })).query(async ({ input }) => {
15154
+ listDirectories: protectedProcedure.input(z27.object({ path: PATH_SCHEMA })).query(async ({ input }) => {
15193
15155
  const dir = safeResolve(input.path);
15194
15156
  const info = await stat8(dir).catch(() => null);
15195
15157
  if (!info?.isDirectory()) {
@@ -15197,14 +15159,14 @@ var filesystemRouter = router({
15197
15159
  }
15198
15160
  let names;
15199
15161
  try {
15200
- names = (await readdir9(dir, { withFileTypes: true })).filter((e) => e.isDirectory() && !e.name.startsWith(".")).map((e) => e.name).sort((a, b) => a.localeCompare(b));
15162
+ names = (await readdir8(dir, { withFileTypes: true })).filter((e) => e.isDirectory() && !e.name.startsWith(".")).map((e) => e.name).sort((a, b) => a.localeCompare(b));
15201
15163
  } catch {
15202
15164
  throw new Error(`R\xE9pertoire illisible : ${dir}`);
15203
15165
  }
15204
15166
  const truncated = names.length > MAX_ENTRIES2;
15205
15167
  const entries2 = [];
15206
15168
  for (const name of names.slice(0, MAX_ENTRIES2)) {
15207
- const path = join25(dir, name);
15169
+ const path = join24(dir, name);
15208
15170
  entries2.push({ name, path, hasRuneyaProject: await holdsRuneyaProject(path) });
15209
15171
  }
15210
15172
  return {
@@ -15223,10 +15185,10 @@ var filesystemRouter = router({
15223
15185
  * Two projects in one directory is what makes services run against the wrong
15224
15186
  * code, so a directory that already holds one is refused outright.
15225
15187
  */
15226
- checkProjectTarget: protectedProcedure.input(z28.object({
15188
+ checkProjectTarget: protectedProcedure.input(z27.object({
15227
15189
  path: PATH_SCHEMA,
15228
- projectId: z28.string().min(1).max(128),
15229
- orgId: z28.string().max(128).nullable().optional()
15190
+ projectId: z27.string().min(1).max(128),
15191
+ orgId: z27.string().max(128).nullable().optional()
15230
15192
  })).query(async ({ input }) => {
15231
15193
  const dir = safeResolve(input.path);
15232
15194
  const info = await stat8(dir).catch(() => null);
@@ -15336,8 +15298,8 @@ async function ensureFilePermissions() {
15336
15298
  }
15337
15299
 
15338
15300
  // ../server/src/services/vault-migration.ts
15339
- import { readdir as readdir10, readFile as readFile19, writeFile as writeFile18, mkdir as mkdir22, copyFile as copyFile2, unlink as unlink5, rename as rename14, stat as stat10 } from "fs/promises";
15340
- import { join as join26 } from "path";
15301
+ import { readdir as readdir9, readFile as readFile18, writeFile as writeFile17, mkdir as mkdir21, copyFile as copyFile2, unlink as unlink5, rename as rename14, stat as stat10 } from "fs/promises";
15302
+ import { join as join25 } from "path";
15341
15303
  var KEY_FILES = [".encryption.key.enc", ".runeya-verify"];
15342
15304
  var MACHINE_FILES = [
15343
15305
  "agents.json",
@@ -15349,7 +15311,7 @@ var MACHINE_FILES = [
15349
15311
  ];
15350
15312
  var rekeyValue = rekeyJsonValue;
15351
15313
  async function vaultJsonFiles(root) {
15352
- const entries2 = await readdir10(root, { withFileTypes: true });
15314
+ const entries2 = await readdir9(root, { withFileTypes: true });
15353
15315
  return entries2.filter((e) => e.isFile() && e.name.endsWith(".json")).map((e) => e.name);
15354
15316
  }
15355
15317
  async function exists2(path) {
@@ -15367,17 +15329,17 @@ async function openLegacyVault(legacyRoot, masterPassword) {
15367
15329
  }
15368
15330
  async function migrateLegacyVault(legacyRoot, machineRootDir, legacyKey, machineKey, now) {
15369
15331
  const stamp = now.toISOString().replace(/[:.]/g, "-").substring(0, 19);
15370
- const backupDir = join26(legacyRoot, `backup-${stamp}`);
15371
- await mkdir22(backupDir, { recursive: true });
15332
+ const backupDir = join25(legacyRoot, `backup-${stamp}`);
15333
+ await mkdir21(backupDir, { recursive: true });
15372
15334
  const jsonFiles = await vaultJsonFiles(legacyRoot);
15373
15335
  for (const name of [...jsonFiles, ...KEY_FILES]) {
15374
- if (await exists2(join26(legacyRoot, name))) {
15375
- await copyFile2(join26(legacyRoot, name), join26(backupDir, name));
15336
+ if (await exists2(join25(legacyRoot, name))) {
15337
+ await copyFile2(join25(legacyRoot, name), join25(backupDir, name));
15376
15338
  }
15377
15339
  }
15378
15340
  const rekeyed = [];
15379
15341
  for (const name of jsonFiles) {
15380
- const raw = await readFile19(join26(legacyRoot, name), "utf-8");
15342
+ const raw = await readFile18(join25(legacyRoot, name), "utf-8");
15381
15343
  let parsed;
15382
15344
  try {
15383
15345
  parsed = JSON.parse(raw);
@@ -15387,13 +15349,13 @@ async function migrateLegacyVault(legacyRoot, machineRootDir, legacyKey, machine
15387
15349
  const counter = { n: 0 };
15388
15350
  const converted = rekeyValue(parsed, legacyKey, machineKey, counter);
15389
15351
  if (counter.n === 0) continue;
15390
- const tmp = join26(legacyRoot, `${name}.tmp`);
15391
- await writeFile18(tmp, JSON.stringify(converted, null, 2), "utf-8");
15392
- await rename14(tmp, join26(legacyRoot, name));
15352
+ const tmp = join25(legacyRoot, `${name}.tmp`);
15353
+ await writeFile17(tmp, JSON.stringify(converted, null, 2), "utf-8");
15354
+ await rename14(tmp, join25(legacyRoot, name));
15393
15355
  rekeyed.push({ file: name, values: counter.n });
15394
15356
  }
15395
15357
  for (const { file, values } of rekeyed) {
15396
- const parsed = JSON.parse(await readFile19(join26(legacyRoot, file), "utf-8"));
15358
+ const parsed = JSON.parse(await readFile18(join25(legacyRoot, file), "utf-8"));
15397
15359
  const check = { n: 0 };
15398
15360
  rekeyValue(parsed, machineKey, machineKey, check);
15399
15361
  if (check.n !== values) {
@@ -15402,13 +15364,13 @@ async function migrateLegacyVault(legacyRoot, machineRootDir, legacyKey, machine
15402
15364
  );
15403
15365
  }
15404
15366
  }
15405
- await mkdir22(machineRootDir, { recursive: true });
15367
+ await mkdir21(machineRootDir, { recursive: true });
15406
15368
  const moved = [];
15407
15369
  const skipped = [];
15408
15370
  for (const name of MACHINE_FILES) {
15409
- const from = join26(legacyRoot, name);
15371
+ const from = join25(legacyRoot, name);
15410
15372
  if (!await exists2(from)) continue;
15411
- const to = join26(machineRootDir, name);
15373
+ const to = join25(machineRootDir, name);
15412
15374
  if (await exists2(to)) {
15413
15375
  skipped.push(name);
15414
15376
  continue;
@@ -15418,15 +15380,15 @@ async function migrateLegacyVault(legacyRoot, machineRootDir, legacyKey, machine
15418
15380
  moved.push(name);
15419
15381
  }
15420
15382
  for (const name of KEY_FILES) {
15421
- const path = join26(legacyRoot, name);
15383
+ const path = join25(legacyRoot, name);
15422
15384
  if (await exists2(path)) await unlink5(path);
15423
15385
  }
15424
15386
  return { backupDir, rekeyed, moved, skipped };
15425
15387
  }
15426
15388
 
15427
15389
  // ../server/src/services/settings-migration.ts
15428
- import { readFile as readFile20, writeFile as writeFile19, rename as rename15, unlink as unlink6, mkdir as mkdir23, chmod as chmod12, stat as stat11 } from "fs/promises";
15429
- import { join as join27 } from "path";
15390
+ import { readFile as readFile19, writeFile as writeFile18, rename as rename15, unlink as unlink6, mkdir as mkdir22, chmod as chmod12, stat as stat11 } from "fs/promises";
15391
+ import { join as join26 } from "path";
15430
15392
  var SETTINGS_FILE2 = "settings.json";
15431
15393
  var SETTINGS_PRIVATE_FILE2 = "settings.private.json";
15432
15394
  async function exists3(path) {
@@ -15439,11 +15401,11 @@ async function legacySource(filename) {
15439
15401
  return null;
15440
15402
  }
15441
15403
  async function moveInto(root, filename, from) {
15442
- await mkdir23(root, { recursive: true });
15443
- const to = join27(root, filename);
15444
- const contents = await readFile20(from, "utf-8");
15404
+ await mkdir22(root, { recursive: true });
15405
+ const to = join26(root, filename);
15406
+ const contents = await readFile19(from, "utf-8");
15445
15407
  const tmp = to + ".tmp";
15446
- await writeFile19(tmp, contents, "utf-8");
15408
+ await writeFile18(tmp, contents, "utf-8");
15447
15409
  await rename15(tmp, to);
15448
15410
  await chmod12(to, 384);
15449
15411
  await unlink6(from);
@@ -15465,7 +15427,7 @@ async function migrateSettingsIntoVaults() {
15465
15427
  const orgId = orgs[0];
15466
15428
  const root = orgVaultRoot(orgId);
15467
15429
  for (const { filename, from } of sources) {
15468
- if (await exists3(join27(root, filename))) continue;
15430
+ if (await exists3(join26(root, filename))) continue;
15469
15431
  await moveInto(root, filename, from);
15470
15432
  report.files.push(filename);
15471
15433
  }
@@ -15481,14 +15443,14 @@ import { createRequire as createRequire2 } from "module";
15481
15443
  import { randomBytes as randomBytes6, createCipheriv as createCipheriv3, createDecipheriv as createDecipheriv3 } from "crypto";
15482
15444
  import { randomUUID as randomUUID12 } from "crypto";
15483
15445
  import {
15484
- readFile as readFile21,
15485
- writeFile as writeFile20,
15446
+ readFile as readFile20,
15447
+ writeFile as writeFile19,
15486
15448
  rename as rename16,
15487
- mkdir as mkdir24,
15449
+ mkdir as mkdir23,
15488
15450
  access as access3,
15489
- readdir as readdir11
15451
+ readdir as readdir10
15490
15452
  } from "fs/promises";
15491
- import { join as join28, dirname as dirname11 } from "path";
15453
+ import { join as join27, dirname as dirname11 } from "path";
15492
15454
  var _require = createRequire2(import.meta.url);
15493
15455
  var PARSER_ID_MAP = {
15494
15456
  "stack-monitor-parser-jsons": "native:json",
@@ -15597,7 +15559,7 @@ async function getSodium() {
15597
15559
  return _sodium;
15598
15560
  }
15599
15561
  async function decryptLegacyFile(filePath, key, sodium) {
15600
- const raw = (await readFile21(filePath, "utf-8")).trim();
15562
+ const raw = (await readFile20(filePath, "utf-8")).trim();
15601
15563
  if (!raw || raw === "[]" || raw === "{}") {
15602
15564
  try {
15603
15565
  return JSON.parse(raw);
@@ -15617,12 +15579,12 @@ async function decryptLegacyFile(filePath, key, sodium) {
15617
15579
  }
15618
15580
  async function atomicWrite(path, data) {
15619
15581
  const tmp = path + ".tmp";
15620
- await writeFile20(tmp, JSON.stringify(data, null, 2), "utf-8");
15582
+ await writeFile19(tmp, JSON.stringify(data, null, 2), "utf-8");
15621
15583
  await rename16(tmp, path);
15622
15584
  }
15623
15585
  async function runLegacyMigration(dataDir) {
15624
15586
  const legacyRoot = env.DATA_DIR;
15625
- const legacyKeyPath = join28(legacyRoot, "dbs", "encryption-key.json");
15587
+ const legacyKeyPath = join27(legacyRoot, "dbs", "encryption-key.json");
15626
15588
  try {
15627
15589
  await access3(legacyKeyPath);
15628
15590
  } catch {
@@ -15630,20 +15592,20 @@ async function runLegacyMigration(dataDir) {
15630
15592
  }
15631
15593
  console.log("[legacy-migration] Legacy Runeya data detected, starting migration\u2026");
15632
15594
  const timestamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-").substring(0, 19);
15633
- const legacyTmp = join28(abCwd, ".runeya-tmp");
15595
+ const legacyTmp = join27(abCwd, ".runeya-tmp");
15634
15596
  await rename16(legacyRoot, legacyTmp);
15635
- await mkdir24(dataDir, { recursive: true });
15636
- const legacyOld = join28(dataDir, `backup-${timestamp}`);
15597
+ await mkdir23(dataDir, { recursive: true });
15598
+ const legacyOld = join27(dataDir, `backup-${timestamp}`);
15637
15599
  await rename16(legacyTmp, legacyOld);
15638
15600
  console.log(`[legacy-migration] Legacy data backed up to ${legacyOld}`);
15639
- const keyJson = JSON.parse(await readFile21(join28(legacyOld, "dbs", "encryption-key.json"), "utf-8"));
15601
+ const keyJson = JSON.parse(await readFile20(join27(legacyOld, "dbs", "encryption-key.json"), "utf-8"));
15640
15602
  const sodium = await getSodium();
15641
15603
  const sodiumKey = sodium.from_base64(keyJson.encryptionKey);
15642
15604
  const keyBuffer = Buffer.from(sodiumKey);
15643
15605
  const runyeaKeyHex = keyBuffer.toString("hex");
15644
15606
  await setEncryptionKey(runyeaKeyHex);
15645
15607
  console.log("[legacy-migration] Encryption key loaded in memory (temporary)");
15646
- const dbsDir = join28(legacyOld, "dbs");
15608
+ const dbsDir = join27(legacyOld, "dbs");
15647
15609
  async function readEncrypted(filePath) {
15648
15610
  try {
15649
15611
  return await decryptLegacyFile(filePath, sodiumKey, sodium);
@@ -15652,29 +15614,29 @@ async function runLegacyMigration(dataDir) {
15652
15614
  return null;
15653
15615
  }
15654
15616
  }
15655
- const envsDir = join28(dbsDir, "envs");
15656
- const envFiles = (await readdir11(envsDir).catch(() => [])).filter((f) => f.endsWith(".encrypted.json"));
15617
+ const envsDir = join27(dbsDir, "envs");
15618
+ const envFiles = (await readdir10(envsDir).catch(() => [])).filter((f) => f.endsWith(".encrypted.json"));
15657
15619
  const legacyEnvs = {};
15658
15620
  for (const file of envFiles) {
15659
15621
  const name = file.replace(".encrypted.json", "");
15660
- legacyEnvs[name] = await readEncrypted(join28(envsDir, file));
15622
+ legacyEnvs[name] = await readEncrypted(join27(envsDir, file));
15661
15623
  }
15662
- const servicesDir = join28(dbsDir, "services");
15663
- const serviceFiles = (await readdir11(servicesDir).catch(() => [])).filter((f) => f.endsWith(".encrypted.json"));
15624
+ const servicesDir = join27(dbsDir, "services");
15625
+ const serviceFiles = (await readdir10(servicesDir).catch(() => [])).filter((f) => f.endsWith(".encrypted.json"));
15664
15626
  const legacyServices = {};
15665
15627
  for (const file of serviceFiles) {
15666
15628
  const name = file.replace(".encrypted.json", "");
15667
15629
  if (!name) continue;
15668
- const data = await readEncrypted(join28(servicesDir, file));
15630
+ const data = await readEncrypted(join27(servicesDir, file));
15669
15631
  if (data?.label) legacyServices[name] = data;
15670
15632
  }
15671
- const overridesDir = join28(dbsDir, "overrides");
15672
- const overrideFiles = (await readdir11(overridesDir).catch(() => [])).filter((f) => f.endsWith(".encrypted.json"));
15633
+ const overridesDir = join27(dbsDir, "overrides");
15634
+ const overrideFiles = (await readdir10(overridesDir).catch(() => [])).filter((f) => f.endsWith(".encrypted.json"));
15673
15635
  const legacyServiceOverrides = {};
15674
15636
  const legacyEnvOverrides = {};
15675
15637
  for (const file of overrideFiles) {
15676
15638
  const baseName = file.replace(".encrypted.json", "");
15677
- const data = await readEncrypted(join28(overridesDir, file));
15639
+ const data = await readEncrypted(join27(overridesDir, file));
15678
15640
  if (!data) continue;
15679
15641
  if (baseName.endsWith("-envs")) {
15680
15642
  legacyServiceOverrides[baseName.slice(0, -5)] = data;
@@ -15682,11 +15644,11 @@ async function runLegacyMigration(dataDir) {
15682
15644
  legacyEnvOverrides[baseName.slice(0, -12)] = data;
15683
15645
  }
15684
15646
  }
15685
- const parsersDir = join28(dbsDir, "parsers");
15686
- const parserFiles = (await readdir11(parsersDir).catch(() => [])).filter((f) => f.endsWith(".encrypted.json"));
15647
+ const parsersDir = join27(dbsDir, "parsers");
15648
+ const parserFiles = (await readdir10(parsersDir).catch(() => [])).filter((f) => f.endsWith(".encrypted.json"));
15687
15649
  const legacyParsers = [];
15688
15650
  for (const file of parserFiles) {
15689
- const data = await readEncrypted(join28(parsersDir, file));
15651
+ const data = await readEncrypted(join27(parsersDir, file));
15690
15652
  if (data?.id) legacyParsers.push(data);
15691
15653
  }
15692
15654
  const projectMonoId = randomUUID12();
@@ -16027,7 +15989,7 @@ async function runLegacyMigration(dataDir) {
16027
15989
  }
16028
15990
  let existingSettings = {};
16029
15991
  try {
16030
- existingSettings = JSON.parse(await readFile21(join28(dataDir, "settings.json"), "utf-8"));
15992
+ existingSettings = JSON.parse(await readFile20(join27(dataDir, "settings.json"), "utf-8"));
16031
15993
  } catch {
16032
15994
  }
16033
15995
  const migratedParsers = legacyParsers.map((p) => ({
@@ -16048,11 +16010,11 @@ ${p.transform ?? ""}`,
16048
16010
  ...migratedParsers
16049
16011
  ]
16050
16012
  };
16051
- await atomicWrite(join28(dataDir, "projects.json"), runeProjects);
16052
- await atomicWrite(join28(dataDir, "services.json"), runeServices);
16053
- await atomicWrite(join28(dataDir, "environments.json"), runeEnvs);
16054
- await atomicWrite(join28(dataDir, "environment-overrides.json"), flatOverrides);
16055
- await atomicWrite(join28(dataDir, "settings.json"), newSettings);
16013
+ await atomicWrite(join27(dataDir, "projects.json"), runeProjects);
16014
+ await atomicWrite(join27(dataDir, "services.json"), runeServices);
16015
+ await atomicWrite(join27(dataDir, "environments.json"), runeEnvs);
16016
+ await atomicWrite(join27(dataDir, "environment-overrides.json"), flatOverrides);
16017
+ await atomicWrite(join27(dataDir, "settings.json"), newSettings);
16056
16018
  const svcEnvCount = runeEnvs.filter((e) => e.scope === "service").length;
16057
16019
  console.log(`[legacy-migration] Written: ${runeProjects.length} projects, ${runeServices.length} services, ${runeEnvs.length} environments (${svcEnvCount} service-scoped)`);
16058
16020
  console.log(`[legacy-migration] Overrides: ${Object.keys(flatOverrides).length}, custom parsers: ${migratedParsers.length} (all disabled \u2014 rewrite required)`);
@@ -16063,12 +16025,12 @@ ${p.transform ?? ""}`,
16063
16025
  }
16064
16026
  async function findLegacyKeyInBackup(dataDir) {
16065
16027
  try {
16066
- const entries2 = await readdir11(dataDir);
16028
+ const entries2 = await readdir10(dataDir);
16067
16029
  const backupDirs = entries2.filter((e) => e.startsWith("backup-")).sort((a, b) => a < b ? -1 : a > b ? 1 : 0).reverse();
16068
16030
  for (const dir of backupDirs) {
16069
- const keyJsonPath = join28(dataDir, dir, "dbs", "encryption-key.json");
16031
+ const keyJsonPath = join27(dataDir, dir, "dbs", "encryption-key.json");
16070
16032
  try {
16071
- const raw = await readFile21(keyJsonPath, "utf-8");
16033
+ const raw = await readFile20(keyJsonPath, "utf-8");
16072
16034
  const parsed = JSON.parse(raw);
16073
16035
  if (!parsed.encryptionKey) continue;
16074
16036
  const sodium = await getSodium();
@@ -16089,14 +16051,14 @@ async function runOverrideOnlyMigration(dataDir) {
16089
16051
  const { resolve: resolve7 } = await import("path");
16090
16052
  const abDataDir = resolve7(dataDir);
16091
16053
  const candidateDirs = [
16092
- join28(dirname11(env.DATA_DIR), ".runeya", "dbs", "overrides"),
16093
- join28(abDataDir, "dbs", "overrides")
16054
+ join27(dirname11(env.DATA_DIR), ".runeya", "dbs", "overrides"),
16055
+ join27(abDataDir, "dbs", "overrides")
16094
16056
  ];
16095
16057
  let overridesDir = null;
16096
16058
  let overrideFiles = [];
16097
16059
  for (const dir of candidateDirs) {
16098
16060
  try {
16099
- const files = (await readdir11(dir)).filter((f) => f.endsWith(".encrypted.json"));
16061
+ const files = (await readdir10(dir)).filter((f) => f.endsWith(".encrypted.json"));
16100
16062
  if (files.length > 0) {
16101
16063
  overridesDir = dir;
16102
16064
  overrideFiles = files;
@@ -16106,7 +16068,7 @@ async function runOverrideOnlyMigration(dataDir) {
16106
16068
  }
16107
16069
  }
16108
16070
  if (!overridesDir || overrideFiles.length === 0) return false;
16109
- const legacyKeyPath = join28(overridesDir, "..", "encryption-key.json");
16071
+ const legacyKeyPath = join27(overridesDir, "..", "encryption-key.json");
16110
16072
  try {
16111
16073
  await access3(legacyKeyPath);
16112
16074
  return false;
@@ -16124,7 +16086,7 @@ async function runOverrideOnlyMigration(dataDir) {
16124
16086
  const envNameToId = {};
16125
16087
  let envsRaw;
16126
16088
  try {
16127
- envsRaw = JSON.parse(await readFile21(join28(abDataDir, "environments.json"), "utf-8"));
16089
+ envsRaw = JSON.parse(await readFile20(join27(abDataDir, "environments.json"), "utf-8"));
16128
16090
  for (const _env of envsRaw) {
16129
16091
  const env2 = _env;
16130
16092
  if (env2["name"] && env2["id"]) {
@@ -16143,7 +16105,7 @@ async function runOverrideOnlyMigration(dataDir) {
16143
16105
  let servicesRaw = [];
16144
16106
  const serviceNameToId = {};
16145
16107
  try {
16146
- servicesRaw = JSON.parse(await readFile21(join28(abDataDir, "services.json"), "utf-8"));
16108
+ servicesRaw = JSON.parse(await readFile20(join27(abDataDir, "services.json"), "utf-8"));
16147
16109
  for (const _svc of servicesRaw) {
16148
16110
  const svc = _svc;
16149
16111
  if (svc["name"] && svc["id"]) serviceNameToId[svc["name"]] = svc["id"];
@@ -16160,7 +16122,7 @@ async function runOverrideOnlyMigration(dataDir) {
16160
16122
  const envName = baseName.slice(0, -"-environment".length);
16161
16123
  let data;
16162
16124
  try {
16163
- data = await decryptLegacyFile(join28(overridesDir, file), sodiumKey, sodium);
16125
+ data = await decryptLegacyFile(join27(overridesDir, file), sodiumKey, sodium);
16164
16126
  } catch (err) {
16165
16127
  console.warn(`[override-migration] Failed to decrypt ${file}: ${err.message}`);
16166
16128
  continue;
@@ -16187,7 +16149,7 @@ async function runOverrideOnlyMigration(dataDir) {
16187
16149
  }
16188
16150
  }
16189
16151
  if (Object.keys(flatOverrides).length > 0) {
16190
- await atomicWrite(join28(abDataDir, "environment-overrides.json"), flatOverrides);
16152
+ await atomicWrite(join27(abDataDir, "environment-overrides.json"), flatOverrides);
16191
16153
  console.log(`[override-migration] Written ${Object.keys(flatOverrides).length} override(s) to environment-overrides.json`);
16192
16154
  }
16193
16155
  let envsModified = false;
@@ -16197,7 +16159,7 @@ async function runOverrideOnlyMigration(dataDir) {
16197
16159
  const serviceName = baseName.slice(0, -"-envs".length);
16198
16160
  let data;
16199
16161
  try {
16200
- data = await decryptLegacyFile(join28(overridesDir, file), sodiumKey, sodium);
16162
+ data = await decryptLegacyFile(join27(overridesDir, file), sodiumKey, sodium);
16201
16163
  } catch (err) {
16202
16164
  console.warn(`[override-migration] Failed to decrypt ${file}: ${err.message}`);
16203
16165
  continue;
@@ -16256,7 +16218,7 @@ async function runOverrideOnlyMigration(dataDir) {
16256
16218
  }
16257
16219
  }
16258
16220
  if (envsModified) {
16259
- await atomicWrite(join28(abDataDir, "environments.json"), envsRaw);
16221
+ await atomicWrite(join27(abDataDir, "environments.json"), envsRaw);
16260
16222
  console.log("[override-migration] Merged service overrides into environments.json");
16261
16223
  }
16262
16224
  const migratedDir = overridesDir.replace(/overrides$/, "overrides-migrated");
@@ -16271,7 +16233,7 @@ async function runOverrideOnlyMigration(dataDir) {
16271
16233
  }
16272
16234
 
16273
16235
  // ../server/src/services/server-state.ts
16274
- import { join as join29 } from "path";
16236
+ import { join as join28 } from "path";
16275
16237
  import { stat as stat12 } from "fs/promises";
16276
16238
  var ENCRYPTED_KEY_FILENAME2 = ".encryption.key.enc";
16277
16239
  var VERIFY_FILENAME2 = ".runeya-verify";
@@ -16281,22 +16243,22 @@ async function getServerState() {
16281
16243
  let hasVerify = false;
16282
16244
  let hasData = false;
16283
16245
  try {
16284
- await stat12(join29(dataDir, ENCRYPTED_KEY_FILENAME2));
16246
+ await stat12(join28(dataDir, ENCRYPTED_KEY_FILENAME2));
16285
16247
  hasEncFile = true;
16286
16248
  } catch {
16287
16249
  }
16288
16250
  try {
16289
- await stat12(join29(dataDir, VERIFY_FILENAME2));
16251
+ await stat12(join28(dataDir, VERIFY_FILENAME2));
16290
16252
  hasVerify = true;
16291
16253
  } catch {
16292
16254
  }
16293
16255
  try {
16294
- await stat12(join29(dataDir, "projects.json"));
16256
+ await stat12(join28(dataDir, "projects.json"));
16295
16257
  hasData = true;
16296
16258
  } catch {
16297
16259
  }
16298
16260
  try {
16299
- await stat12(join29(dataDir, "project.json"));
16261
+ await stat12(join28(dataDir, "project.json"));
16300
16262
  hasData = true;
16301
16263
  } catch {
16302
16264
  }
@@ -17556,4 +17518,4 @@ export {
17556
17518
  pullEnv,
17557
17519
  registerInstance
17558
17520
  };
17559
- //# sourceMappingURL=src-MQWOIUB5.js.map
17521
+ //# sourceMappingURL=src-Q3PSCMS7.js.map