@runeya/runeya 2.0.93 → 2.0.95
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.
- package/agent/index.js +213 -161
- package/agent/index.js.map +1 -1
- package/agent-manager-QQPKKLKY.js +8 -0
- package/chat-LPMGV2VS.js +18 -0
- package/chat-LPMGV2VS.js.map +1 -0
- package/chunk-3MJLS5UY.js +1944 -0
- package/chunk-3MJLS5UY.js.map +1 -0
- package/chunk-72LQC3YC.js +152 -0
- package/chunk-72LQC3YC.js.map +1 -0
- package/{chunk-6Y24763Y.js → chunk-HX3ZIWHM.js} +83 -1926
- package/chunk-HX3ZIWHM.js.map +1 -0
- package/chunk-MD3VW4HQ.js +160 -0
- package/chunk-MD3VW4HQ.js.map +1 -0
- package/chunk-R23UUB5R.js +7285 -0
- package/chunk-R23UUB5R.js.map +1 -0
- package/chunk-WAC56HS4.js +1842 -0
- package/chunk-WAC56HS4.js.map +1 -0
- package/codex-thread-db-LE5FYDDA.js +27 -0
- package/codex-thread-db-LE5FYDDA.js.map +1 -0
- package/companion-VPYVGTRU.js +42 -0
- package/companion-VPYVGTRU.js.map +1 -0
- package/index.js +1 -1
- package/package.json +1 -1
- package/settings-manager-6FKHBLIV.js +8 -0
- package/settings-manager-6FKHBLIV.js.map +1 -0
- package/src-NEPDGULO.js +10087 -0
- package/src-NEPDGULO.js.map +1 -0
- package/web/assets/AgentScalarChatInterface.vue-DCnwgzWk.js +13 -0
- package/web/assets/AgentTerminalPage-D6iEzvyz.js +1 -0
- package/web/assets/AiChatPage-DDr9XEv8.js +1 -0
- package/web/assets/AppLayout-B2mreJVA.js +1 -0
- package/web/assets/{AppLayout-DiMqQfDk.css → AppLayout-nrP0jOZ1.css} +1 -1
- package/web/assets/AuditPage-CSPHdf0m.js +1 -0
- package/web/assets/CloudAccountPage-BAkTRH4E.js +1 -0
- package/web/assets/CloudAccountPage-C35O8Pcb.js +1 -0
- package/web/assets/CloudMessagingPage-5KYELXQI.js +2 -0
- package/web/assets/DashboardPage-Dl9Sg1av.js +1 -0
- package/web/assets/DirectoryPickerDialog-DLL3-GoW.js +1 -0
- package/web/assets/{EditorIcon-Bi1br5gY.js → EditorIcon-CzgSjXLk.js} +2 -2
- package/web/assets/EnvironmentForm-DmdDIJHv.js +108 -0
- package/web/assets/EnvironmentFormPage-CV7Uq6Ah.js +1 -0
- package/web/assets/EnvironmentListPage-Cfo9RA_g.js +1 -0
- package/web/assets/KanbanBoardPage-DL1WwMBP.js +4 -0
- package/web/assets/KanbanListPage-CSAnUfp3.js +1 -0
- package/web/assets/LaunchProjectPage-vNUoulGP.js +1 -0
- package/web/assets/LaunchServicesPage-CkWZxZ-v.js +1 -0
- package/web/assets/LogViewer-CW-bBjZF.js +3 -0
- package/web/assets/OrgKeyPrompt-CHoMbQhn.js +1 -0
- package/web/assets/ProcessControls-WS6BjS5H.js +1 -0
- package/web/assets/ProcessDetailPage-DEvZSNER.js +1 -0
- package/web/assets/{ProcessMetrics-DgnpYxXh.js → ProcessMetrics-DmhwoKrE.js} +2 -2
- package/web/assets/ProjectDetailPage-DanRrWb4.js +1 -0
- package/web/assets/ProjectListPage--l9HsQ9u.js +1 -0
- package/web/assets/RandomJobTestPage-BMatxc2G.js +2 -0
- package/web/assets/ScenarioEditorPage-D_rJyudb.js +14 -0
- package/web/assets/SendToChatDialog-D-pDvIjD.js +1 -0
- package/web/assets/ServiceControlsBar-BSU25sZZ.js +1 -0
- package/web/assets/ServiceDetailPage-B1oRlnId.js +2 -0
- package/web/assets/ServiceFormPage-D-DmI0fF.js +1 -0
- package/web/assets/SettingsPage-Bi72zdhb.js +2063 -0
- package/web/assets/{SettingsPage-BKcxRTQg.css → SettingsPage-D6-jpORY.css} +1 -1
- package/web/assets/UnlockPage-BtunlGnX.js +1 -0
- package/web/assets/VariablesTable-D3HpHlKY.js +1 -0
- package/web/assets/VaultMigrationPage-DrrFR1I-.js +1 -0
- package/web/assets/VersionBadge-DqcS34oJ.js +1 -0
- package/web/assets/WikiPage-BH8A-IB_.js +1 -0
- package/web/assets/WikiView-BasNZv2A.js +1 -0
- package/web/assets/add-scalar-classes-_PYrgVvq.js +387 -0
- package/web/assets/auth-errors-CwswzwYV.js +1 -0
- package/web/assets/{autocomplete-CFCpVybl.js → autocomplete-B-t_B26t.js} +3 -3
- package/web/assets/{basic-shape-diamond.svg-1nuR3NBd.js → basic-shape-diamond.svg-BFWFQIZl.js} +1 -1
- package/web/assets/{basic-shape-hexagon.svg-Be-pZrVb.js → basic-shape-hexagon.svg-CXsvZPky.js} +1 -1
- package/web/assets/{basic-shape-primary-circle-ellipse-round.svg-9-zHtg8B.js → basic-shape-primary-circle-ellipse-round.svg-BVg6oDS7.js} +1 -1
- package/web/assets/{basic-shape-primary-square-rectangle.svg-CRTdnGnv.js → basic-shape-primary-square-rectangle.svg-jiLWQSQV.js} +1 -1
- package/web/assets/{basic-shape-shield.svg-D9JPqU4l.js → basic-shape-shield.svg-CEybfnew.js} +1 -1
- package/web/assets/{browser-BdPlaU7e.js → browser-BfO6Jws3.js} +1 -1
- package/web/assets/{browser-B6dstq4l.js → browser-C7Qh0u0N.js} +1 -1
- package/web/assets/card-CWgnvlKH.js +32 -0
- package/web/assets/{checkbox-C_jx73WS.js → checkbox-CKzrAJQv.js} +3 -3
- package/web/assets/{chip-CQdqiw0W.js → chip-Bu5HllmO.js} +2 -2
- package/web/assets/{client-B_gN8XJ7.js → client-CM-a2QU7.js} +2 -2
- package/web/assets/cloud-chat.store-Cjbzzy1s.js +1 -0
- package/web/assets/cloud-session-Bs6siQ-Y.js +1 -0
- package/web/assets/column-pXrOxKc1.js +907 -0
- package/web/assets/{computer-device-desktop-monitor.svg-CGh6MdTp.js → computer-device-desktop-monitor.svg-DO-9EDqA.js} +1 -1
- package/web/assets/{computer-device-desktop.svg-CSnagCeX.js → computer-device-desktop.svg-U_EIk7jo.js} +1 -1
- package/web/assets/{computer-device-laptop.svg-BI_ZVURQ.js → computer-device-laptop.svg-nvbz6sjk.js} +1 -1
- package/web/assets/{computer-device-mobile-phone-android-samsung-back.svg-DM3ubsFX.js → computer-device-mobile-phone-android-samsung-back.svg-Cs34LunN.js} +1 -1
- package/web/assets/{computer-device-mobile-phone-android-samsung.svg-CzDW3wBE.js → computer-device-mobile-phone-android-samsung.svg-CDJgG-4L.js} +1 -1
- package/web/assets/{computer-device-mobile-phone-iphone-x-back.svg-B41GuLoA.js → computer-device-mobile-phone-iphone-x-back.svg-Du0pDh9L.js} +1 -1
- package/web/assets/{computer-device-mobile-phone-iphone-x.svg-Cd1cIH5V.js → computer-device-mobile-phone-iphone-x.svg-CUgXFf2r.js} +1 -1
- package/web/assets/{computer-device-mobile-tablet-touch.svg-rLuvpYQb.js → computer-device-mobile-tablet-touch.svg-CKdx-iUf.js} +1 -1
- package/web/assets/{computer-device-mobile-tablet.svg-DjBMAEhJ.js → computer-device-mobile-tablet.svg-BTvPzs5h.js} +1 -1
- package/web/assets/{computer-device-network-ethernet-cat6.svg-BeYjyXJh.js → computer-device-network-ethernet-cat6.svg-Cway8Bfw.js} +1 -1
- package/web/assets/{computer-device-network-lan-www.svg-DIR2TMun.js → computer-device-network-lan-www.svg-iuTZVSKE.js} +1 -1
- package/web/assets/{computer-device-network-wifi-connection.svg-DhI5VT5g.js → computer-device-network-wifi-connection.svg-DEemVGZq.js} +1 -1
- package/web/assets/{computer-device-network-wifi-router.svg-BO0V5pTE.js → computer-device-network-wifi-router.svg-BH2ASeRD.js} +1 -1
- package/web/assets/{cssMode-CdzA9M1t.js → cssMode-BHBdzmoS.js} +1 -1
- package/web/assets/{divider-Cijz8TDb.js → divider-kwmULX2U.js} +2 -2
- package/web/assets/{docx-preview-B8bBf__c.js → docx-preview-CxXKUxi7.js} +1 -1
- package/web/assets/{ecology-science-erlenmeyer-flask.svg-BuQW5h4a.js → ecology-science-erlenmeyer-flask.svg-9yLzulSu.js} +1 -1
- package/web/assets/{fieldset-DnZ-YI4V.js → fieldset-DE0sJS29.js} +2 -2
- package/web/assets/{freemarker2-Bp9032QT.js → freemarker2-CpcoAmNk.js} +1 -1
- package/web/assets/{handlebars-CdVx92lU.js → handlebars-D6DR-MoZ.js} +1 -1
- package/web/assets/{html-CkCZL9gx.js → html-Dx3QwrZR.js} +1 -1
- package/web/assets/{htmlMode-CW62-ie1.js → htmlMode-KSJ_gFzK.js} +1 -1
- package/web/assets/{image-flash-lightning.svg-DeCuJWFT.js → image-flash-lightning.svg-O9m-H64P.js} +1 -1
- package/web/assets/{image-picture-flower.svg-2ZzZbPy5.js → image-picture-flower.svg--rWVVuqu.js} +1 -1
- package/web/assets/index-B4UWj2Kz.css +1 -0
- package/web/assets/{index-DXGQcaBP.js → index-pLefgbhx.js} +284 -284
- package/web/assets/{interface-alert-exclamation-diamond.svg-D9th7CAp.js → interface-alert-exclamation-diamond.svg-CVeV0ecY.js} +1 -1
- package/web/assets/{interface-alert-exclamation-triangle-warning.svg-Dq9CWWQ3.js → interface-alert-exclamation-triangle-warning.svg-MIboYRzH.js} +1 -1
- package/web/assets/{interface-alert-information-circle.svg-FaG159xA.js → interface-alert-information-circle.svg-BlkHMxfH.js} +1 -1
- package/web/assets/{interface-award-crown.svg-B_-NjgUX.js → interface-award-crown.svg-C_Z3sLuP.js} +1 -1
- package/web/assets/{interface-bookmark.svg--yRnhMMQ.js → interface-bookmark-tag.svg-ClOZOhfV.js} +1 -1
- package/web/assets/{interface-bookmark-tag.svg-CKjw2WEc.js → interface-bookmark.svg-CVpTNe3u.js} +1 -1
- package/web/assets/{interface-calendar-date-one.svg-BCrvSyS9.js → interface-calendar-date-one.svg-BvG92AQi.js} +1 -1
- package/web/assets/{interface-content-book-open-pages.svg-BGIlAwyt.js → interface-content-book-open-pages.svg-aXXXkj7d.js} +1 -1
- package/web/assets/{interface-content-book-page.svg-DeLfK2OR.js → interface-content-book-page.svg-BE0nQJw_.js} +1 -1
- package/web/assets/{interface-content-file.svg-DUdYkC8Y.js → interface-content-file.svg-DYonyE7J.js} +1 -1
- package/web/assets/{interface-content-folder.svg-Ccx-IYD-.js → interface-content-folder.svg-DKc7vKrm.js} +1 -1
- package/web/assets/interface-content-folder.svg-FcNZvOjy.js +1 -0
- package/web/assets/{interface-copy-clipboard.svg-DIR-oOPG.js → interface-copy-clipboard.svg-sAixmHqz.js} +1 -1
- package/web/assets/{interface-edit-attachment.svg-B9CSuzCn.js → interface-edit-attachment.svg-DsSzCN6U.js} +1 -1
- package/web/assets/{interface-edit-binocular.svg-6WTqf985.js → interface-edit-binocular.svg-Bheco9Gv.js} +1 -1
- package/web/assets/{interface-edit-magic-wand.svg-BiB-GwoX.js → interface-edit-magic-wand.svg-C73TY2iI.js} +1 -1
- package/web/assets/{interface-edit-tool-paint-roller.svg-Dh0uneh4.js → interface-edit-tool-paint-roller.svg-D2OSqY3g.js} +1 -1
- package/web/assets/{interface-edit-tool-pencil.svg-C9HQOHce.js → interface-edit-tool-pencil.svg-CdY316cw.js} +1 -1
- package/web/assets/{interface-favorite-award.svg-Cj_1Qy9M.js → interface-favorite-award.svg-DfA-EhHI.js} +1 -1
- package/web/assets/{interface-favorite-flag.svg-BniwQaep.js → interface-favorite-flag.svg-BnmSaveE.js} +1 -1
- package/web/assets/{interface-favorite-heart.svg-CYJO0Ggb.js → interface-favorite-heart.svg-28i7lMzG.js} +1 -1
- package/web/assets/{interface-favorite-star.svg-CeFb37gO.js → interface-favorite-star.svg-u8xOEB4S.js} +1 -1
- package/web/assets/{interface-favorite-stars-sparkles.svg-CGeIzi4_.js → interface-favorite-stars-sparkles.svg-D5UHf8ar.js} +1 -1
- package/web/assets/{interface-hierarchy-flowchart.svg-Bd0q_y82.js → interface-hierarchy-flowchart.svg-BmjWTh4K.js} +1 -1
- package/web/assets/{interface-home-house.svg-BqSaaLzz.js → interface-home-house.svg-DJ5Ae65N.js} +1 -1
- package/web/assets/{interface-hyperlink.svg-DTtVOw8L.js → interface-hyperlink.svg-HdHKY_g6.js} +1 -1
- package/web/assets/{interface-lighting-brightness.svg-CVNVwF9w.js → interface-lighting-brightness.svg-BDELfqMb.js} +1 -1
- package/web/assets/{interface-lock-closed.svg-CKiXr8tU.js → interface-lock-closed.svg-BcDwDHkS.js} +1 -1
- package/web/assets/{interface-lock-open-unlock.svg-DU7sOeAE.js → interface-lock-open-unlock.svg-DNESCRC1.js} +1 -1
- package/web/assets/{interface-login-key.svg-tuWEt_tn.js → interface-login-key.svg-BOEot3yk.js} +1 -1
- package/web/assets/{interface-search.svg-CMDkllvr.js → interface-search.svg-CFsy7ant.js} +1 -1
- package/web/assets/{interface-setting-cog.svg-BRM5rRrt.js → interface-setting-cog.svg-B7Zk0iyr.js} +1 -1
- package/web/assets/{interface-share-megaphone-bullhorn.svg-BgC_SP2r.js → interface-share-megaphone-bullhorn.svg-DvG0IjMM.js} +1 -1
- package/web/assets/{interface-share-rocket.svg-C-XkIKh-.js → interface-share-rocket.svg-gvaeTB8G.js} +1 -1
- package/web/assets/{interface-share-satellite.svg-BKE0N18S.js → interface-share-satellite.svg-B5pXJ0kU.js} +1 -1
- package/web/assets/{interface-share-space-ship.svg-B8QuVt2r.js → interface-share-space-ship.svg-B0k0J-VQ.js} +1 -1
- package/web/assets/{interface-share.svg-bp3wJyGc.js → interface-share.svg-ZDsCvjRe.js} +1 -1
- package/web/assets/{interface-signal-square.svg-D8XH70Ue.js → interface-signal-square.svg-DBkJ1ire.js} +1 -1
- package/web/assets/{interface-time-clock-circle.svg-2DN-SbMe.js → interface-time-clock-circle.svg-BheUirew.js} +1 -1
- package/web/assets/{interface-time-hour-glass.svg-l0_r1j5d.js → interface-time-hour-glass.svg-iORrebF8.js} +1 -1
- package/web/assets/{interface-users-multiple.svg-RRlj9uIH.js → interface-users-multiple.svg-DMqedSUQ.js} +1 -1
- package/web/assets/{interface-weather-moon.svg-BmdUGPZe.js → interface-weather-moon.svg-DyPBHRw4.js} +1 -1
- package/web/assets/{javascript-edU55dwl.js → javascript-B1Y6a5s6.js} +1 -1
- package/web/assets/{jsonMode-CdE3cdJ3.js → jsonMode-Vj2pUgcF.js} +2 -2
- package/web/assets/{liquid-DCimUdoJ.js → liquid-lb0ImGEW.js} +1 -1
- package/web/assets/{lspLanguageFeatures-BKbLLFFb.js → lspLanguageFeatures-sh8ggL5N.js} +3 -3
- package/web/assets/{mail-chat-bubble-square.svg-VF1duUdO.js → mail-chat-bubble-square.svg-D_Lx6uBc.js} +1 -1
- package/web/assets/{mail-send-email-paper-airplane.svg-CdSpwALk.js → mail-send-email-paper-airplane.svg-B6FGKBZu.js} +1 -1
- package/web/assets/{mail-send-envelope.svg-DZVjejiO.js → mail-send-envelope.svg-Dtm4h_xw.js} +1 -1
- package/web/assets/{mdx-Hv_NSA7I.js → mdx-BbFPcqiD.js} +1 -1
- package/web/assets/{minus-CQh_GdvM.js → minus-Bxw18Xk6.js} +2 -2
- package/web/assets/modal-D-InjkVP.js +1 -0
- package/web/assets/modal-DLY8nKW-.js +5 -0
- package/web/assets/{money-cashier-receipt.svg-CWHgfD-x.js → money-cashier-receipt.svg-VpEb8Ewv.js} +1 -1
- package/web/assets/{money-currency-dollar-pay.svg-XPcb5aF0.js → money-currency-dollar-pay.svg-vo8FP9z4.js} +1 -1
- package/web/assets/{money-graph-arrow-increase.svg-DhPujH9x.js → money-graph-arrow-increase.svg-CNcLQGOf.js} +1 -1
- package/web/assets/{money-graph-bar-chart-increase.svg-BPOlqEDC.js → money-graph-bar-chart-increase.svg-BHAccBVA.js} +1 -1
- package/web/assets/multiselect-Crs0miOE.js +251 -0
- package/web/assets/{nature-ecology-leaf.svg-DZNsVAVa.js → nature-ecology-leaf.svg-0y4pBUlD.js} +1 -1
- package/web/assets/{password-DlR3LzLg.js → password-TyT4zuky.js} +4 -4
- package/web/assets/{phone-telephone.svg-B7vr6N8Z.js → phone-telephone.svg-BdWhV2vz.js} +1 -1
- package/web/assets/{plus-BDDTopKC.js → plus-4Z2iFfO2.js} +6 -6
- package/web/assets/process.store-DsGpeQnV.js +1 -0
- package/web/assets/{programming-bug.svg-DzlGVlKX.js → programming-bug.svg-DXDJVNeQ.js} +1 -1
- package/web/assets/{programming-cloud.svg-QHMLI7Md.js → programming-cloud.svg-qWtq6fXG.js} +1 -1
- package/web/assets/{programming-computer-database-server.svg-CjsFgYWn.js → programming-computer-database-server.svg-DnlJHfH5.js} +1 -1
- package/web/assets/{programming-computer-database.svg-vah-tira.js → programming-computer-database.svg-Bl73TDXc.js} +1 -1
- package/web/assets/{programming-module-four-layout.svg-Cpk_lyys.js → programming-module-four-layout.svg-CGkj0OdH.js} +1 -1
- package/web/assets/{programming-module-three.svg-DPfLaBLh.js → programming-module-three.svg-Bsk_vaHc.js} +1 -1
- package/web/assets/{programming-module.svg-X3KTc78b.js → programming-module.svg-D0Yng9wS.js} +1 -1
- package/web/assets/{programming-script-code.svg-Btq5sYVs.js → programming-script-code.svg-DTHMCNnF.js} +1 -1
- package/web/assets/{python-7W62NPSS.js → python-HImlMuYG.js} +1 -1
- package/web/assets/{razor-B-cp1S_J.js → razor-CTFKddzv.js} +1 -1
- package/web/assets/router-DTzqrUba.js +1 -0
- package/web/assets/settings-scope-CdxhnRbI.js +1 -0
- package/web/assets/{settings.store-DvVMtpRN.js → settings.store-CZ9jrl3S.js} +1 -1
- package/web/assets/{shopping-cart.svg-CrAdOGHj.js → shopping-cart.svg-Cx50mrFm.js} +1 -1
- package/web/assets/{shopping-gift-present.svg-D6lzCq46.js → shopping-gift-present.svg-DGOO6qNg.js} +1 -1
- package/web/assets/{shopping-shipping-box-parcel-package.svg-CvE0Deq6.js → shopping-shipping-box-parcel-package.svg-C96i4g3q.js} +1 -1
- package/web/assets/src-CGoYw9sv.css +1 -0
- package/web/assets/src-DRjytMUo.js +1 -0
- package/web/assets/{src-KOvSDAsA.js → src-DzMKqnGu.js} +13 -13
- package/web/assets/{tabpanel-DnsLaWEk.js → tabpanel-BW8I76pY.js} +3 -3
- package/web/assets/{tag-new-circle.svg-K7X6DVeY.js → tag-new-circle.svg-CV5DN0Db.js} +1 -1
- package/web/assets/{toggleswitch-BIl5_ynN.js → toggleswitch-D4Evlg2b.js} +2 -2
- package/web/assets/{travel-map-earth-globe.svg-CGHyssIY.js → travel-map-earth-globe.svg-CWvuO_Hb.js} +1 -1
- package/web/assets/trpc-BbC62Ymt.js +1 -0
- package/web/assets/{tsMode-Ch-xMRFe.js → tsMode-BqKPHyIc.js} +6 -6
- package/web/assets/{typescript-DaR3W1ZS.js → typescript-Bme6ns4u.js} +1 -1
- package/web/assets/useAiChatDrawer-BG8t5Thc.js +1 -0
- package/web/assets/{useCloudOrg-CZ_ZOAVy.css → useCloudOrg-CkiFdVCe.css} +1 -1
- package/web/assets/useCloudOrg-D2LTbk-G.js +572 -0
- package/web/assets/useProjectCatalog-nlAZuu5X.js +1 -0
- package/web/assets/{xml-DGHPJrA4.js → xml-zsImRVNc.js} +1 -1
- package/web/assets/{yaml-Bt6S3avV.js → yaml-VubzWJpP.js} +1 -1
- package/web/index.html +2 -2
- package/agent-manager-WEFGEFRT.js +0 -7
- package/chunk-6Y24763Y.js.map +0 -1
- package/src-QDQ5AT4C.js +0 -18369
- package/src-QDQ5AT4C.js.map +0 -1
- package/web/assets/AgentScalarChatInterface.vue-BlRhaFfk.js +0 -13
- package/web/assets/AgentTerminalPage-Bgz6cN1-.js +0 -1
- package/web/assets/AiChatPage-B2ZLntQV.js +0 -1
- package/web/assets/AppLayout-CwaLgkBP.js +0 -1
- package/web/assets/AuditPage-qw3Fsus7.js +0 -1
- package/web/assets/CloudAccountPage-BLOaK1nf.js +0 -1
- package/web/assets/CloudAccountPage-DTs4O-ua.js +0 -1
- package/web/assets/CloudMessagingPage-Cz1EQr91.js +0 -2
- package/web/assets/DashboardPage-B5HHqw9m.js +0 -1
- package/web/assets/DirectoryPickerDialog-DjpBVTYW.js +0 -1
- package/web/assets/EnvironmentForm-C9NbiSRH.js +0 -108
- package/web/assets/EnvironmentFormPage-DWbL-D3Z.js +0 -1
- package/web/assets/EnvironmentListPage-rdeHc34g.js +0 -1
- package/web/assets/KanbanBoardPage-CbHeyK8M.js +0 -4
- package/web/assets/KanbanListPage-xIqiBmTe.js +0 -1
- package/web/assets/LaunchProjectPage-D04ok35N.js +0 -1
- package/web/assets/LaunchServicesPage-Bkhxe7Ko.js +0 -1
- package/web/assets/LogViewer-BHlGDB4l.js +0 -3
- package/web/assets/OrgKeyPrompt-CnIY4K5s.js +0 -1
- package/web/assets/ProcessControls-BOkCKqGc.js +0 -1
- package/web/assets/ProcessDetailPage-BQCvKU0O.js +0 -1
- package/web/assets/ProjectDetailPage-Cjf4eBYS.js +0 -1
- package/web/assets/ProjectListPage-DQH1VC3U.js +0 -1
- package/web/assets/RandomJobTestPage-BMc4Qb-3.js +0 -2
- package/web/assets/ScenarioEditorPage-ClH3vxG9.js +0 -14
- package/web/assets/SendToChatDialog-5TxDHf96.js +0 -1
- package/web/assets/ServiceControlsBar-D0nrwVz7.js +0 -1
- package/web/assets/ServiceDetailPage-DLTZ2bjR.js +0 -2
- package/web/assets/ServiceFormPage-BoUHidR5.js +0 -1
- package/web/assets/SettingsPage-B6jM9W4h.js +0 -2063
- package/web/assets/UnlockPage-boIYvNg3.js +0 -1
- package/web/assets/VariablesTable-Eyqi7OJN.js +0 -1
- package/web/assets/VaultMigrationPage-CqKla89B.js +0 -1
- package/web/assets/VersionBadge-CnU3z5Rl.js +0 -1
- package/web/assets/WikiPage-Cbdxhr9G.js +0 -1
- package/web/assets/WikiView-BeWVPGaR.js +0 -1
- package/web/assets/add-scalar-classes-CG8bn7uZ.js +0 -387
- package/web/assets/auth-errors-CZibvB1j.js +0 -1
- package/web/assets/card-D8XCM024.js +0 -32
- package/web/assets/cloud-chat.store-BB8XNnvp.js +0 -1
- package/web/assets/cloud-session-f2IzlmYY.js +0 -1
- package/web/assets/column-BaNiShA4.js +0 -907
- package/web/assets/index-Cz0azMVO.css +0 -1
- package/web/assets/interface-content-folder.svg-C0OEvuKX.js +0 -1
- package/web/assets/modal-BO4YZ0Od.js +0 -1
- package/web/assets/modal-DUYd78Dq.js +0 -5
- package/web/assets/multiselect-DJ80nRN0.js +0 -251
- package/web/assets/process.store-Dq6j8zkt.js +0 -1
- package/web/assets/router-BzVLWWlf.js +0 -1
- package/web/assets/settings-scope-B2XSViBz.js +0 -1
- package/web/assets/src-DB1mNeZv.js +0 -1
- package/web/assets/src-DIxcn8ra.css +0 -1
- package/web/assets/trpc-BT4fWDYZ.js +0 -1
- package/web/assets/useAiChatDrawer-Bn5BBHr2.js +0 -1
- package/web/assets/useCloudOrg-CCXszfIX.js +0 -572
- package/web/assets/useProjectCatalog-CHgbaNsn.js +0 -1
- /package/{agent-manager-WEFGEFRT.js.map → agent-manager-QQPKKLKY.js.map} +0 -0
|
@@ -0,0 +1,1842 @@
|
|
|
1
|
+
import {
|
|
2
|
+
findThreadDb,
|
|
3
|
+
lastTurn,
|
|
4
|
+
threadMessages,
|
|
5
|
+
threadsStartedAfter
|
|
6
|
+
} from "./chunk-MD3VW4HQ.js";
|
|
7
|
+
import {
|
|
8
|
+
AmbiguousProjectError,
|
|
9
|
+
agentManager,
|
|
10
|
+
decryptValue,
|
|
11
|
+
encryptValue,
|
|
12
|
+
ensureProjectsLoaded,
|
|
13
|
+
environmentStore,
|
|
14
|
+
forgetProject,
|
|
15
|
+
getEncryptionKey,
|
|
16
|
+
hasChangedSinceRead,
|
|
17
|
+
projectKey,
|
|
18
|
+
refOfProjectAmong,
|
|
19
|
+
registerProjectLoader,
|
|
20
|
+
rememberFile,
|
|
21
|
+
rootOfProjectAmong,
|
|
22
|
+
rootOfProjectRef,
|
|
23
|
+
serviceStore,
|
|
24
|
+
setProjectServices,
|
|
25
|
+
setProjectVault,
|
|
26
|
+
wasRead
|
|
27
|
+
} from "./chunk-HX3ZIWHM.js";
|
|
28
|
+
import {
|
|
29
|
+
COMPANION_RELAY_CLOSE,
|
|
30
|
+
COMPANION_RELAY_OPEN,
|
|
31
|
+
ProjectSchema,
|
|
32
|
+
allVaultRoots,
|
|
33
|
+
ensureLocalVault,
|
|
34
|
+
extractCompanionRelay,
|
|
35
|
+
keyedFilePath,
|
|
36
|
+
legacyMachineFilePath,
|
|
37
|
+
listCloudVaults,
|
|
38
|
+
localVaultRoot,
|
|
39
|
+
machineFilePath,
|
|
40
|
+
machineRoot,
|
|
41
|
+
orgIdOfRoot,
|
|
42
|
+
vaultFor,
|
|
43
|
+
vaultIdOfRoot
|
|
44
|
+
} from "./chunk-3MJLS5UY.js";
|
|
45
|
+
|
|
46
|
+
// ../server/src/services/conversation-store.ts
|
|
47
|
+
import { randomUUID as randomUUID2 } from "crypto";
|
|
48
|
+
import { readFile as readFile3, writeFile as writeFile4, rename as rename3, mkdir as mkdir3, chmod as chmod3 } from "fs/promises";
|
|
49
|
+
import { dirname, join as join4 } from "path";
|
|
50
|
+
import { EventEmitter as EventEmitter2 } from "events";
|
|
51
|
+
|
|
52
|
+
// ../server/src/services/project-store.ts
|
|
53
|
+
import { randomUUID } from "crypto";
|
|
54
|
+
import { EventEmitter } from "events";
|
|
55
|
+
import { readFile as readFile2, writeFile as writeFile3, rename as rename2, mkdir as mkdir2, chmod as chmod2, rm } from "fs/promises";
|
|
56
|
+
import { join as join3 } from "path";
|
|
57
|
+
|
|
58
|
+
// ../server/src/utils/ensure-data-gitignore.ts
|
|
59
|
+
import { writeFile } from "fs/promises";
|
|
60
|
+
import { join } from "path";
|
|
61
|
+
var GITIGNORE_CONTENT = `
|
|
62
|
+
#################
|
|
63
|
+
# Do not edit this file #
|
|
64
|
+
#################
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
# Encryption key (contains secrets \u2014 never commit)
|
|
68
|
+
.encryption.key
|
|
69
|
+
.encryption.key.enc
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
# Key verification canary \u2014 MUST be committed so new team members can validate their encryption key
|
|
73
|
+
!.runeya-verify
|
|
74
|
+
|
|
75
|
+
# This machine's cloud session token (encrypted, and specific to this install)
|
|
76
|
+
cloud-session.json
|
|
77
|
+
|
|
78
|
+
# Backup in local only
|
|
79
|
+
backup-*
|
|
80
|
+
|
|
81
|
+
# Per-user environment overrides (never commit \u2014 each dev has their own)
|
|
82
|
+
environment-overrides.json
|
|
83
|
+
|
|
84
|
+
# Private user data (conversations, personal state \u2014 never commit)
|
|
85
|
+
*.private
|
|
86
|
+
*.private.json*
|
|
87
|
+
conversations/
|
|
88
|
+
conversations.private.json
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
# Runtime logs
|
|
93
|
+
*.log
|
|
94
|
+
*.log.*
|
|
95
|
+
`;
|
|
96
|
+
async function ensureDataGitignore(dataDir) {
|
|
97
|
+
const gitignorePath = join(dataDir, ".gitignore");
|
|
98
|
+
await writeFile(gitignorePath, GITIGNORE_CONTENT, "utf-8");
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// ../server/src/services/project-path-store.ts
|
|
102
|
+
import { readFile, writeFile as writeFile2, rename, mkdir, chmod, stat } from "fs/promises";
|
|
103
|
+
import { isAbsolute, join as join2, resolve } from "path";
|
|
104
|
+
var FILENAME = "project-paths.json";
|
|
105
|
+
var ProjectPathStore = class {
|
|
106
|
+
paths = null;
|
|
107
|
+
async load() {
|
|
108
|
+
if (this.paths) return this.paths;
|
|
109
|
+
try {
|
|
110
|
+
const raw = await readFile(machineFilePath(FILENAME), "utf-8");
|
|
111
|
+
const parsed = JSON.parse(raw);
|
|
112
|
+
this.paths = isPathMap(parsed) ? parsed : {};
|
|
113
|
+
} catch {
|
|
114
|
+
this.paths = {};
|
|
115
|
+
}
|
|
116
|
+
return this.paths;
|
|
117
|
+
}
|
|
118
|
+
async persist() {
|
|
119
|
+
await mkdir(machineRoot(), { recursive: true });
|
|
120
|
+
const filePath = machineFilePath(FILENAME);
|
|
121
|
+
const tmpPath = `${filePath}.tmp`;
|
|
122
|
+
await writeFile2(tmpPath, JSON.stringify(this.paths ?? {}, null, 2), "utf-8");
|
|
123
|
+
await rename(tmpPath, filePath);
|
|
124
|
+
await chmod(filePath, 384);
|
|
125
|
+
}
|
|
126
|
+
/** The directory chosen for this project, or null while it has none. */
|
|
127
|
+
async get(ref) {
|
|
128
|
+
const paths = await this.load();
|
|
129
|
+
return paths[projectKey(ref)] ?? paths[ref.id] ?? null;
|
|
130
|
+
}
|
|
131
|
+
async all() {
|
|
132
|
+
return { ...await this.load() };
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* The project already placed in this directory, if any other one is.
|
|
136
|
+
*
|
|
137
|
+
* Answers with the key as stored — the couple, or the bare id for an entry an
|
|
138
|
+
* older version wrote — which is enough to say "taken" and to name it.
|
|
139
|
+
*/
|
|
140
|
+
async projectAt(path, exclude) {
|
|
141
|
+
const target = resolve(path);
|
|
142
|
+
const excluded = exclude ? [projectKey(exclude), exclude.id] : [];
|
|
143
|
+
for (const [key, dir] of Object.entries(await this.load())) {
|
|
144
|
+
if (!excluded.includes(key) && resolve(dir) === target) return key;
|
|
145
|
+
}
|
|
146
|
+
return null;
|
|
147
|
+
}
|
|
148
|
+
async set(ref, path) {
|
|
149
|
+
if (!isAbsolute(path)) throw new Error("Le chemin du projet doit \xEAtre absolu");
|
|
150
|
+
const paths = await this.load();
|
|
151
|
+
delete paths[ref.id];
|
|
152
|
+
paths[projectKey(ref)] = resolve(path);
|
|
153
|
+
await this.persist();
|
|
154
|
+
}
|
|
155
|
+
/**
|
|
156
|
+
* Faire suivre le répertoire quand un projet change d'organisation.
|
|
157
|
+
*
|
|
158
|
+
* La clé est le couple `(organisation, projet)` : publier un projet local le
|
|
159
|
+
* fait passer de `local:<id>` à `<org>:<id>`, et l'entrée d'avant cesse alors
|
|
160
|
+
* de répondre — `get` ne retombe que sur l'identifiant nu, forme héritée, pas
|
|
161
|
+
* sur `local:<id>`. Le projet se retrouvait donc sans emplacement connu juste
|
|
162
|
+
* après avoir été publié, alors que ses sources n'avaient pas bougé d'un
|
|
163
|
+
* octet.
|
|
164
|
+
*/
|
|
165
|
+
async relocate(from, to) {
|
|
166
|
+
const paths = await this.load();
|
|
167
|
+
const previous = paths[projectKey(from)] ?? paths[from.id];
|
|
168
|
+
if (!previous) return;
|
|
169
|
+
delete paths[projectKey(from)];
|
|
170
|
+
delete paths[from.id];
|
|
171
|
+
paths[projectKey(to)] = previous;
|
|
172
|
+
await this.persist();
|
|
173
|
+
}
|
|
174
|
+
async clear(ref) {
|
|
175
|
+
const paths = await this.load();
|
|
176
|
+
const keys = [projectKey(ref), ref.id].filter((k) => k in paths);
|
|
177
|
+
if (keys.length === 0) return;
|
|
178
|
+
for (const key of keys) delete paths[key];
|
|
179
|
+
await this.persist();
|
|
180
|
+
}
|
|
181
|
+
/** Test seam: forget what was read from disk. */
|
|
182
|
+
reset() {
|
|
183
|
+
this.paths = null;
|
|
184
|
+
}
|
|
185
|
+
};
|
|
186
|
+
function isPathMap(value) {
|
|
187
|
+
return !!value && typeof value === "object" && !Array.isArray(value) && Object.values(value).every((v) => typeof v === "string");
|
|
188
|
+
}
|
|
189
|
+
async function holdsRuneyaProject(path) {
|
|
190
|
+
for (const candidate of ["project.json", "projects.json"]) {
|
|
191
|
+
try {
|
|
192
|
+
await stat(join2(path, ".runeya", candidate));
|
|
193
|
+
return true;
|
|
194
|
+
} catch {
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
return false;
|
|
198
|
+
}
|
|
199
|
+
var projectPathStore = new ProjectPathStore();
|
|
200
|
+
|
|
201
|
+
// ../server/src/services/project-store.ts
|
|
202
|
+
var FILENAME2 = "project.json";
|
|
203
|
+
var LEGACY_FILENAME = "projects.json";
|
|
204
|
+
function toDiskForm(project) {
|
|
205
|
+
const { activeEnvironmentId: _runtimeOnly, ...rest } = project;
|
|
206
|
+
return rest;
|
|
207
|
+
}
|
|
208
|
+
async function readLocalProject() {
|
|
209
|
+
const root = localVaultRoot();
|
|
210
|
+
let current = null;
|
|
211
|
+
try {
|
|
212
|
+
const raw = await readFile2(join3(root, FILENAME2), "utf-8");
|
|
213
|
+
const parsed = ProjectSchema.safeParse(JSON.parse(raw));
|
|
214
|
+
if (parsed.success) current = parsed.data;
|
|
215
|
+
} catch {
|
|
216
|
+
}
|
|
217
|
+
let legacy;
|
|
218
|
+
try {
|
|
219
|
+
legacy = JSON.parse(await readFile2(join3(root, LEGACY_FILENAME), "utf-8"));
|
|
220
|
+
} catch {
|
|
221
|
+
return { local: current, strayCloud: [] };
|
|
222
|
+
}
|
|
223
|
+
const strayCloud = legacy.filter((p) => p.orgId);
|
|
224
|
+
if (current) return { local: current, strayCloud };
|
|
225
|
+
const locals = legacy.filter((p) => !p.orgId);
|
|
226
|
+
if (locals.length > 1) {
|
|
227
|
+
const names = locals.map((p) => `"${p.name}" (${p.id})`).join(", ");
|
|
228
|
+
throw new Error(
|
|
229
|
+
`${join3(root, LEGACY_FILENAME)} holds ${locals.length} local projects: ${names}. A directory now holds a single local project \u2014 move the others to their own directory, or into an organization, then start again.`
|
|
230
|
+
);
|
|
231
|
+
}
|
|
232
|
+
return { local: locals[0] ?? null, strayCloud };
|
|
233
|
+
}
|
|
234
|
+
async function readCloudProjects() {
|
|
235
|
+
const found = [];
|
|
236
|
+
for (const { orgId, projectId, root } of await listCloudVaults()) {
|
|
237
|
+
try {
|
|
238
|
+
const raw = await readFile2(join3(root, FILENAME2), "utf-8");
|
|
239
|
+
const parsed = ProjectSchema.safeParse({ ...JSON.parse(raw), id: projectId, orgId });
|
|
240
|
+
if (parsed.success) found.push({ project: parsed.data, root });
|
|
241
|
+
} catch {
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
return found;
|
|
245
|
+
}
|
|
246
|
+
var LocalProjectExistsError = class extends Error {
|
|
247
|
+
constructor(projectName) {
|
|
248
|
+
super(
|
|
249
|
+
`This directory already holds the local project "${projectName}". A directory holds one local project: use another directory, or create the project in an organization.`
|
|
250
|
+
);
|
|
251
|
+
this.projectName = projectName;
|
|
252
|
+
this.name = "LocalProjectExistsError";
|
|
253
|
+
}
|
|
254
|
+
code = "LOCAL_PROJECT_EXISTS";
|
|
255
|
+
};
|
|
256
|
+
var ProjectStore = class {
|
|
257
|
+
/**
|
|
258
|
+
* Keyed by the (organization, project) couple, never by the project id alone:
|
|
259
|
+
* the same id can name a different project in each organization, and keying
|
|
260
|
+
* by id let the second one read from disk overwrite the first — then `save()`
|
|
261
|
+
* wrote that survivor back into both vaults.
|
|
262
|
+
*/
|
|
263
|
+
projects = /* @__PURE__ */ new Map();
|
|
264
|
+
loaded = false;
|
|
265
|
+
/** Emits `'saved'` after every persisted mutation, for the cloud sync bridge. */
|
|
266
|
+
events = new EventEmitter();
|
|
267
|
+
getFilePath(root = localVaultRoot()) {
|
|
268
|
+
return join3(root, FILENAME2);
|
|
269
|
+
}
|
|
270
|
+
async writeAtomic(filePath, data) {
|
|
271
|
+
const tmpPath = filePath + ".tmp";
|
|
272
|
+
await writeFile3(tmpPath, data, "utf-8");
|
|
273
|
+
await rename2(tmpPath, filePath);
|
|
274
|
+
await chmod2(filePath, 384);
|
|
275
|
+
}
|
|
276
|
+
/** Adopt a project read from disk, remembering which vault it came from. */
|
|
277
|
+
async absorb(p, root) {
|
|
278
|
+
const projectEnvs = await environmentStore.getByProjectId(p.id).catch(() => []);
|
|
279
|
+
const defaultEnv = projectEnvs.find((e) => e.default);
|
|
280
|
+
this.projects.set(projectKey(p), {
|
|
281
|
+
...p,
|
|
282
|
+
environmentIds: p.environmentIds ?? [],
|
|
283
|
+
groupOrder: p.groupOrder ?? [],
|
|
284
|
+
// Prefer the env marked default, fall back to first env
|
|
285
|
+
activeEnvironmentId: defaultEnv?.id ?? p.environmentIds?.[0]
|
|
286
|
+
});
|
|
287
|
+
setProjectVault(p, root);
|
|
288
|
+
setProjectServices(p, p.serviceIds ?? []);
|
|
289
|
+
}
|
|
290
|
+
/**
|
|
291
|
+
* Read every vault: the launch directory, then each organization's projects.
|
|
292
|
+
*
|
|
293
|
+
* Cloud vaults are discovered by walking `~/.runeya/orgs/*/projects/*` rather
|
|
294
|
+
* than from an index. An index is one more thing that can disagree with the
|
|
295
|
+
* disk — and a project present on disk but missing from it is exactly the
|
|
296
|
+
* "already here, still shown as downloadable" bug.
|
|
297
|
+
*/
|
|
298
|
+
async load() {
|
|
299
|
+
if (this.loaded) return;
|
|
300
|
+
this.loaded = true;
|
|
301
|
+
const { local, strayCloud } = await readLocalProject();
|
|
302
|
+
if (local) await this.absorb(local, localVaultRoot());
|
|
303
|
+
let migrated = false;
|
|
304
|
+
for (const project of strayCloud) {
|
|
305
|
+
try {
|
|
306
|
+
await this.absorb(project, vaultFor(project).root);
|
|
307
|
+
migrated = true;
|
|
308
|
+
} catch {
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
for (const { project, root } of await readCloudProjects()) {
|
|
312
|
+
await this.absorb(project, root);
|
|
313
|
+
}
|
|
314
|
+
if (migrated) {
|
|
315
|
+
await this.save();
|
|
316
|
+
await serviceStore.relocateAll();
|
|
317
|
+
await environmentStore.relocateAll();
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
/**
|
|
321
|
+
* Persist every vault this store knows about.
|
|
322
|
+
*
|
|
323
|
+
* Writing all of them rather than only the one that changed keeps a project
|
|
324
|
+
* that moved between vaults — a local project adopted by an org, a cloud
|
|
325
|
+
* project pulled for the first time — from lingering in the vault it left.
|
|
326
|
+
*/
|
|
327
|
+
async save() {
|
|
328
|
+
const locals = [];
|
|
329
|
+
const cloud = [];
|
|
330
|
+
for (const project of this.projects.values()) {
|
|
331
|
+
if (!project.orgId) locals.push(project);
|
|
332
|
+
else cloud.push({ project, root: vaultFor(project).root });
|
|
333
|
+
}
|
|
334
|
+
if (locals.length > 1) {
|
|
335
|
+
const names = locals.map((p) => `"${p.name}" (${p.id})`).join(", ");
|
|
336
|
+
throw new Error(
|
|
337
|
+
`Refusing to save ${locals.length} local projects in one directory: ${names}. A directory holds a single local project.`
|
|
338
|
+
);
|
|
339
|
+
}
|
|
340
|
+
const [local] = locals;
|
|
341
|
+
if (local) {
|
|
342
|
+
await ensureLocalVault();
|
|
343
|
+
await ensureDataGitignore(localVaultRoot());
|
|
344
|
+
await this.writeAtomic(this.getFilePath(), JSON.stringify(toDiskForm(local), null, 2));
|
|
345
|
+
} else {
|
|
346
|
+
await rm(this.getFilePath(), { force: true }).catch(() => {
|
|
347
|
+
});
|
|
348
|
+
}
|
|
349
|
+
await rename2(join3(localVaultRoot(), LEGACY_FILENAME), join3(localVaultRoot(), LEGACY_FILENAME + ".bak")).catch(() => {
|
|
350
|
+
});
|
|
351
|
+
for (const { project, root } of cloud) {
|
|
352
|
+
await mkdir2(root, { recursive: true });
|
|
353
|
+
await this.writeAtomic(this.getFilePath(root), JSON.stringify(toDiskForm(project), null, 2));
|
|
354
|
+
setProjectVault(project, root);
|
|
355
|
+
}
|
|
356
|
+
this.events.emit("saved");
|
|
357
|
+
}
|
|
358
|
+
/**
|
|
359
|
+
* The project a caller means, from a couple or from a bare id.
|
|
360
|
+
*
|
|
361
|
+
* A ref carrying its `orgId` — even an explicit `null` for a local project —
|
|
362
|
+
* names exactly one project and is looked up directly. A bare id is what a
|
|
363
|
+
* contained entity (an environment, a conversation) still carries, and it is
|
|
364
|
+
* resolved only while it answers for a single project: electing one of two
|
|
365
|
+
* would write into the wrong organization's vault.
|
|
366
|
+
*/
|
|
367
|
+
resolveKey(target) {
|
|
368
|
+
if (typeof target !== "string" && target.orgId !== void 0) return projectKey(target);
|
|
369
|
+
const id = typeof target === "string" ? target : target.id;
|
|
370
|
+
const matches = Array.from(this.projects.values()).filter((p) => p.id === id);
|
|
371
|
+
if (matches.length > 1) {
|
|
372
|
+
throw new AmbiguousProjectError(id, matches.map((p) => p.orgId ?? null));
|
|
373
|
+
}
|
|
374
|
+
return matches[0] ? projectKey(matches[0]) : null;
|
|
375
|
+
}
|
|
376
|
+
find(target) {
|
|
377
|
+
const key = this.resolveKey(target);
|
|
378
|
+
const project = key ? this.projects.get(key) : void 0;
|
|
379
|
+
return key && project ? { key, project } : null;
|
|
380
|
+
}
|
|
381
|
+
async list() {
|
|
382
|
+
await this.load();
|
|
383
|
+
return Array.from(this.projects.values());
|
|
384
|
+
}
|
|
385
|
+
async get(target) {
|
|
386
|
+
await this.load();
|
|
387
|
+
return this.find(target)?.project ?? null;
|
|
388
|
+
}
|
|
389
|
+
async create(input) {
|
|
390
|
+
await this.load();
|
|
391
|
+
if (!input.orgId) {
|
|
392
|
+
const existing = Array.from(this.projects.values()).find((p) => !p.orgId);
|
|
393
|
+
if (existing) {
|
|
394
|
+
throw new LocalProjectExistsError(existing.name);
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
const project = {
|
|
398
|
+
id: randomUUID(),
|
|
399
|
+
orgId: input.orgId ?? null,
|
|
400
|
+
name: input.name,
|
|
401
|
+
description: input.description ?? "",
|
|
402
|
+
serviceIds: input.serviceIds ?? [],
|
|
403
|
+
environmentIds: input.environmentIds ?? [],
|
|
404
|
+
activeEnvironmentId: input.activeEnvironmentId,
|
|
405
|
+
groupOrder: input.groupOrder ?? []
|
|
406
|
+
};
|
|
407
|
+
this.projects.set(projectKey(project), project);
|
|
408
|
+
setProjectVault(project, vaultFor(project).root);
|
|
409
|
+
setProjectServices(project, project.serviceIds);
|
|
410
|
+
await this.save();
|
|
411
|
+
return project;
|
|
412
|
+
}
|
|
413
|
+
/**
|
|
414
|
+
* `input.orgId` names which project is being updated, it does not move it:
|
|
415
|
+
* a project is born local or in an organization and stays there, so treating
|
|
416
|
+
* it as a new value would silently relocate a vault instead of editing it.
|
|
417
|
+
*/
|
|
418
|
+
async update(input) {
|
|
419
|
+
await this.load();
|
|
420
|
+
const found = this.find(input);
|
|
421
|
+
if (!found) return null;
|
|
422
|
+
const { key, project: existing } = found;
|
|
423
|
+
const updated = {
|
|
424
|
+
...existing,
|
|
425
|
+
...input.name !== void 0 && { name: input.name },
|
|
426
|
+
...input.description !== void 0 && { description: input.description },
|
|
427
|
+
...input.serviceIds !== void 0 && { serviceIds: input.serviceIds },
|
|
428
|
+
...input.environmentIds !== void 0 && { environmentIds: input.environmentIds },
|
|
429
|
+
...input.activeEnvironmentId !== void 0 && { activeEnvironmentId: input.activeEnvironmentId },
|
|
430
|
+
...input.groupOrder !== void 0 && { groupOrder: input.groupOrder }
|
|
431
|
+
};
|
|
432
|
+
this.projects.set(key, updated);
|
|
433
|
+
setProjectVault(updated, vaultFor(updated).root);
|
|
434
|
+
setProjectServices(updated, updated.serviceIds);
|
|
435
|
+
await this.save();
|
|
436
|
+
return updated;
|
|
437
|
+
}
|
|
438
|
+
/**
|
|
439
|
+
* Faire passer le projet local de ce dossier dans une organisation.
|
|
440
|
+
*
|
|
441
|
+
* Le seul déplacement de coffre qu'on s'autorise, et il ne va que dans ce
|
|
442
|
+
* sens : le projet quitte `<cwd>/.runeya` pour
|
|
443
|
+
* `~/.runeya/orgs/<org>/projects/<id>/`, en gardant son identifiant. Il n'y a
|
|
444
|
+
* donc jamais deux copies à réconcilier — c'est ce qui rendait « publier »
|
|
445
|
+
* dangereux tant que le projet restait aussi local.
|
|
446
|
+
*
|
|
447
|
+
* Refusé si l'organisation porte déjà ce projet : la copie d'ici écraserait
|
|
448
|
+
* celle du cloud, et ces deux histoires ne sont pas à nous de les fusionner.
|
|
449
|
+
*
|
|
450
|
+
* Ce qui suit le projet est écrit par les stores eux-mêmes : `save()` réécrit
|
|
451
|
+
* chaque coffre — le fichier local disparaît, celui de l'org apparaît — puis
|
|
452
|
+
* les contenus sont relogés depuis le registre, qui pointe désormais ailleurs.
|
|
453
|
+
*/
|
|
454
|
+
async moveToOrg(projectId, orgId) {
|
|
455
|
+
await this.load();
|
|
456
|
+
const local = this.projects.get(projectKey({ id: projectId, orgId: null }));
|
|
457
|
+
if (!local) throw new Error("PROJECT_NOT_LOCAL");
|
|
458
|
+
if (this.projects.get(projectKey({ id: projectId, orgId }))) {
|
|
459
|
+
throw new Error("PROJECT_ALREADY_IN_ORG");
|
|
460
|
+
}
|
|
461
|
+
const moved = { ...local, orgId };
|
|
462
|
+
this.projects.delete(projectKey(local));
|
|
463
|
+
forgetProject(local);
|
|
464
|
+
this.projects.set(projectKey(moved), moved);
|
|
465
|
+
setProjectVault(moved, vaultFor(moved).root);
|
|
466
|
+
setProjectServices(moved, moved.serviceIds ?? []);
|
|
467
|
+
await this.save();
|
|
468
|
+
await serviceStore.reassignOrg(moved.serviceIds ?? [], orgId);
|
|
469
|
+
await environmentStore.relocateAll();
|
|
470
|
+
return moved;
|
|
471
|
+
}
|
|
472
|
+
async delete(target) {
|
|
473
|
+
await this.load();
|
|
474
|
+
const found = this.find(target);
|
|
475
|
+
if (!found) return false;
|
|
476
|
+
const { key, project } = found;
|
|
477
|
+
this.projects.delete(key);
|
|
478
|
+
await this.save();
|
|
479
|
+
await this.discardVault(project);
|
|
480
|
+
await projectPathStore.clear(project);
|
|
481
|
+
return true;
|
|
482
|
+
}
|
|
483
|
+
/**
|
|
484
|
+
* Remove what a deleted cloud project left behind.
|
|
485
|
+
*
|
|
486
|
+
* Its vault is a directory of its own, so dropping the project means dropping
|
|
487
|
+
* the directory — otherwise `readCloudVaults` would resurrect it on the next
|
|
488
|
+
* load. Local projects share their vault with the directory's own files and
|
|
489
|
+
* are only removed from the list.
|
|
490
|
+
*/
|
|
491
|
+
async discardVault(project) {
|
|
492
|
+
forgetProject(project);
|
|
493
|
+
if (!project.orgId) return;
|
|
494
|
+
await rm(vaultFor(project).root, { recursive: true, force: true }).catch(() => {
|
|
495
|
+
});
|
|
496
|
+
}
|
|
497
|
+
/**
|
|
498
|
+
* Stop holding a project on this machine, leaving the doc alone.
|
|
499
|
+
*
|
|
500
|
+
* Memory only: the caller removes the vault itself. Its contents are dropped
|
|
501
|
+
* too, because a service or environment surviving in memory would be written
|
|
502
|
+
* back on the next save — and, its project being unknown by then, into the
|
|
503
|
+
* local vault.
|
|
504
|
+
*/
|
|
505
|
+
async forgetLocally(target) {
|
|
506
|
+
await this.load();
|
|
507
|
+
const found = this.find(target);
|
|
508
|
+
if (!found) return;
|
|
509
|
+
const { key, project } = found;
|
|
510
|
+
this.projects.delete(key);
|
|
511
|
+
forgetProject(project);
|
|
512
|
+
await serviceStore.forgetLocally(project.serviceIds ?? []);
|
|
513
|
+
await environmentStore.forgetByProject(project.id);
|
|
514
|
+
await projectPathStore.clear(project);
|
|
515
|
+
}
|
|
516
|
+
/** All projects linked to a given organization (for the sync bridge). */
|
|
517
|
+
async listByOrg(orgId) {
|
|
518
|
+
await this.load();
|
|
519
|
+
return Array.from(this.projects.values()).filter((p) => p.orgId === orgId);
|
|
520
|
+
}
|
|
521
|
+
/** Projects of an organization in their on-disk form (what the cloud mirrors). */
|
|
522
|
+
async listDiskFormByOrg(orgId) {
|
|
523
|
+
return (await this.listByOrg(orgId)).map(toDiskForm);
|
|
524
|
+
}
|
|
525
|
+
/**
|
|
526
|
+
* Apply a project received from the cloud, in its on-disk form.
|
|
527
|
+
*
|
|
528
|
+
* Validated rather than trusted: a peer on another version — or a tampered
|
|
529
|
+
* doc — must not be able to write arbitrary shapes into `projects.json`.
|
|
530
|
+
* `activeEnvironmentId` is runtime-only and stays whatever it is locally.
|
|
531
|
+
* Diff-guarded: an identical project neither saves nor emits (loop-safe).
|
|
532
|
+
* Returns whether a new local project was created, so the caller can
|
|
533
|
+
* bootstrap project-local state (e.g. a default environment).
|
|
534
|
+
*/
|
|
535
|
+
async applyRemoteUpsert(id, fields) {
|
|
536
|
+
await this.load();
|
|
537
|
+
const parsed = ProjectSchema.safeParse({ ...fields, id });
|
|
538
|
+
if (!parsed.success) {
|
|
539
|
+
console.error("[project-store] Rejected remote project %s:", id, parsed.error.message);
|
|
540
|
+
return null;
|
|
541
|
+
}
|
|
542
|
+
const incoming = toDiskForm(parsed.data);
|
|
543
|
+
const key = projectKey(parsed.data);
|
|
544
|
+
const existing = this.projects.get(key);
|
|
545
|
+
if (existing) {
|
|
546
|
+
if (JSON.stringify(toDiskForm(existing)) === JSON.stringify(incoming)) {
|
|
547
|
+
return { project: existing, created: false };
|
|
548
|
+
}
|
|
549
|
+
const updated = { ...incoming, activeEnvironmentId: existing.activeEnvironmentId };
|
|
550
|
+
this.projects.set(key, updated);
|
|
551
|
+
setProjectVault(updated, vaultFor(updated).root);
|
|
552
|
+
setProjectServices(updated, updated.serviceIds);
|
|
553
|
+
await this.save();
|
|
554
|
+
return { project: updated, created: false };
|
|
555
|
+
}
|
|
556
|
+
const project = { ...incoming, activeEnvironmentId: void 0 };
|
|
557
|
+
this.projects.set(key, project);
|
|
558
|
+
setProjectVault(project, vaultFor(project).root);
|
|
559
|
+
setProjectServices(project, project.serviceIds);
|
|
560
|
+
await this.save();
|
|
561
|
+
return { project, created: true };
|
|
562
|
+
}
|
|
563
|
+
/** Delete a project that was removed on another peer. */
|
|
564
|
+
async applyRemoteDelete(target) {
|
|
565
|
+
await this.load();
|
|
566
|
+
const found = this.find(target);
|
|
567
|
+
if (!found) return false;
|
|
568
|
+
const { key, project } = found;
|
|
569
|
+
this.projects.delete(key);
|
|
570
|
+
await this.save();
|
|
571
|
+
await this.discardVault(project);
|
|
572
|
+
return true;
|
|
573
|
+
}
|
|
574
|
+
async addService(target, serviceId) {
|
|
575
|
+
await this.load();
|
|
576
|
+
const found = this.find(target);
|
|
577
|
+
if (!found) return null;
|
|
578
|
+
const { key, project } = found;
|
|
579
|
+
if (project.serviceIds.includes(serviceId)) return project;
|
|
580
|
+
const updated = { ...project, serviceIds: [...project.serviceIds, serviceId] };
|
|
581
|
+
this.projects.set(key, updated);
|
|
582
|
+
setProjectServices(updated, updated.serviceIds);
|
|
583
|
+
await this.save();
|
|
584
|
+
return updated;
|
|
585
|
+
}
|
|
586
|
+
async removeService(target, serviceId) {
|
|
587
|
+
await this.load();
|
|
588
|
+
const found = this.find(target);
|
|
589
|
+
if (!found) return null;
|
|
590
|
+
const { key, project } = found;
|
|
591
|
+
const updated = { ...project, serviceIds: project.serviceIds.filter((id) => id !== serviceId) };
|
|
592
|
+
this.projects.set(key, updated);
|
|
593
|
+
setProjectServices(updated, updated.serviceIds);
|
|
594
|
+
await this.save();
|
|
595
|
+
return updated;
|
|
596
|
+
}
|
|
597
|
+
async addEnvironment(target, environmentId) {
|
|
598
|
+
await this.load();
|
|
599
|
+
const found = this.find(target);
|
|
600
|
+
if (!found) return null;
|
|
601
|
+
const { key, project } = found;
|
|
602
|
+
if (project.environmentIds?.includes(environmentId)) return project;
|
|
603
|
+
const updated = {
|
|
604
|
+
...project,
|
|
605
|
+
environmentIds: [...project.environmentIds ?? [], environmentId]
|
|
606
|
+
};
|
|
607
|
+
this.projects.set(key, updated);
|
|
608
|
+
await this.save();
|
|
609
|
+
return updated;
|
|
610
|
+
}
|
|
611
|
+
async removeEnvironment(target, environmentId) {
|
|
612
|
+
await this.load();
|
|
613
|
+
const found = this.find(target);
|
|
614
|
+
if (!found) return null;
|
|
615
|
+
const { key, project } = found;
|
|
616
|
+
const updated = {
|
|
617
|
+
...project,
|
|
618
|
+
environmentIds: (project.environmentIds ?? []).filter((id) => id !== environmentId),
|
|
619
|
+
// Clear activeEnvironmentId if it was the removed environment
|
|
620
|
+
...project.activeEnvironmentId === environmentId && { activeEnvironmentId: void 0 }
|
|
621
|
+
};
|
|
622
|
+
this.projects.set(key, updated);
|
|
623
|
+
await this.save();
|
|
624
|
+
return updated;
|
|
625
|
+
}
|
|
626
|
+
async setActiveEnvironment(target, environmentId) {
|
|
627
|
+
await this.load();
|
|
628
|
+
const found = this.find(target);
|
|
629
|
+
if (!found) return null;
|
|
630
|
+
const { key, project } = found;
|
|
631
|
+
const updated = {
|
|
632
|
+
...project,
|
|
633
|
+
activeEnvironmentId: environmentId
|
|
634
|
+
};
|
|
635
|
+
this.projects.set(key, updated);
|
|
636
|
+
return updated;
|
|
637
|
+
}
|
|
638
|
+
};
|
|
639
|
+
var projectStore = new ProjectStore();
|
|
640
|
+
registerProjectLoader(() => projectStore.list().then(() => void 0));
|
|
641
|
+
|
|
642
|
+
// ../server/src/services/conversation-store.ts
|
|
643
|
+
var FILENAME3 = "conversations.private.json";
|
|
644
|
+
async function localProjectId() {
|
|
645
|
+
const projects = await projectStore.list().catch(() => []);
|
|
646
|
+
return projects.find((p) => !p.orgId)?.id ?? null;
|
|
647
|
+
}
|
|
648
|
+
var ConversationStore = class {
|
|
649
|
+
conversations = /* @__PURE__ */ new Map();
|
|
650
|
+
/** Which vault each conversation was read from, until its project is known. */
|
|
651
|
+
roots = /* @__PURE__ */ new Map();
|
|
652
|
+
/**
|
|
653
|
+
* Les conversations qu'on n'a pas su déchiffrer, par coffre, sous leur forme
|
|
654
|
+
* de disque.
|
|
655
|
+
*
|
|
656
|
+
* Une entrée illisible était simplement ignorée. Tant qu'aucune de ses
|
|
657
|
+
* voisines ne se chargeait, le fichier n'était jamais réécrit et la donnée
|
|
658
|
+
* survivait par accident ; dès qu'une seule s'ouvrait, la sauvegarde suivante
|
|
659
|
+
* réécrivait le fichier avec elle seule — et effaçait les autres. Elles sont
|
|
660
|
+
* donc gardées ici, telles quelles, et réémises à l'identique.
|
|
661
|
+
*/
|
|
662
|
+
unreadable = /* @__PURE__ */ new Map();
|
|
663
|
+
loaded = false;
|
|
664
|
+
events = new EventEmitter2().setMaxListeners(0);
|
|
665
|
+
saveQueue = Promise.resolve();
|
|
666
|
+
snapshotConversation(conv) {
|
|
667
|
+
return structuredClone(conv);
|
|
668
|
+
}
|
|
669
|
+
/**
|
|
670
|
+
* Encrypted, so it lives beside the key that opens it — usually the machine
|
|
671
|
+
* root, but a launch directory owning its own key keeps both together.
|
|
672
|
+
*/
|
|
673
|
+
getFilePath() {
|
|
674
|
+
return keyedFilePath(FILENAME3);
|
|
675
|
+
}
|
|
676
|
+
/** Where older versions kept it — read only, so an older Runeya keeps working. */
|
|
677
|
+
getLegacyFilePath() {
|
|
678
|
+
return legacyMachineFilePath(FILENAME3);
|
|
679
|
+
}
|
|
680
|
+
/**
|
|
681
|
+
* Read the machine file, falling back to the launch directory of old.
|
|
682
|
+
*
|
|
683
|
+
* The legacy file is left in place: the next save writes the new location,
|
|
684
|
+
* and a directory still opened by an older Runeya keeps working meanwhile.
|
|
685
|
+
*/
|
|
686
|
+
async readFromDisk() {
|
|
687
|
+
try {
|
|
688
|
+
return await readFile3(await this.getFilePath(), "utf-8");
|
|
689
|
+
} catch {
|
|
690
|
+
return readFile3(this.getLegacyFilePath(), "utf-8");
|
|
691
|
+
}
|
|
692
|
+
}
|
|
693
|
+
async ensureDir() {
|
|
694
|
+
await mkdir3(dirname(await this.getFilePath()), { recursive: true });
|
|
695
|
+
}
|
|
696
|
+
encryptMessages(messages, vault) {
|
|
697
|
+
return encryptValue(JSON.stringify(messages), vault);
|
|
698
|
+
}
|
|
699
|
+
decryptMessages(encrypted, vault) {
|
|
700
|
+
const json = decryptValue(encrypted, vault);
|
|
701
|
+
return JSON.parse(json);
|
|
702
|
+
}
|
|
703
|
+
/**
|
|
704
|
+
* Refuser d'écrire dans le coffre d'une organisation sans nommer sa clé.
|
|
705
|
+
*
|
|
706
|
+
* `getEncryptionKey(undefined)` rend la clé machine, et c'est le bon défaut
|
|
707
|
+
* pour le coffre local. Appliqué à un fichier rangé sous `orgs/<org>/`, il
|
|
708
|
+
* produit un chiffré que ce coffre n'emploie pas — lisible par personne à la
|
|
709
|
+
* relecture, et impossible à distinguer d'une corruption.
|
|
710
|
+
*
|
|
711
|
+
* Le cas a été observé plusieurs fois sans qu'on parvienne à nommer le
|
|
712
|
+
* chemin fautif : les garde-fous en place traitent le coffre fermé, le
|
|
713
|
+
* coffre mal résolu et la copie périmée, aucun ne couvrait « coffre non
|
|
714
|
+
* précisé ». Ces écritures-là n'ont jamais fonctionné — elles échouaient en
|
|
715
|
+
* silence, ce qui est pire. On les saute désormais en le disant, avec la
|
|
716
|
+
* pile d'appels qui manquait pour remonter au chemin fautif.
|
|
717
|
+
*/
|
|
718
|
+
isVaultNamed(root, vault) {
|
|
719
|
+
const orgId = orgIdOfRoot(root);
|
|
720
|
+
if (!orgId || vault) return true;
|
|
721
|
+
console.error(
|
|
722
|
+
`[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.
|
|
723
|
+
${new Error("trace").stack}`
|
|
724
|
+
);
|
|
725
|
+
return false;
|
|
726
|
+
}
|
|
727
|
+
toDisk(conv, vault) {
|
|
728
|
+
return {
|
|
729
|
+
id: conv.id,
|
|
730
|
+
projectId: conv.projectId,
|
|
731
|
+
title: conv.title,
|
|
732
|
+
encryptedMessages: this.encryptMessages(conv.messages, vault),
|
|
733
|
+
isCli: conv.isCli,
|
|
734
|
+
runner: conv.runner,
|
|
735
|
+
providerId: conv.providerId,
|
|
736
|
+
codexThreadId: conv.codexThreadId,
|
|
737
|
+
contextUsage: conv.contextUsage,
|
|
738
|
+
workflowId: conv.workflowId,
|
|
739
|
+
workflowType: conv.workflowType,
|
|
740
|
+
kanbanBoardId: conv.kanbanBoardId,
|
|
741
|
+
kanbanCardId: conv.kanbanCardId,
|
|
742
|
+
companionEnabled: conv.companionEnabled,
|
|
743
|
+
companionRunner: conv.companionRunner,
|
|
744
|
+
companionConversationId: conv.companionConversationId,
|
|
745
|
+
isCompanion: conv.isCompanion,
|
|
746
|
+
archived: conv.archived,
|
|
747
|
+
createdAt: conv.createdAt,
|
|
748
|
+
updatedAt: conv.updatedAt
|
|
749
|
+
};
|
|
750
|
+
}
|
|
751
|
+
fromDisk(disk, vault) {
|
|
752
|
+
const workflowType = (disk.workflowType === "scenario" ? "scenario" : void 0) ?? (disk.scenarioId ? "scenario" : void 0);
|
|
753
|
+
return {
|
|
754
|
+
id: disk.id,
|
|
755
|
+
projectId: disk.projectId,
|
|
756
|
+
title: disk.title,
|
|
757
|
+
messages: this.decryptMessages(disk.encryptedMessages, vault),
|
|
758
|
+
isCli: disk.isCli ?? disk.isClaudeCode ?? false,
|
|
759
|
+
runner: disk.runner,
|
|
760
|
+
providerId: disk.providerId,
|
|
761
|
+
codexThreadId: disk.codexThreadId,
|
|
762
|
+
contextUsage: disk.contextUsage,
|
|
763
|
+
workflowId: disk.workflowId ?? disk.scenarioId,
|
|
764
|
+
workflowType,
|
|
765
|
+
kanbanBoardId: disk.kanbanBoardId,
|
|
766
|
+
kanbanCardId: disk.kanbanCardId,
|
|
767
|
+
companionEnabled: disk.companionEnabled,
|
|
768
|
+
companionRunner: disk.companionRunner,
|
|
769
|
+
companionConversationId: disk.companionConversationId,
|
|
770
|
+
isCompanion: disk.isCompanion,
|
|
771
|
+
archived: disk.archived,
|
|
772
|
+
createdAt: disk.createdAt,
|
|
773
|
+
updatedAt: disk.updatedAt
|
|
774
|
+
};
|
|
775
|
+
}
|
|
776
|
+
/**
|
|
777
|
+
* Read every project vault, then the homes older versions used.
|
|
778
|
+
*
|
|
779
|
+
* A conversation found in a legacy file is attributed to the local project of
|
|
780
|
+
* that directory — which is what it was about, back when one directory meant
|
|
781
|
+
* one project. With no local project to point at, it is left unattributed
|
|
782
|
+
* rather than filed under a project picked at random.
|
|
783
|
+
*/
|
|
784
|
+
async load() {
|
|
785
|
+
if (this.loaded) return;
|
|
786
|
+
if (!getEncryptionKey()) {
|
|
787
|
+
return;
|
|
788
|
+
}
|
|
789
|
+
this.loaded = true;
|
|
790
|
+
this.unreadable.clear();
|
|
791
|
+
await ensureProjectsLoaded();
|
|
792
|
+
const vaults = await allVaultRoots();
|
|
793
|
+
const legacyPaths = [this.getLegacyFilePath(), await keyedFilePath(FILENAME3)];
|
|
794
|
+
const sources = [
|
|
795
|
+
...legacyPaths.map((path) => ({ path, legacy: true })),
|
|
796
|
+
...vaults.map((root) => ({ path: join4(root, FILENAME3), legacy: false }))
|
|
797
|
+
];
|
|
798
|
+
for (const { path, legacy } of sources) {
|
|
799
|
+
let data;
|
|
800
|
+
try {
|
|
801
|
+
data = JSON.parse(await readFile3(path, "utf-8"));
|
|
802
|
+
} catch {
|
|
803
|
+
continue;
|
|
804
|
+
}
|
|
805
|
+
const inherited = legacy ? await localProjectId() : null;
|
|
806
|
+
const root = dirname(path);
|
|
807
|
+
await rememberFile(path);
|
|
808
|
+
for (const item of data) {
|
|
809
|
+
try {
|
|
810
|
+
const conv = this.fromDisk(item, vaultIdOfRoot(root));
|
|
811
|
+
if (!conv.projectId && inherited) conv.projectId = inherited;
|
|
812
|
+
this.conversations.set(item.id, conv);
|
|
813
|
+
this.roots.set(item.id, root);
|
|
814
|
+
} catch {
|
|
815
|
+
const bucket = this.unreadable.get(root) ?? [];
|
|
816
|
+
bucket.push(item);
|
|
817
|
+
this.unreadable.set(root, bucket);
|
|
818
|
+
}
|
|
819
|
+
}
|
|
820
|
+
}
|
|
821
|
+
}
|
|
822
|
+
/**
|
|
823
|
+
* Oublier ce qui a été lu, pour tout relire sous la clé courante.
|
|
824
|
+
*
|
|
825
|
+
* Appelé après l'adoption d'une clé d'organisation : le disque vient d'être
|
|
826
|
+
* re-chiffré sous les pieds du store, si bien que sa mémoire décrit un état
|
|
827
|
+
* qui n'existe plus. La laisser en place n'affichait pas seulement des
|
|
828
|
+
* comptes périmés — la sauvegarde suivante réécrivait l'ancien chiffré
|
|
829
|
+
* par-dessus le nouveau, et défaisait l'adoption.
|
|
830
|
+
*/
|
|
831
|
+
resetCache() {
|
|
832
|
+
this.conversations.clear();
|
|
833
|
+
this.roots.clear();
|
|
834
|
+
this.unreadable.clear();
|
|
835
|
+
this.loaded = false;
|
|
836
|
+
this.events.emit("changed", { type: "reloaded" });
|
|
837
|
+
}
|
|
838
|
+
/**
|
|
839
|
+
* Combien de conversations ce coffre garde sans savoir les lire, par
|
|
840
|
+
* organisation. La machine elle-même est rangée sous `null`.
|
|
841
|
+
*/
|
|
842
|
+
async unreadableByVault() {
|
|
843
|
+
await this.load();
|
|
844
|
+
const out = /* @__PURE__ */ new Map();
|
|
845
|
+
for (const [root, items] of this.unreadable) {
|
|
846
|
+
const vault = vaultIdOfRoot(root) ?? null;
|
|
847
|
+
out.set(vault, (out.get(vault) ?? 0) + items.length);
|
|
848
|
+
}
|
|
849
|
+
return out;
|
|
850
|
+
}
|
|
851
|
+
/**
|
|
852
|
+
* Write each vault that holds — or held — a conversation.
|
|
853
|
+
*
|
|
854
|
+
* A vault emptied by this save is still written, as an empty list: a
|
|
855
|
+
* conversation that found its project must not survive in the file it was
|
|
856
|
+
* read from.
|
|
857
|
+
*/
|
|
858
|
+
async saveInternal() {
|
|
859
|
+
if (!getEncryptionKey()) return;
|
|
860
|
+
const byRoot = /* @__PURE__ */ new Map();
|
|
861
|
+
for (const root of this.roots.values()) byRoot.set(root, []);
|
|
862
|
+
for (const conv of this.conversations.values()) {
|
|
863
|
+
const root = await this.rootFor(conv);
|
|
864
|
+
this.roots.set(conv.id, root);
|
|
865
|
+
const bucket = byRoot.get(root) ?? [];
|
|
866
|
+
const vault = vaultIdOfRoot(root);
|
|
867
|
+
if (!this.isVaultNamed(root, vault)) continue;
|
|
868
|
+
bucket.push(this.toDisk(conv, vault));
|
|
869
|
+
byRoot.set(root, bucket);
|
|
870
|
+
}
|
|
871
|
+
for (const [root, items] of this.unreadable) {
|
|
872
|
+
byRoot.set(root, [...items, ...byRoot.get(root) ?? []]);
|
|
873
|
+
}
|
|
874
|
+
const stale = /* @__PURE__ */ new Set();
|
|
875
|
+
for (const root of byRoot.keys()) {
|
|
876
|
+
const path = join4(root, FILENAME3);
|
|
877
|
+
if (wasRead(path) && await hasChangedSinceRead(path)) stale.add(root);
|
|
878
|
+
}
|
|
879
|
+
for (const [root, data] of byRoot) {
|
|
880
|
+
const filePath = join4(root, FILENAME3);
|
|
881
|
+
if (stale.has(root)) {
|
|
882
|
+
console.warn(
|
|
883
|
+
`[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.`
|
|
884
|
+
);
|
|
885
|
+
continue;
|
|
886
|
+
}
|
|
887
|
+
await mkdir3(root, { recursive: true });
|
|
888
|
+
const tmpPath = `${filePath}.tmp`;
|
|
889
|
+
await writeFile4(tmpPath, JSON.stringify(data, null, 2), "utf-8");
|
|
890
|
+
await rename3(tmpPath, filePath);
|
|
891
|
+
await chmod3(filePath, 384);
|
|
892
|
+
await rememberFile(filePath);
|
|
893
|
+
}
|
|
894
|
+
}
|
|
895
|
+
/**
|
|
896
|
+
* The vault a conversation is written to: its project's.
|
|
897
|
+
*
|
|
898
|
+
* One that names no project stays where it was read, falling back to the
|
|
899
|
+
* key's own directory — never lost for want of an owner.
|
|
900
|
+
*/
|
|
901
|
+
async rootFor(conv) {
|
|
902
|
+
const known = this.roots.get(conv.id) ?? null;
|
|
903
|
+
if (conv.projectId) return rootOfProjectAmong(conv.projectId, known);
|
|
904
|
+
return known ?? dirname(await keyedFilePath(FILENAME3));
|
|
905
|
+
}
|
|
906
|
+
/**
|
|
907
|
+
* Le projet d'une conversation, organisation comprise.
|
|
908
|
+
*
|
|
909
|
+
* Ce que la conversation ne dit pas d'elle-même : elle ne nomme que l'id.
|
|
910
|
+
* Le coffre d'où elle vient fournit l'organisation, et c'est ce couple qu'il
|
|
911
|
+
* faut pour retrouver le répertoire de travail du projet.
|
|
912
|
+
*/
|
|
913
|
+
async projectRefOf(conv) {
|
|
914
|
+
if (!conv.projectId) return null;
|
|
915
|
+
const known = this.roots.get(conv.id) ?? null;
|
|
916
|
+
const ref = refOfProjectAmong(conv.projectId, known);
|
|
917
|
+
return { id: ref.id, orgId: ref.orgId ?? null };
|
|
918
|
+
}
|
|
919
|
+
/**
|
|
920
|
+
* Réécrire chaque coffre, en déplaçant ce qui appartient désormais ailleurs.
|
|
921
|
+
*
|
|
922
|
+
* Appelé quand un projet a changé de coffre : ses conversations sont écrites
|
|
923
|
+
* dans le nouveau, et le fichier d'où elles viennent est réécrit sans elles.
|
|
924
|
+
*/
|
|
925
|
+
async relocateAll() {
|
|
926
|
+
await this.load();
|
|
927
|
+
await this.save();
|
|
928
|
+
}
|
|
929
|
+
async save() {
|
|
930
|
+
const write = this.saveQueue.then(() => this.saveInternal());
|
|
931
|
+
this.saveQueue = write.catch(() => {
|
|
932
|
+
});
|
|
933
|
+
return write;
|
|
934
|
+
}
|
|
935
|
+
async list() {
|
|
936
|
+
await this.load();
|
|
937
|
+
return Array.from(this.conversations.values()).sort((a, b) => b.updatedAt - a.updatedAt);
|
|
938
|
+
}
|
|
939
|
+
/**
|
|
940
|
+
* Les conversations d'un projet.
|
|
941
|
+
*
|
|
942
|
+
* Celle qui ne nomme aucun projet — écrite avant le champ, ou qu'une
|
|
943
|
+
* migration n'a pas su attribuer — n'est rendue que dans les projets de son
|
|
944
|
+
* propre coffre : elle reste atteignable là où elle a été écrite, sans se
|
|
945
|
+
* déverser dans toutes les organisations de la machine.
|
|
946
|
+
*/
|
|
947
|
+
async listForProject(ref) {
|
|
948
|
+
const belongs = await this.matcherForProject(ref);
|
|
949
|
+
return (await this.list()).filter(belongs);
|
|
950
|
+
}
|
|
951
|
+
/**
|
|
952
|
+
* Le test d'appartenance à un projet, préparé une fois.
|
|
953
|
+
*
|
|
954
|
+
* Le flux temps réel en a besoin autant que la liste : une conversation qui
|
|
955
|
+
* arrive par événement doit passer le même filtre, sans quoi elle se
|
|
956
|
+
* réinvite dans un projet qui ne l'a jamais demandée.
|
|
957
|
+
*/
|
|
958
|
+
async matcherForProject(ref) {
|
|
959
|
+
await this.load();
|
|
960
|
+
let projectRoot = null;
|
|
961
|
+
try {
|
|
962
|
+
projectRoot = ref.orgId === void 0 ? rootOfProjectAmong(ref.id, null) : rootOfProjectRef({ id: ref.id, orgId: ref.orgId ?? null });
|
|
963
|
+
} catch {
|
|
964
|
+
projectRoot = null;
|
|
965
|
+
}
|
|
966
|
+
return (conv) => {
|
|
967
|
+
if (conv.projectId) return conv.projectId === ref.id;
|
|
968
|
+
if (!projectRoot) return true;
|
|
969
|
+
return this.roots.get(conv.id) === projectRoot;
|
|
970
|
+
};
|
|
971
|
+
}
|
|
972
|
+
async get(id) {
|
|
973
|
+
await this.load();
|
|
974
|
+
return this.conversations.get(id) ?? null;
|
|
975
|
+
}
|
|
976
|
+
async create(title, isCli = false, workflowId, workflowType, runner, kanbanBoardId, kanbanCardId, providerId, projectId, orgId, extra) {
|
|
977
|
+
await this.load();
|
|
978
|
+
const now = Date.now();
|
|
979
|
+
const conv = {
|
|
980
|
+
id: randomUUID2(),
|
|
981
|
+
projectId,
|
|
982
|
+
title: title ?? "New conversation",
|
|
983
|
+
messages: [],
|
|
984
|
+
isCli,
|
|
985
|
+
runner,
|
|
986
|
+
providerId,
|
|
987
|
+
workflowId,
|
|
988
|
+
workflowType,
|
|
989
|
+
kanbanBoardId,
|
|
990
|
+
kanbanCardId,
|
|
991
|
+
isCompanion: extra?.isCompanion,
|
|
992
|
+
companionEnabled: extra?.companionEnabled,
|
|
993
|
+
companionRunner: extra?.companionRunner,
|
|
994
|
+
createdAt: now,
|
|
995
|
+
updatedAt: now
|
|
996
|
+
};
|
|
997
|
+
this.conversations.set(conv.id, conv);
|
|
998
|
+
if (projectId && orgId !== void 0) {
|
|
999
|
+
this.roots.set(conv.id, vaultFor({ id: projectId, orgId }).root);
|
|
1000
|
+
}
|
|
1001
|
+
await this.save();
|
|
1002
|
+
this.events.emit("changed", { type: "created", conversation: this.snapshotConversation(conv) });
|
|
1003
|
+
return conv;
|
|
1004
|
+
}
|
|
1005
|
+
async update(id, patch, options) {
|
|
1006
|
+
await this.load();
|
|
1007
|
+
const conv = this.conversations.get(id);
|
|
1008
|
+
if (!conv) return null;
|
|
1009
|
+
if (patch.title !== void 0) conv.title = patch.title;
|
|
1010
|
+
if (patch.messages !== void 0) conv.messages = patch.messages;
|
|
1011
|
+
if (patch.contextUsage !== void 0) conv.contextUsage = patch.contextUsage;
|
|
1012
|
+
if (patch.workflowId !== void 0) conv.workflowId = patch.workflowId;
|
|
1013
|
+
if (patch.archived !== void 0) conv.archived = patch.archived;
|
|
1014
|
+
if (patch.codexThreadId !== void 0) conv.codexThreadId = patch.codexThreadId;
|
|
1015
|
+
if (patch.companionEnabled !== void 0) conv.companionEnabled = patch.companionEnabled;
|
|
1016
|
+
if (patch.companionRunner !== void 0) conv.companionRunner = patch.companionRunner;
|
|
1017
|
+
if (patch.companionConversationId !== void 0) conv.companionConversationId = patch.companionConversationId;
|
|
1018
|
+
if (options?.touch !== false) conv.updatedAt = Date.now();
|
|
1019
|
+
await this.save();
|
|
1020
|
+
if (!options?.silent) {
|
|
1021
|
+
this.events.emit("changed", { type: "updated", conversation: this.snapshotConversation(conv) });
|
|
1022
|
+
}
|
|
1023
|
+
return conv;
|
|
1024
|
+
}
|
|
1025
|
+
/**
|
|
1026
|
+
* Synchronously update the last assistant message content in memory, then schedule a save.
|
|
1027
|
+
* Used by activeSessionRegistry.persistNow() to avoid the async get() → .then() race.
|
|
1028
|
+
*/
|
|
1029
|
+
updateLastAssistantContent(id, content, parts) {
|
|
1030
|
+
const conv = this.conversations.get(id);
|
|
1031
|
+
if (!conv) return;
|
|
1032
|
+
const lastMsg = conv.messages[conv.messages.length - 1];
|
|
1033
|
+
if (!lastMsg || lastMsg.role !== "assistant") return;
|
|
1034
|
+
lastMsg.content = content;
|
|
1035
|
+
if (parts !== void 0) lastMsg.parts = parts;
|
|
1036
|
+
conv.updatedAt = Date.now();
|
|
1037
|
+
this.save().catch(() => {
|
|
1038
|
+
});
|
|
1039
|
+
}
|
|
1040
|
+
async listByKanbanBoardId(boardId) {
|
|
1041
|
+
await this.load();
|
|
1042
|
+
return Array.from(this.conversations.values()).filter((c) => c.kanbanBoardId === boardId);
|
|
1043
|
+
}
|
|
1044
|
+
async listByKanbanCardId(cardId) {
|
|
1045
|
+
await this.load();
|
|
1046
|
+
return Array.from(this.conversations.values()).filter((c) => c.kanbanCardId === cardId);
|
|
1047
|
+
}
|
|
1048
|
+
async delete(id) {
|
|
1049
|
+
await this.load();
|
|
1050
|
+
const existed = this.conversations.delete(id);
|
|
1051
|
+
await this.save();
|
|
1052
|
+
if (existed) {
|
|
1053
|
+
this.events.emit("changed", { type: "deleted", id });
|
|
1054
|
+
}
|
|
1055
|
+
}
|
|
1056
|
+
onChanged(handler) {
|
|
1057
|
+
this.events.on("changed", handler);
|
|
1058
|
+
}
|
|
1059
|
+
offChanged(handler) {
|
|
1060
|
+
this.events.off("changed", handler);
|
|
1061
|
+
}
|
|
1062
|
+
};
|
|
1063
|
+
var conversationStore = new ConversationStore();
|
|
1064
|
+
|
|
1065
|
+
// ../server/src/services/turn-events.ts
|
|
1066
|
+
import { EventEmitter as EventEmitter3 } from "events";
|
|
1067
|
+
var bus = new EventEmitter3();
|
|
1068
|
+
bus.setMaxListeners(0);
|
|
1069
|
+
function emitTurnFinished(conversationId) {
|
|
1070
|
+
bus.emit("turn-finished", conversationId);
|
|
1071
|
+
}
|
|
1072
|
+
function onTurnFinished(listener) {
|
|
1073
|
+
bus.on("turn-finished", listener);
|
|
1074
|
+
return () => {
|
|
1075
|
+
bus.off("turn-finished", listener);
|
|
1076
|
+
};
|
|
1077
|
+
}
|
|
1078
|
+
|
|
1079
|
+
// ../server/src/services/claude-transcript-sync.ts
|
|
1080
|
+
import { readdir, access, readFile as readFile4 } from "fs/promises";
|
|
1081
|
+
import { watch } from "fs";
|
|
1082
|
+
import { homedir } from "os";
|
|
1083
|
+
import { join as join5 } from "path";
|
|
1084
|
+
var PROJECTS_DIR = join5(homedir(), ".claude", "projects");
|
|
1085
|
+
var DEFAULT_TITLES = /* @__PURE__ */ new Set(["", "New conversation", "Nouvelle conversation"]);
|
|
1086
|
+
async function findClaudeSessionFile(sessionId) {
|
|
1087
|
+
let entries;
|
|
1088
|
+
try {
|
|
1089
|
+
entries = await readdir(PROJECTS_DIR);
|
|
1090
|
+
} catch {
|
|
1091
|
+
return null;
|
|
1092
|
+
}
|
|
1093
|
+
const filename = `${sessionId}.jsonl`;
|
|
1094
|
+
for (const dir of entries) {
|
|
1095
|
+
const candidate = join5(PROJECTS_DIR, dir, filename);
|
|
1096
|
+
try {
|
|
1097
|
+
await access(candidate);
|
|
1098
|
+
return candidate;
|
|
1099
|
+
} catch {
|
|
1100
|
+
}
|
|
1101
|
+
}
|
|
1102
|
+
return null;
|
|
1103
|
+
}
|
|
1104
|
+
async function claudeSessionExists(sessionId) {
|
|
1105
|
+
return await findClaudeSessionFile(sessionId) !== null;
|
|
1106
|
+
}
|
|
1107
|
+
async function readClaudeTranscript(sessionId) {
|
|
1108
|
+
const path = await findClaudeSessionFile(sessionId);
|
|
1109
|
+
if (!path) return { found: false, path: null, entries: [] };
|
|
1110
|
+
const raw = await readFile4(path, "utf-8");
|
|
1111
|
+
const entries = [];
|
|
1112
|
+
for (const line of raw.split("\n")) {
|
|
1113
|
+
if (!line.trim()) continue;
|
|
1114
|
+
try {
|
|
1115
|
+
entries.push(JSON.parse(line));
|
|
1116
|
+
} catch {
|
|
1117
|
+
}
|
|
1118
|
+
}
|
|
1119
|
+
return { found: true, path, entries };
|
|
1120
|
+
}
|
|
1121
|
+
function claudeEntriesToMessages(entries) {
|
|
1122
|
+
const messages = [];
|
|
1123
|
+
const toolPartById = /* @__PURE__ */ new Map();
|
|
1124
|
+
for (const entry of entries) {
|
|
1125
|
+
if (entry.type !== "user" && entry.type !== "assistant") continue;
|
|
1126
|
+
const msg = entry.message;
|
|
1127
|
+
if (!msg) continue;
|
|
1128
|
+
const role = msg.role === "user" || msg.role === "assistant" ? msg.role : entry.type;
|
|
1129
|
+
const content = msg.content;
|
|
1130
|
+
const texts = [];
|
|
1131
|
+
const parts = [];
|
|
1132
|
+
let consumedByToolResult = false;
|
|
1133
|
+
if (typeof content === "string") {
|
|
1134
|
+
texts.push(content);
|
|
1135
|
+
} else if (Array.isArray(content)) {
|
|
1136
|
+
for (const block of content) {
|
|
1137
|
+
if (typeof block === "string") {
|
|
1138
|
+
texts.push(block);
|
|
1139
|
+
continue;
|
|
1140
|
+
}
|
|
1141
|
+
if (!block || typeof block !== "object") continue;
|
|
1142
|
+
const b = block;
|
|
1143
|
+
if (b.type === "text" && typeof b.text === "string") {
|
|
1144
|
+
texts.push(b.text);
|
|
1145
|
+
parts.push({ type: "text", text: b.text });
|
|
1146
|
+
} else if (b.type === "tool_use" && b.id) {
|
|
1147
|
+
const part = {
|
|
1148
|
+
type: "tool",
|
|
1149
|
+
id: b.id,
|
|
1150
|
+
name: b.name ?? "tool",
|
|
1151
|
+
input: b.input,
|
|
1152
|
+
pending: true
|
|
1153
|
+
};
|
|
1154
|
+
parts.push(part);
|
|
1155
|
+
toolPartById.set(b.id, part);
|
|
1156
|
+
} else if (b.type === "tool_result" && b.tool_use_id) {
|
|
1157
|
+
const target = toolPartById.get(b.tool_use_id);
|
|
1158
|
+
if (target) {
|
|
1159
|
+
target.result = b.content;
|
|
1160
|
+
target.isError = b.is_error;
|
|
1161
|
+
target.pending = false;
|
|
1162
|
+
}
|
|
1163
|
+
consumedByToolResult = true;
|
|
1164
|
+
}
|
|
1165
|
+
}
|
|
1166
|
+
}
|
|
1167
|
+
if (role === "user" && consumedByToolResult && texts.length === 0) continue;
|
|
1168
|
+
const last = messages[messages.length - 1];
|
|
1169
|
+
if (last && last.role === role) {
|
|
1170
|
+
const joinedText = [last.content, texts.join("\n").trim()].filter(Boolean).join("\n");
|
|
1171
|
+
last.content = joinedText;
|
|
1172
|
+
if (parts.length > 0) {
|
|
1173
|
+
last.parts = [...last.parts ?? [], ...parts];
|
|
1174
|
+
}
|
|
1175
|
+
continue;
|
|
1176
|
+
}
|
|
1177
|
+
messages.push({
|
|
1178
|
+
role,
|
|
1179
|
+
content: texts.join("\n").trim(),
|
|
1180
|
+
parts: parts.length > 0 ? parts : void 0
|
|
1181
|
+
});
|
|
1182
|
+
}
|
|
1183
|
+
return messages;
|
|
1184
|
+
}
|
|
1185
|
+
function lastFinishedTurnId(entries) {
|
|
1186
|
+
for (let i = entries.length - 1; i >= 0; i--) {
|
|
1187
|
+
const entry = entries[i];
|
|
1188
|
+
if (entry.type !== "assistant") continue;
|
|
1189
|
+
if (entry.message?.stop_reason !== "end_turn") return null;
|
|
1190
|
+
return entry.uuid ?? `${entry.timestamp ?? ""}#${i}`;
|
|
1191
|
+
}
|
|
1192
|
+
return null;
|
|
1193
|
+
}
|
|
1194
|
+
var announcedTurns = /* @__PURE__ */ new Map();
|
|
1195
|
+
function shouldAnnounceTurn(conversationId, turnId) {
|
|
1196
|
+
if (!turnId) return false;
|
|
1197
|
+
if (announcedTurns.get(conversationId) === turnId) return false;
|
|
1198
|
+
announcedTurns.set(conversationId, turnId);
|
|
1199
|
+
return true;
|
|
1200
|
+
}
|
|
1201
|
+
function forgetAnnouncedTurns(conversationId) {
|
|
1202
|
+
announcedTurns.delete(conversationId);
|
|
1203
|
+
}
|
|
1204
|
+
async function syncConversationFromClaudeTranscript(conversationId) {
|
|
1205
|
+
const conv = await conversationStore.get(conversationId);
|
|
1206
|
+
if (!conv) return false;
|
|
1207
|
+
const transcript = await readClaudeTranscript(conversationId);
|
|
1208
|
+
if (!transcript.found) return false;
|
|
1209
|
+
const messages = claudeEntriesToMessages(transcript.entries);
|
|
1210
|
+
const isDefaultTitle = !conv.title || DEFAULT_TITLES.has(conv.title);
|
|
1211
|
+
const sameLength = messages.length === conv.messages.length;
|
|
1212
|
+
const sameLastContent = sameLength && (messages.at(-1)?.content ?? "") === (conv.messages.at(-1)?.content ?? "");
|
|
1213
|
+
if (sameLength && sameLastContent && !isDefaultTitle) {
|
|
1214
|
+
return false;
|
|
1215
|
+
}
|
|
1216
|
+
const patch = { messages };
|
|
1217
|
+
if (isDefaultTitle) {
|
|
1218
|
+
const firstUserMsg = messages.find((m) => m.role === "user" && m.content.trim().length > 0);
|
|
1219
|
+
if (firstUserMsg) {
|
|
1220
|
+
const firstLine = firstUserMsg.content.split("\n")[0].trim();
|
|
1221
|
+
patch.title = firstLine.length > 80 ? firstLine.slice(0, 80) + "\u2026" : firstLine;
|
|
1222
|
+
}
|
|
1223
|
+
}
|
|
1224
|
+
await conversationStore.update(conversationId, patch, { touch: false });
|
|
1225
|
+
if (shouldAnnounceTurn(conversationId, lastFinishedTurnId(transcript.entries))) {
|
|
1226
|
+
emitTurnFinished(conversationId);
|
|
1227
|
+
}
|
|
1228
|
+
return true;
|
|
1229
|
+
}
|
|
1230
|
+
var lastTranscriptActivity = /* @__PURE__ */ new Map();
|
|
1231
|
+
function getLastTranscriptActivity(sessionId) {
|
|
1232
|
+
return lastTranscriptActivity.get(sessionId) ?? null;
|
|
1233
|
+
}
|
|
1234
|
+
var started = false;
|
|
1235
|
+
var watchers = [];
|
|
1236
|
+
var pendingResyncs = /* @__PURE__ */ new Map();
|
|
1237
|
+
var RESYNC_DEBOUNCE_MS = 250;
|
|
1238
|
+
function scheduleResync(sessionId) {
|
|
1239
|
+
const existing = pendingResyncs.get(sessionId);
|
|
1240
|
+
if (existing) clearTimeout(existing);
|
|
1241
|
+
pendingResyncs.set(
|
|
1242
|
+
sessionId,
|
|
1243
|
+
setTimeout(() => {
|
|
1244
|
+
pendingResyncs.delete(sessionId);
|
|
1245
|
+
syncConversationFromClaudeTranscript(sessionId).catch((err) => {
|
|
1246
|
+
console.warn("[claude-transcript-sync] resync failed for", sessionId, err);
|
|
1247
|
+
});
|
|
1248
|
+
}, RESYNC_DEBOUNCE_MS)
|
|
1249
|
+
);
|
|
1250
|
+
}
|
|
1251
|
+
function handleFsEvent(filename) {
|
|
1252
|
+
if (!filename) return;
|
|
1253
|
+
if (!filename.endsWith(".jsonl")) return;
|
|
1254
|
+
const base = filename.split("/").pop();
|
|
1255
|
+
const sessionId = base.slice(0, -".jsonl".length);
|
|
1256
|
+
if (!/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(sessionId)) return;
|
|
1257
|
+
lastTranscriptActivity.set(sessionId, Date.now());
|
|
1258
|
+
scheduleResync(sessionId);
|
|
1259
|
+
}
|
|
1260
|
+
async function startClaudeTranscriptWatcher() {
|
|
1261
|
+
if (started) return;
|
|
1262
|
+
started = true;
|
|
1263
|
+
let topLevel;
|
|
1264
|
+
try {
|
|
1265
|
+
topLevel = await readdir(PROJECTS_DIR);
|
|
1266
|
+
} catch {
|
|
1267
|
+
return;
|
|
1268
|
+
}
|
|
1269
|
+
try {
|
|
1270
|
+
const w = watch(PROJECTS_DIR, { recursive: true }, (_event, filename) => {
|
|
1271
|
+
handleFsEvent(filename ? filename.toString() : null);
|
|
1272
|
+
});
|
|
1273
|
+
w.on("error", (err) => console.warn("[claude-transcript-sync] watcher error:", err));
|
|
1274
|
+
watchers.push(w);
|
|
1275
|
+
return;
|
|
1276
|
+
} catch {
|
|
1277
|
+
}
|
|
1278
|
+
for (const dir of topLevel) {
|
|
1279
|
+
try {
|
|
1280
|
+
const w = watch(join5(PROJECTS_DIR, dir), (_event, filename) => {
|
|
1281
|
+
handleFsEvent(filename ? filename.toString() : null);
|
|
1282
|
+
});
|
|
1283
|
+
w.on("error", () => {
|
|
1284
|
+
});
|
|
1285
|
+
watchers.push(w);
|
|
1286
|
+
} catch {
|
|
1287
|
+
}
|
|
1288
|
+
}
|
|
1289
|
+
}
|
|
1290
|
+
function stopClaudeTranscriptWatcher() {
|
|
1291
|
+
for (const w of watchers) {
|
|
1292
|
+
try {
|
|
1293
|
+
w.close();
|
|
1294
|
+
} catch {
|
|
1295
|
+
}
|
|
1296
|
+
}
|
|
1297
|
+
watchers.length = 0;
|
|
1298
|
+
for (const t of pendingResyncs.values()) clearTimeout(t);
|
|
1299
|
+
pendingResyncs.clear();
|
|
1300
|
+
started = false;
|
|
1301
|
+
}
|
|
1302
|
+
|
|
1303
|
+
// ../server/src/services/codex-transcript-sync.ts
|
|
1304
|
+
import { watch as watch2 } from "fs";
|
|
1305
|
+
import { dirname as dirname2 } from "path";
|
|
1306
|
+
var announced = /* @__PURE__ */ new Map();
|
|
1307
|
+
function forgetCodexTurns(conversationId) {
|
|
1308
|
+
announced.delete(conversationId);
|
|
1309
|
+
}
|
|
1310
|
+
async function syncConversationFromCodexTranscript(conversationId) {
|
|
1311
|
+
const conv = await conversationStore.get(conversationId);
|
|
1312
|
+
if (!conv?.codexThreadId) return false;
|
|
1313
|
+
const messages = threadMessages(conv.codexThreadId);
|
|
1314
|
+
if (messages.length === 0) return false;
|
|
1315
|
+
const same = messages.length === conv.messages.length && (messages.at(-1)?.content ?? "") === (conv.messages.at(-1)?.content ?? "");
|
|
1316
|
+
if (!same) await conversationStore.update(conversationId, { messages }, { touch: false });
|
|
1317
|
+
const turn = lastTurn(conv.codexThreadId);
|
|
1318
|
+
if (turn?.completedAt && announced.get(conversationId) !== turn.turnId) {
|
|
1319
|
+
announced.set(conversationId, turn.turnId);
|
|
1320
|
+
emitTurnFinished(conversationId);
|
|
1321
|
+
}
|
|
1322
|
+
return !same;
|
|
1323
|
+
}
|
|
1324
|
+
var watcher = null;
|
|
1325
|
+
var debounce = null;
|
|
1326
|
+
async function resyncAll() {
|
|
1327
|
+
const convs = await conversationStore.list();
|
|
1328
|
+
await bindPendingSpawns(convs.map((c) => c.codexThreadId).filter((x) => !!x));
|
|
1329
|
+
for (const c of await conversationStore.list()) {
|
|
1330
|
+
if (!c.codexThreadId) continue;
|
|
1331
|
+
await syncConversationFromCodexTranscript(c.id).catch(() => false);
|
|
1332
|
+
}
|
|
1333
|
+
}
|
|
1334
|
+
var BIND_MARGIN_SECONDS = 10;
|
|
1335
|
+
async function bindPendingSpawns(boundThreadIds) {
|
|
1336
|
+
for (const [spawnId, spawn] of spawnConversations) {
|
|
1337
|
+
const conv = await conversationStore.get(spawn.conversationId);
|
|
1338
|
+
if (!conv) {
|
|
1339
|
+
spawnConversations.delete(spawnId);
|
|
1340
|
+
continue;
|
|
1341
|
+
}
|
|
1342
|
+
if (conv.codexThreadId) {
|
|
1343
|
+
spawnConversations.delete(spawnId);
|
|
1344
|
+
continue;
|
|
1345
|
+
}
|
|
1346
|
+
if (conv.runner !== "codex") continue;
|
|
1347
|
+
const candidates = threadsStartedAfter(spawn.startedAt - BIND_MARGIN_SECONDS).filter((id) => !boundThreadIds.includes(id));
|
|
1348
|
+
if (candidates.length !== 1) continue;
|
|
1349
|
+
await conversationStore.update(conv.id, { codexThreadId: candidates[0] });
|
|
1350
|
+
boundThreadIds.push(candidates[0]);
|
|
1351
|
+
spawnConversations.delete(spawnId);
|
|
1352
|
+
}
|
|
1353
|
+
}
|
|
1354
|
+
function startCodexTranscriptWatcher() {
|
|
1355
|
+
if (watcher) return;
|
|
1356
|
+
const db = findThreadDb();
|
|
1357
|
+
if (!db) return;
|
|
1358
|
+
try {
|
|
1359
|
+
watcher = watch2(dirname2(db), (_event, filename) => {
|
|
1360
|
+
if (!filename || !/^thread_history_\d+\.sqlite/.test(filename.toString())) return;
|
|
1361
|
+
if (debounce) clearTimeout(debounce);
|
|
1362
|
+
debounce = setTimeout(() => {
|
|
1363
|
+
debounce = null;
|
|
1364
|
+
void resyncAll();
|
|
1365
|
+
}, 400);
|
|
1366
|
+
});
|
|
1367
|
+
watcher.on("error", (err) => console.warn("[codex-thread-sync]", err));
|
|
1368
|
+
} catch {
|
|
1369
|
+
}
|
|
1370
|
+
}
|
|
1371
|
+
function stopCodexTranscriptWatcher() {
|
|
1372
|
+
watcher?.close();
|
|
1373
|
+
watcher = null;
|
|
1374
|
+
if (debounce) {
|
|
1375
|
+
clearTimeout(debounce);
|
|
1376
|
+
debounce = null;
|
|
1377
|
+
}
|
|
1378
|
+
tails.clear();
|
|
1379
|
+
spawnConversations.clear();
|
|
1380
|
+
}
|
|
1381
|
+
var CODEX_RESUME_RE = /codex resume[\s\S]{0,300}?\(([0-9a-fA-F]{8}-[0-9a-fA-F-]{27})\)/;
|
|
1382
|
+
var MAX_TAIL = 4e3;
|
|
1383
|
+
var tails = /* @__PURE__ */ new Map();
|
|
1384
|
+
var spawnConversations = /* @__PURE__ */ new Map();
|
|
1385
|
+
function noteSpawnConversation(spawnId, conversationId) {
|
|
1386
|
+
if (!conversationId) return;
|
|
1387
|
+
spawnConversations.set(spawnId, { conversationId, startedAt: Math.floor(Date.now() / 1e3) });
|
|
1388
|
+
}
|
|
1389
|
+
function codexSessionIdIn(text) {
|
|
1390
|
+
return CODEX_RESUME_RE.exec(text)?.[1] ?? null;
|
|
1391
|
+
}
|
|
1392
|
+
function observePtyChunk(spawnId, data) {
|
|
1393
|
+
const spawn = spawnConversations.get(spawnId);
|
|
1394
|
+
if (!spawn) return;
|
|
1395
|
+
const conversationId = spawn.conversationId;
|
|
1396
|
+
const tail = (tails.get(spawnId) ?? "") + data;
|
|
1397
|
+
const sessionId = codexSessionIdIn(tail);
|
|
1398
|
+
if (!sessionId) {
|
|
1399
|
+
tails.set(spawnId, tail.slice(-MAX_TAIL));
|
|
1400
|
+
return;
|
|
1401
|
+
}
|
|
1402
|
+
tails.delete(spawnId);
|
|
1403
|
+
spawnConversations.delete(spawnId);
|
|
1404
|
+
void (async () => {
|
|
1405
|
+
const conv = await conversationStore.get(conversationId);
|
|
1406
|
+
if (!conv || conv.codexThreadId === sessionId) return;
|
|
1407
|
+
await conversationStore.update(conversationId, { codexThreadId: sessionId });
|
|
1408
|
+
await syncConversationFromCodexTranscript(conversationId).catch(() => false);
|
|
1409
|
+
})();
|
|
1410
|
+
}
|
|
1411
|
+
function forgetSpawn(spawnId) {
|
|
1412
|
+
tails.delete(spawnId);
|
|
1413
|
+
spawnConversations.delete(spawnId);
|
|
1414
|
+
}
|
|
1415
|
+
|
|
1416
|
+
// ../server/src/services/auth-token.ts
|
|
1417
|
+
import { randomBytes } from "crypto";
|
|
1418
|
+
import { SignJWT, jwtVerify } from "jose";
|
|
1419
|
+
var jwtSecret = null;
|
|
1420
|
+
var jwtSecretGeneratedAt = 0;
|
|
1421
|
+
function generateJwtSecret() {
|
|
1422
|
+
jwtSecret = new Uint8Array(randomBytes(32));
|
|
1423
|
+
jwtSecretGeneratedAt = Math.floor(Date.now() / 1e3);
|
|
1424
|
+
}
|
|
1425
|
+
function getJwtSecretGeneratedAt() {
|
|
1426
|
+
return jwtSecretGeneratedAt;
|
|
1427
|
+
}
|
|
1428
|
+
function getJwtSecret() {
|
|
1429
|
+
if (!jwtSecret) throw new Error("JWT secret not yet generated");
|
|
1430
|
+
return jwtSecret;
|
|
1431
|
+
}
|
|
1432
|
+
async function signJwt() {
|
|
1433
|
+
const secret = getJwtSecret();
|
|
1434
|
+
return new SignJWT({ sub: "app" }).setProtectedHeader({ alg: "HS256" }).setIssuedAt().setExpirationTime("8h").sign(secret);
|
|
1435
|
+
}
|
|
1436
|
+
async function verifyJwt(token) {
|
|
1437
|
+
const secret = getJwtSecret();
|
|
1438
|
+
const { payload } = await jwtVerify(token, secret);
|
|
1439
|
+
return { exp: payload.exp, iat: payload.iat };
|
|
1440
|
+
}
|
|
1441
|
+
|
|
1442
|
+
// ../server/src/services/live-pty.ts
|
|
1443
|
+
async function findLivePty(sessionId) {
|
|
1444
|
+
for (const agent of agentManager.listAgents()) {
|
|
1445
|
+
const agentId = agent.config.id;
|
|
1446
|
+
try {
|
|
1447
|
+
const pty = await agentManager.agentQuery(
|
|
1448
|
+
agentId,
|
|
1449
|
+
"ai.findPtyBySession",
|
|
1450
|
+
{ sessionId }
|
|
1451
|
+
);
|
|
1452
|
+
if (pty?.status === "running") return { agentId, spawnId: pty.spawnId };
|
|
1453
|
+
} catch {
|
|
1454
|
+
}
|
|
1455
|
+
}
|
|
1456
|
+
return null;
|
|
1457
|
+
}
|
|
1458
|
+
|
|
1459
|
+
// ../server/src/services/companion.ts
|
|
1460
|
+
var MAX_CONSECUTIVE_RELAUNCHES = 3;
|
|
1461
|
+
var MAX_EXCHANGE_CHARS = 2e4;
|
|
1462
|
+
var VERDICT_NOTHING = "RAS";
|
|
1463
|
+
var RELAY_OPEN = COMPANION_RELAY_OPEN;
|
|
1464
|
+
var RELAY_CLOSE = COMPANION_RELAY_CLOSE;
|
|
1465
|
+
var relaunches = /* @__PURE__ */ new Map();
|
|
1466
|
+
function resetRelaunches(conversationId) {
|
|
1467
|
+
relaunches.delete(conversationId);
|
|
1468
|
+
}
|
|
1469
|
+
function takeRelaunchSlot(conversationId) {
|
|
1470
|
+
const used = relaunches.get(conversationId) ?? 0;
|
|
1471
|
+
if (used >= MAX_CONSECUTIVE_RELAUNCHES) {
|
|
1472
|
+
relaunches.delete(conversationId);
|
|
1473
|
+
return false;
|
|
1474
|
+
}
|
|
1475
|
+
relaunches.set(conversationId, used + 1);
|
|
1476
|
+
return true;
|
|
1477
|
+
}
|
|
1478
|
+
function relayableMessage(raw) {
|
|
1479
|
+
const text = (raw ?? "").trim();
|
|
1480
|
+
if (!text) return null;
|
|
1481
|
+
const inner = extractCompanionRelay(text);
|
|
1482
|
+
if (inner !== null) return isNothing(inner) ? null : inner;
|
|
1483
|
+
if (text.includes(RELAY_OPEN)) return null;
|
|
1484
|
+
return isNothing(text) ? null : text;
|
|
1485
|
+
}
|
|
1486
|
+
function isNothing(text) {
|
|
1487
|
+
const bare = (v) => v.replace(/^[\s"'«»([.,;:—-]+|[\s"'«»)\].,;:!—-]+$/g, "");
|
|
1488
|
+
if (bare(text).toUpperCase() === VERDICT_NOTHING) return true;
|
|
1489
|
+
const lines = text.split("\n").map((l) => l.trim()).filter(Boolean);
|
|
1490
|
+
const last = lines[lines.length - 1] ?? "";
|
|
1491
|
+
return bare(last).toUpperCase() === VERDICT_NOTHING;
|
|
1492
|
+
}
|
|
1493
|
+
var reviewedUpTo = /* @__PURE__ */ new Map();
|
|
1494
|
+
function forgetReviewProgress(conversationId) {
|
|
1495
|
+
reviewedUpTo.delete(conversationId);
|
|
1496
|
+
}
|
|
1497
|
+
function reviewWindow(messages, reviewed) {
|
|
1498
|
+
const list = messages ?? [];
|
|
1499
|
+
const count = list.length;
|
|
1500
|
+
let lastUser = -1;
|
|
1501
|
+
for (let i = count - 1; i >= 0; i--) {
|
|
1502
|
+
if (list[i].role === "user") {
|
|
1503
|
+
lastUser = i;
|
|
1504
|
+
break;
|
|
1505
|
+
}
|
|
1506
|
+
}
|
|
1507
|
+
const mark = reviewed === void 0 ? lastUser >= 0 ? lastUser : Math.max(0, count - 2) : Math.min(Math.max(0, reviewed), count);
|
|
1508
|
+
return {
|
|
1509
|
+
from: lastUser >= 0 ? Math.min(mark, lastUser) : mark,
|
|
1510
|
+
hasNew: count > mark
|
|
1511
|
+
};
|
|
1512
|
+
}
|
|
1513
|
+
function formatExchange(messages, from) {
|
|
1514
|
+
const text = (messages ?? []).slice(from).map((m) => [`[${m.role}]`, m.contextNote ? `(contexte : ${m.contextNote})` : "", m.content ?? ""].filter(Boolean).join("\n")).join("\n\n");
|
|
1515
|
+
return text.length > MAX_EXCHANGE_CHARS ? text.slice(-MAX_EXCHANGE_CHARS) : text;
|
|
1516
|
+
}
|
|
1517
|
+
function fullInstructions() {
|
|
1518
|
+
return [
|
|
1519
|
+
"Tu relis le travail d'un autre agent, dans la m\xEAme conversation que lui.",
|
|
1520
|
+
"Tu ne modifies rien : tu regardes, et tu d\xE9cides si tu as quelque chose \xE0 dire.",
|
|
1521
|
+
"Cela vaut aussi pour la carte kanban si la conversation en a une : tu peux la",
|
|
1522
|
+
"lire \u2014 elle dit ce qui a \xE9t\xE9 demand\xE9, donc ce que \xAB termin\xE9 \xBB veut dire ici \u2014",
|
|
1523
|
+
"mais tu ne la d\xE9places pas, tu ne la commentes pas, tu ne la modifies pas.",
|
|
1524
|
+
"Ton seul produit est le message que tu renvoies.",
|
|
1525
|
+
"",
|
|
1526
|
+
"Les images ne te sont PAS jointes : l'\xE9change n'en porte que le chemin, sous",
|
|
1527
|
+
"la forme [Image: source: /chemin/vers/le/fichier.png]. Quand on te demande",
|
|
1528
|
+
"d'en lire une, OUVRE ce chemin avec ton outil de lecture de fichiers \u2014 tu en",
|
|
1529
|
+
"as un, et il lit les images.",
|
|
1530
|
+
"N'invente JAMAIS le contenu d'une image que tu n'as pas ouverte : si le",
|
|
1531
|
+
"chemin manque ou que la lecture \xE9choue, dis-le. On te croirait.",
|
|
1532
|
+
"",
|
|
1533
|
+
"R\xC8GLE 1 \u2014 ON PEUT T'ADRESSER LA PAROLE, ET ALORS TU R\xC9PONDS.",
|
|
1534
|
+
"Si l'\xE9change ci-dessous te parle, te nomme, te pose une question ou te",
|
|
1535
|
+
"demande quelque chose : tu r\xE9ponds, TOUJOURS. M\xEAme si la demande te para\xEEt",
|
|
1536
|
+
"triviale, hors sujet, ou sans rapport avec de la relecture. Ce n'est pas une",
|
|
1537
|
+
"interruption, c'est une demande qui t'est faite. Ne r\xE9ponds JAMAIS " + VERDICT_NOTHING,
|
|
1538
|
+
"dans ce cas.",
|
|
1539
|
+
"",
|
|
1540
|
+
"R\xC8GLE 2 \u2014 sinon, tu relis, et tu ne parles que si tu as trouv\xE9 quelque chose.",
|
|
1541
|
+
"Ce qui vaut le plus souvent la peine :",
|
|
1542
|
+
" \xB7 une r\xE9gression \u2014 un comportement qui change sans que ce soit voulu ;",
|
|
1543
|
+
" \xB7 un effet de bord \u2014 ce que la modification touche sans le dire ;",
|
|
1544
|
+
" \xB7 de la duplication \u2014 du code qui existe d\xE9j\xE0 ailleurs dans le d\xE9p\xF4t ;",
|
|
1545
|
+
" \xB7 une affirmation non v\xE9rifi\xE9e pr\xE9sent\xE9e comme acquise.",
|
|
1546
|
+
"C'est l\xE0, et l\xE0 seulement, qu'il faut \xEAtre sobre : une remarque que personne",
|
|
1547
|
+
"n'a demand\xE9e interrompt quelqu'un au travail. Pointe un fichier et une ligne",
|
|
1548
|
+
"d\xE8s que tu le peux \u2014 une remarque qu'on peut v\xE9rifier en vaut dix qu'on ne",
|
|
1549
|
+
"peut que croire.",
|
|
1550
|
+
"",
|
|
1551
|
+
"Tu gardes le fil d'un tour \xE0 l'autre : ce qu'on te donne est ce que tu n'as",
|
|
1552
|
+
"PAS encore relu. Ne resignale pas ce que tu as d\xE9j\xE0 signal\xE9 \u2014 v\xE9rifie plut\xF4t",
|
|
1553
|
+
"si \xE7'a \xE9t\xE9 trait\xE9, et reviens dessus seulement si \xE7a ne l'a pas \xE9t\xE9.",
|
|
1554
|
+
"",
|
|
1555
|
+
"Le tour a-t-il modifi\xE9 du code ? S'il n'a rien chang\xE9 \u2014 une question, une",
|
|
1556
|
+
"explication, un affichage, un refus \u2014 n'inspecte PAS le d\xE9p\xF4t : il n'y a rien",
|
|
1557
|
+
"\xE0 y relire. S'il a modifi\xE9 quelque chose, va le lire toi-m\xEAme : `git diff`,",
|
|
1558
|
+
"`git status`, le contenu des fichiers touch\xE9s. Ne demande rien, cherche. Et",
|
|
1559
|
+
"ne juge que ce que CE tour a chang\xE9, pas ce qui tra\xEEnait avant.",
|
|
1560
|
+
"",
|
|
1561
|
+
"Ces consignes valent pour toute la session : elles ne te seront pas r\xE9p\xE9t\xE9es."
|
|
1562
|
+
].join("\n");
|
|
1563
|
+
}
|
|
1564
|
+
function responseProtocol() {
|
|
1565
|
+
return [
|
|
1566
|
+
"COMMENT R\xC9PONDRE :",
|
|
1567
|
+
" \xB7 Si tu n'as rien \xE0 dire \u2014 et seulement dans ce cas \u2014 r\xE9ponds exactement :",
|
|
1568
|
+
` ${VERDICT_NOTHING}`,
|
|
1569
|
+
" Rien ne sera transmis.",
|
|
1570
|
+
" \xB7 Sinon, \xE9cris ce que tu veux lui dire ENTRE CES DEUX BORNES :",
|
|
1571
|
+
` ${RELAY_OPEN}`,
|
|
1572
|
+
" ton message",
|
|
1573
|
+
` ${RELAY_CLOSE}`,
|
|
1574
|
+
" Seul ce qui est entre les bornes lui est transmis, tel quel. Ce que tu",
|
|
1575
|
+
" \xE9cris en dehors reste ton brouillon : tu peux y r\xE9fl\xE9chir \xE0 voix haute,",
|
|
1576
|
+
" dire ce que tu inspectes, personne ne le lira.",
|
|
1577
|
+
""
|
|
1578
|
+
].join("\n");
|
|
1579
|
+
}
|
|
1580
|
+
function reminder() {
|
|
1581
|
+
return [
|
|
1582
|
+
"Tour suivant \xE0 relire. M\xEAmes r\xE8gles.",
|
|
1583
|
+
`Rien \xE0 dire \u2192 ${VERDICT_NOTHING}. Sinon, ton message entre ${RELAY_OPEN} et ${RELAY_CLOSE}.`,
|
|
1584
|
+
"Si l'\xE9change t'adresse quelque chose, tu r\xE9ponds \u2014 jamais " + VERDICT_NOTHING + " dans ce cas."
|
|
1585
|
+
].join("\n");
|
|
1586
|
+
}
|
|
1587
|
+
function companionDefaultPrompt() {
|
|
1588
|
+
return fullInstructions();
|
|
1589
|
+
}
|
|
1590
|
+
function buildCompanionPrompt(exchange, isFirstTurn = true, custom) {
|
|
1591
|
+
const instructions = isFirstTurn ? [
|
|
1592
|
+
// Les consignes de l'utilisateur remplacent les nôtres, elles ne s'y
|
|
1593
|
+
// ajoutent pas : les empiler produirait deux jeux de règles
|
|
1594
|
+
// contradictoires.
|
|
1595
|
+
custom?.trim() || fullInstructions(),
|
|
1596
|
+
"",
|
|
1597
|
+
// Le protocole, lui, s'ajoute TOUJOURS : c'est par lui que Runeya lit le
|
|
1598
|
+
// verdict. Remplaçable, il rendrait le compagnon illisible.
|
|
1599
|
+
responseProtocol()
|
|
1600
|
+
].join("\n") : reminder();
|
|
1601
|
+
return [
|
|
1602
|
+
instructions,
|
|
1603
|
+
"",
|
|
1604
|
+
"--- Ce que tu n'as pas encore relu de la conversation ---",
|
|
1605
|
+
exchange || "(vide)"
|
|
1606
|
+
].join("\n");
|
|
1607
|
+
}
|
|
1608
|
+
var COMPANION_SIGNATURE = "[COMPAGNON]";
|
|
1609
|
+
function buildRelaunchMessage(message) {
|
|
1610
|
+
return `${COMPANION_SIGNATURE} ${message.trim()}`;
|
|
1611
|
+
}
|
|
1612
|
+
function companionSystemNote() {
|
|
1613
|
+
return [
|
|
1614
|
+
"## Relecture par un compagnon",
|
|
1615
|
+
"",
|
|
1616
|
+
"Cette conversation peut \xEAtre relue par un second agent, le \xAB compagnon \xBB.",
|
|
1617
|
+
"S'il est actif, il lit chaque tour que tu produis ainsi que les",
|
|
1618
|
+
"modifications du d\xE9p\xF4t.",
|
|
1619
|
+
"",
|
|
1620
|
+
`Quand il a quelque chose \xE0 te dire, son message t'arrive pr\xE9fix\xE9 par`,
|
|
1621
|
+
`${COMPANION_SIGNATURE}. Il peut te signaler une r\xE9gression, poser une`,
|
|
1622
|
+
"question, demander une pr\xE9cision, ou confirmer que c'est bon \u2014 il \xE9crit",
|
|
1623
|
+
"librement.",
|
|
1624
|
+
"",
|
|
1625
|
+
"Ce n'est PAS un sous-agent : tu ne le lances pas, tu ne peux pas lui parler.",
|
|
1626
|
+
"Il lit tes tours de lui-m\xEAme et t'\xE9crit quand il veut. Si on te demande de le",
|
|
1627
|
+
"solliciter, ne lance rien \u2014 dis que tu laisses le tour lui parvenir.",
|
|
1628
|
+
"",
|
|
1629
|
+
"Un tel message ne vient PAS de l'utilisateur : c'est une relecture, \xE0",
|
|
1630
|
+
"v\xE9rifier avant d'agir. Si elle est juste, corrige la cause ; si elle est",
|
|
1631
|
+
"fausse, dis-le en une phrase et poursuis. Ne t'excuse pas aupr\xE8s de",
|
|
1632
|
+
"l'utilisateur pour une remarque du compagnon, et ne la lui attribue pas.",
|
|
1633
|
+
"",
|
|
1634
|
+
"\xC9cris donc en sachant que ton travail peut \xEAtre relu : dis ce que tu as v\xE9rifi\xE9",
|
|
1635
|
+
"et ce que tu n'as pas v\xE9rifi\xE9, plut\xF4t que d'affirmer sans preuve."
|
|
1636
|
+
].join("\n");
|
|
1637
|
+
}
|
|
1638
|
+
function trace(conversationId, reason) {
|
|
1639
|
+
console.log(`[companion] ${conversationId.slice(0, 8)} \u2014 ${reason}`);
|
|
1640
|
+
}
|
|
1641
|
+
async function runCompanionForTurn(conversationId) {
|
|
1642
|
+
const conv = await conversationStore.get(conversationId);
|
|
1643
|
+
if (!conv) return trace(conversationId, "conversation introuvable");
|
|
1644
|
+
if (conv.isCompanion) return;
|
|
1645
|
+
if (!conv.companionEnabled) return;
|
|
1646
|
+
const messageCount = conv.messages?.length ?? 0;
|
|
1647
|
+
const { from, hasNew } = reviewWindow(conv.messages, reviewedUpTo.get(conversationId));
|
|
1648
|
+
if (!hasNew) {
|
|
1649
|
+
return trace(conversationId, `rien de neuf \xE0 relire (${messageCount} messages)`);
|
|
1650
|
+
}
|
|
1651
|
+
const { startCliSession } = await import("./chat-LPMGV2VS.js");
|
|
1652
|
+
const companionConv = await ensureCompanionConversation(conv);
|
|
1653
|
+
const isFirstTurn = (companionConv.messages?.length ?? 0) === 0;
|
|
1654
|
+
const exchange = formatExchange(conv.messages, from);
|
|
1655
|
+
const { prompt: customPrompt } = await companionSettings();
|
|
1656
|
+
const prompt = buildCompanionPrompt(exchange, isFirstTurn, customPrompt);
|
|
1657
|
+
const jwt = await signJwt();
|
|
1658
|
+
const environmentId = await activeEnvironmentOf(conv);
|
|
1659
|
+
const agentId = (await findLivePty(conv.id))?.agentId;
|
|
1660
|
+
try {
|
|
1661
|
+
const { done } = await startCliSession(
|
|
1662
|
+
conv.companionRunner ?? conv.runner,
|
|
1663
|
+
companionConv.id,
|
|
1664
|
+
prompt,
|
|
1665
|
+
{ jwt, environmentId, agentId }
|
|
1666
|
+
);
|
|
1667
|
+
await done;
|
|
1668
|
+
} catch (err) {
|
|
1669
|
+
return trace(conversationId, `relecture impossible : ${err.message}`);
|
|
1670
|
+
}
|
|
1671
|
+
reviewedUpTo.set(conversationId, messageCount);
|
|
1672
|
+
const raw = await readCompanionAnswer(companionConv.id, conv.companionRunner ?? conv.runner);
|
|
1673
|
+
const toRelay = relayableMessage(raw);
|
|
1674
|
+
trace(conversationId, `relecture termin\xE9e \u2014 r\xE9ponse de ${raw.length} caract\xE8res, ${toRelay ? "\xE0 transmettre" : "rien \xE0 transmettre"}`);
|
|
1675
|
+
if (!toRelay) {
|
|
1676
|
+
relaunches.delete(conversationId);
|
|
1677
|
+
forgetDelivery(conversationId);
|
|
1678
|
+
return;
|
|
1679
|
+
}
|
|
1680
|
+
if (!takeRelaunchSlot(conversationId)) {
|
|
1681
|
+
return trace(conversationId, "plafond de relances atteint \u2014 on rend la main");
|
|
1682
|
+
}
|
|
1683
|
+
await deliverToMainConversation(conv, buildRelaunchMessage(toRelay));
|
|
1684
|
+
}
|
|
1685
|
+
var PTY_SUBMIT_DELAY_MS = 120;
|
|
1686
|
+
function flattenForPty(message) {
|
|
1687
|
+
return message.split(/\n{2,}/).map((block) => block.replace(/\s*\n\s*/g, " ").trim()).filter(Boolean).join(" \xB7 ").replace(/\s{2,}/g, " ").trim();
|
|
1688
|
+
}
|
|
1689
|
+
var lastDelivery = /* @__PURE__ */ new Map();
|
|
1690
|
+
function lastCompanionDelivery(conversationId) {
|
|
1691
|
+
return lastDelivery.get(conversationId) ?? null;
|
|
1692
|
+
}
|
|
1693
|
+
function forgetDelivery(conversationId) {
|
|
1694
|
+
lastDelivery.delete(conversationId);
|
|
1695
|
+
}
|
|
1696
|
+
async function deliverToMainConversation(conv, message) {
|
|
1697
|
+
const route = await attemptDelivery(conv, message);
|
|
1698
|
+
lastDelivery.set(conv.id, { route, at: Date.now() });
|
|
1699
|
+
console.log(`[companion] livraison vers ${conv.id.slice(0, 8)} : ${route}`);
|
|
1700
|
+
return route;
|
|
1701
|
+
}
|
|
1702
|
+
async function attemptDelivery(conv, message) {
|
|
1703
|
+
if (await writeToLivePty(conv.id, message)) return "pty";
|
|
1704
|
+
try {
|
|
1705
|
+
const { startCliSession } = await import("./chat-LPMGV2VS.js");
|
|
1706
|
+
await startCliSession(conv.runner, conv.id, message, { jwt: await signJwt() });
|
|
1707
|
+
return "headless";
|
|
1708
|
+
} catch (err) {
|
|
1709
|
+
console.warn(`[companion] livraison impossible : ${err.message}`);
|
|
1710
|
+
return "failed";
|
|
1711
|
+
}
|
|
1712
|
+
}
|
|
1713
|
+
async function writeToLivePty(conversationId, message) {
|
|
1714
|
+
const pty = await findLivePty(conversationId);
|
|
1715
|
+
if (!pty) return false;
|
|
1716
|
+
const write = (data) => agentManager.agentMutation(pty.agentId, "ai.writePty", { spawnId: pty.spawnId, data });
|
|
1717
|
+
try {
|
|
1718
|
+
await write(flattenForPty(message));
|
|
1719
|
+
await new Promise((r) => setTimeout(r, PTY_SUBMIT_DELAY_MS));
|
|
1720
|
+
await write("\r");
|
|
1721
|
+
return true;
|
|
1722
|
+
} catch {
|
|
1723
|
+
return false;
|
|
1724
|
+
}
|
|
1725
|
+
}
|
|
1726
|
+
async function companionSettings() {
|
|
1727
|
+
try {
|
|
1728
|
+
const { settingsManager } = await import("./settings-manager-6FKHBLIV.js");
|
|
1729
|
+
const settings = await settingsManager.getSettings();
|
|
1730
|
+
return settings.companion ?? { enabledByDefault: false };
|
|
1731
|
+
} catch {
|
|
1732
|
+
return { enabledByDefault: false };
|
|
1733
|
+
}
|
|
1734
|
+
}
|
|
1735
|
+
async function activeEnvironmentOf(conv) {
|
|
1736
|
+
if (!conv.projectId) return void 0;
|
|
1737
|
+
try {
|
|
1738
|
+
const ref = await conversationStore.projectRefOf(conv);
|
|
1739
|
+
const project = ref ? await projectStore.get(ref) : null;
|
|
1740
|
+
return project?.activeEnvironmentId;
|
|
1741
|
+
} catch {
|
|
1742
|
+
return void 0;
|
|
1743
|
+
}
|
|
1744
|
+
}
|
|
1745
|
+
var ANSWER_TIMEOUT_MS = 15e3;
|
|
1746
|
+
var ANSWER_POLL_MS = 250;
|
|
1747
|
+
async function readCompanionAnswer(companionId, runner) {
|
|
1748
|
+
const deadline = Date.now() + ANSWER_TIMEOUT_MS;
|
|
1749
|
+
const sync = runner === "codex" ? syncConversationFromCodexTranscript : syncConversationFromClaudeTranscript;
|
|
1750
|
+
let last = "";
|
|
1751
|
+
while (Date.now() < deadline) {
|
|
1752
|
+
await sync(companionId).catch(() => false);
|
|
1753
|
+
const conv = await conversationStore.get(companionId);
|
|
1754
|
+
const msg = conv?.messages?.[conv.messages.length - 1];
|
|
1755
|
+
last = msg?.role === "assistant" ? msg.content ?? "" : "";
|
|
1756
|
+
if (last.trim()) return last;
|
|
1757
|
+
await new Promise((r) => setTimeout(r, ANSWER_POLL_MS));
|
|
1758
|
+
}
|
|
1759
|
+
return last;
|
|
1760
|
+
}
|
|
1761
|
+
async function ensureCompanionConversation(conv) {
|
|
1762
|
+
if (conv.companionConversationId) {
|
|
1763
|
+
const existing = await conversationStore.get(conv.companionConversationId);
|
|
1764
|
+
if (existing) return existing;
|
|
1765
|
+
}
|
|
1766
|
+
const companion = await conversationStore.create(
|
|
1767
|
+
`Compagnon \xB7 ${conv.title}`,
|
|
1768
|
+
true,
|
|
1769
|
+
void 0,
|
|
1770
|
+
void 0,
|
|
1771
|
+
conv.companionRunner ?? conv.runner,
|
|
1772
|
+
// La carte kanban de la conversation relue, s'il y en a une.
|
|
1773
|
+
//
|
|
1774
|
+
// `buildCliSystemContext` en tire la description de la carte, ses
|
|
1775
|
+
// commentaires et ses pièces jointes — c'est-à-dire ce qui a été DEMANDÉ.
|
|
1776
|
+
// Sans elle, le compagnon peut juger qu'un code est correct, jamais qu'un
|
|
1777
|
+
// travail est complet : il ignore ce qu'on attendait.
|
|
1778
|
+
conv.kanbanBoardId,
|
|
1779
|
+
conv.kanbanCardId,
|
|
1780
|
+
void 0,
|
|
1781
|
+
conv.projectId,
|
|
1782
|
+
void 0,
|
|
1783
|
+
{ isCompanion: true }
|
|
1784
|
+
);
|
|
1785
|
+
await conversationStore.update(conv.id, { companionConversationId: companion.id });
|
|
1786
|
+
return companion;
|
|
1787
|
+
}
|
|
1788
|
+
var unsubscribe = null;
|
|
1789
|
+
function initCompanion() {
|
|
1790
|
+
if (unsubscribe) return;
|
|
1791
|
+
unsubscribe = onTurnFinished((conversationId) => {
|
|
1792
|
+
runCompanionForTurn(conversationId).catch((err) => {
|
|
1793
|
+
console.error("[companion]", err.message);
|
|
1794
|
+
});
|
|
1795
|
+
});
|
|
1796
|
+
}
|
|
1797
|
+
|
|
1798
|
+
export {
|
|
1799
|
+
generateJwtSecret,
|
|
1800
|
+
getJwtSecretGeneratedAt,
|
|
1801
|
+
signJwt,
|
|
1802
|
+
verifyJwt,
|
|
1803
|
+
ensureDataGitignore,
|
|
1804
|
+
holdsRuneyaProject,
|
|
1805
|
+
projectPathStore,
|
|
1806
|
+
LocalProjectExistsError,
|
|
1807
|
+
projectStore,
|
|
1808
|
+
conversationStore,
|
|
1809
|
+
emitTurnFinished,
|
|
1810
|
+
findClaudeSessionFile,
|
|
1811
|
+
claudeSessionExists,
|
|
1812
|
+
readClaudeTranscript,
|
|
1813
|
+
forgetAnnouncedTurns,
|
|
1814
|
+
syncConversationFromClaudeTranscript,
|
|
1815
|
+
getLastTranscriptActivity,
|
|
1816
|
+
startClaudeTranscriptWatcher,
|
|
1817
|
+
stopClaudeTranscriptWatcher,
|
|
1818
|
+
forgetCodexTurns,
|
|
1819
|
+
startCodexTranscriptWatcher,
|
|
1820
|
+
stopCodexTranscriptWatcher,
|
|
1821
|
+
noteSpawnConversation,
|
|
1822
|
+
observePtyChunk,
|
|
1823
|
+
forgetSpawn,
|
|
1824
|
+
MAX_CONSECUTIVE_RELAUNCHES,
|
|
1825
|
+
resetRelaunches,
|
|
1826
|
+
takeRelaunchSlot,
|
|
1827
|
+
relayableMessage,
|
|
1828
|
+
forgetReviewProgress,
|
|
1829
|
+
reviewWindow,
|
|
1830
|
+
formatExchange,
|
|
1831
|
+
companionDefaultPrompt,
|
|
1832
|
+
buildCompanionPrompt,
|
|
1833
|
+
COMPANION_SIGNATURE,
|
|
1834
|
+
buildRelaunchMessage,
|
|
1835
|
+
companionSystemNote,
|
|
1836
|
+
flattenForPty,
|
|
1837
|
+
lastCompanionDelivery,
|
|
1838
|
+
forgetDelivery,
|
|
1839
|
+
deliverToMainConversation,
|
|
1840
|
+
initCompanion
|
|
1841
|
+
};
|
|
1842
|
+
//# sourceMappingURL=chunk-WAC56HS4.js.map
|