@runeya/runeya 2.0.146 → 2.0.147

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (171) hide show
  1. package/agent/index.js +10 -4
  2. package/agent/index.js.map +1 -1
  3. package/{agent-manager-R7BPQO6D.js → agent-manager-46C4NIQ2.js} +4 -2
  4. package/{chat-LPML22J5.js → chat-CMNDMXIP.js} +9 -5
  5. package/{chunk-SEPAURTQ.js → chunk-2NE52CL5.js} +156 -923
  6. package/chunk-2NE52CL5.js.map +1 -0
  7. package/chunk-4D3GCBPO.js +462 -0
  8. package/chunk-4D3GCBPO.js.map +1 -0
  9. package/{chunk-KV4GJKXU.js → chunk-5XG3X2TM.js} +27 -1479
  10. package/chunk-5XG3X2TM.js.map +1 -0
  11. package/chunk-7QXP4FXD.js +969 -0
  12. package/chunk-7QXP4FXD.js.map +1 -0
  13. package/chunk-DA2DZBTL.js +43 -0
  14. package/chunk-DA2DZBTL.js.map +1 -0
  15. package/chunk-DFPZ4MTI.js +1041 -0
  16. package/chunk-DFPZ4MTI.js.map +1 -0
  17. package/{chunk-UD42LACH.js → chunk-JNK6H5KH.js} +164 -1007
  18. package/chunk-JNK6H5KH.js.map +1 -0
  19. package/chunk-WDHLEO5S.js +809 -0
  20. package/chunk-WDHLEO5S.js.map +1 -0
  21. package/{companion-W3UMZWGH.js → companion-XKMNRIJB.js} +8 -3
  22. package/environment-resolver-ZDTF3MHI.js +18 -0
  23. package/environment-resolver-ZDTF3MHI.js.map +1 -0
  24. package/index.js +7 -7
  25. package/{install-gate-PVMGJDWE.js → install-gate-OJJEUU4E.js} +2 -2
  26. package/mcp-ready-TAPBAOZY.js +9 -0
  27. package/mcp-ready-TAPBAOZY.js.map +1 -0
  28. package/mcp-server/index.js +24 -0
  29. package/mcp-server/index.js.map +1 -1
  30. package/mcp-server/launch.d.ts +6 -0
  31. package/mcp-server/launch.js +2 -1
  32. package/mcp-server/launch.js.map +1 -1
  33. package/package.json +1 -1
  34. package/project-store-FLXTAD3J.js +23 -0
  35. package/project-store-FLXTAD3J.js.map +1 -0
  36. package/{src-LEDGW5KP.js → src-KWO3CKZU.js} +139 -59
  37. package/src-KWO3CKZU.js.map +1 -0
  38. package/web/assets/{AgentScalarChatInterface.vue-BYszptaB.js → AgentScalarChatInterface.vue-DKBinB_l.js} +2 -2
  39. package/web/assets/AgentTerminalPage-CixU7x6O.js +1 -0
  40. package/web/assets/AiChatModals-BvGUd-QD.css +1 -0
  41. package/web/assets/AiChatModals-C-hXImkO.js +4 -0
  42. package/web/assets/AppLayout-At9wk0sx.js +1 -0
  43. package/web/assets/{AppLayout-tsw5Ow0w.css → AppLayout-BlhwflMq.css} +1 -1
  44. package/web/assets/AuditPage-BkqbHxjY.js +1 -0
  45. package/web/assets/CloudAccountPage-D5C2SCS9.js +1 -0
  46. package/web/assets/CloudMessagingPage-Bi57GGHO.js +2 -0
  47. package/web/assets/DashboardPage-dEQop9ql.js +1 -0
  48. package/web/assets/DirectoryPickerDialog-DIMSeDhw.js +1 -0
  49. package/web/assets/{EditorIcon-CypGLV6w.js → EditorIcon-W3Szca4J.js} +1 -1
  50. package/web/assets/EnvironmentForm-C5emavNh.js +1 -0
  51. package/web/assets/EnvironmentFormPage-BIoy8fsW.js +1 -0
  52. package/web/assets/EnvironmentListPage-BDYeh7XF.js +1 -0
  53. package/web/assets/{FlagChip-R_T5qYpL.js → FlagChip-DPtIBCjQ.js} +1 -1
  54. package/web/assets/KanbanBoardPage-BlCcjBwV.js +4 -0
  55. package/web/assets/KanbanListPage-DbXONxfC.js +1 -0
  56. package/web/assets/LaunchProjectPage-Cg92hHwQ.js +1 -0
  57. package/web/assets/LaunchServicesPage-CO8gOB9D.js +1 -0
  58. package/web/assets/LaunchServicesPage-h5o3zLcy.css +1 -0
  59. package/web/assets/{LaunchShell-DUrSunB4.js → LaunchShell-CDb21ihZ.js} +1 -1
  60. package/web/assets/LogViewer-DsusKk2b.js +3 -0
  61. package/web/assets/OrgKeyPrompt-CX9K7r9I.js +1 -0
  62. package/web/assets/ProcessControls-B7hWVqiR.css +1 -0
  63. package/web/assets/ProcessControls-TEZutuxq.js +1 -0
  64. package/web/assets/ProcessDetailPage-Cj23FECq.js +1 -0
  65. package/web/assets/ProcessMetrics-1P6iE2Il.js +1 -0
  66. package/web/assets/ProjectCreateDialog-D3aMWKfw.js +1 -0
  67. package/web/assets/{ProjectLocationGate-DhMVdIjN.js → ProjectLocationGate-DU0Jj8Nw.js} +1 -1
  68. package/web/assets/ProjectPublishDialog-D7sNKei3.js +1 -0
  69. package/web/assets/{RandomJobTestPage-BUS2Ty6-.js → RandomJobTestPage-DQUMRHyf.js} +1 -1
  70. package/web/assets/{ScenarioEditorPage-vMoRt0Qn.css → ScenarioEditorPage-BSjSX4bw.css} +1 -1
  71. package/web/assets/ScenarioEditorPage-DvCdcY9m.js +14 -0
  72. package/web/assets/SendToChatDialog-BdWKjYmv.js +1 -0
  73. package/web/assets/ServiceControlsBar-B_J5m-Kc.js +1 -0
  74. package/web/assets/ServiceDetailPage-DhJcjFfY.js +2 -0
  75. package/web/assets/{ServiceDetailPage-COdaCZ6r.css → ServiceDetailPage-DnSf5cm9.css} +1 -1
  76. package/web/assets/ServiceFormPage-fp8NCHSv.js +1 -0
  77. package/web/assets/{SettingsPage-4pKUgKPm.css → SettingsPage-NH2fQap4.css} +1 -1
  78. package/web/assets/SettingsPage-s28BodWe.js +2086 -0
  79. package/web/assets/UnlockPage-BXXIPHW_.js +1 -0
  80. package/web/assets/VaultMigrationPage-BfNuKtkJ.js +1 -0
  81. package/web/assets/{VersionBadge-BmArk24p.js → VersionBadge-CW7czacg.js} +1 -1
  82. package/web/assets/{WikiPage-DADu8WUA.js → WikiPage-BiwpCM3E.js} +1 -1
  83. package/web/assets/WikiView-CMch_06Z.js +1 -0
  84. package/web/assets/{add-scalar-classes-dWEWPlFD.js → add-scalar-classes-Ckanjq1h.js} +14 -14
  85. package/web/assets/{agent.store-CKLAkF9a.js → agent.store-piokQ9Dt.js} +1 -1
  86. package/web/assets/{auth-Cg9BWTfK.js → auth-BT_jf-5e.js} +2 -2
  87. package/web/assets/cloud-chat.store-CDKh4tIy.js +1 -0
  88. package/web/assets/{cloud-client-D_zCiW0s.js → cloud-client-CzPdZLDq.js} +2 -2
  89. package/web/assets/{cloud-session-DOaDpZTy.js → cloud-session-BYfn6YM4.js} +2 -2
  90. package/web/assets/floating-ui.dom-DF3oyfHd.js +1 -0
  91. package/web/assets/i18n-D0FT7rCj.js +1 -0
  92. package/web/assets/index-3LTzgeU-.css +1 -0
  93. package/web/assets/index-Ci2CNnit.js +309 -0
  94. package/web/assets/modal-iRaP1izE.js +5 -0
  95. package/web/assets/notification.store-CvvuTBVz.js +1 -0
  96. package/web/assets/{project-launch.store-y7JBMNiE.js → project-launch.store-CTmbl8UY.js} +1 -1
  97. package/web/assets/{project.store-DGJr0Qr9.js → project.store-CNC6_4r4.js} +1 -1
  98. package/web/assets/runtime-dom.esm-bundler-CyEVRK6B.js +1 -0
  99. package/web/assets/scenario.store-BIFQPXgl.js +7 -0
  100. package/web/assets/{service.store-5jWsJxPx.js → service.store-B40U4QzI.js} +1 -1
  101. package/web/assets/settings-scope-CyWMCjRY.js +1 -0
  102. package/web/assets/settings.store-Dw6bAoWE.js +1 -0
  103. package/web/assets/{src-Dnwk6NpV.js → src-CrNi4Pjg.js} +3 -3
  104. package/web/assets/src-DIBHat7I.js +3 -0
  105. package/web/assets/src-DtCW_XCh.js +1 -0
  106. package/web/assets/{trpc-Drabzwpd.js → trpc-BAZGRZBD.js} +3 -3
  107. package/web/assets/{useCloudOrg-BAQEtFPl.js → useCloudOrg-EnXFBwt1.js} +1 -1
  108. package/web/assets/useEnvVarAutocomplete-BxJh2Lw0.js +1 -0
  109. package/web/assets/useProjectCatalog-BrvUGv9A.js +1 -0
  110. package/web/assets/{useProjectCloudOrg-CEN9-_OA.js → useProjectCloudOrg-Czxlg00k.js} +1 -1
  111. package/web/assets/{useServiceUrlHost-BuoPEXZX.js → useServiceUrlHost-Szz9y3rh.js} +1 -1
  112. package/web/assets/vue-i18n-o7yraQpR.js +3 -0
  113. package/web/index.html +21 -21
  114. package/chunk-KV4GJKXU.js.map +0 -1
  115. package/chunk-SEPAURTQ.js.map +0 -1
  116. package/chunk-UD42LACH.js.map +0 -1
  117. package/src-LEDGW5KP.js.map +0 -1
  118. package/web/assets/AgentTerminalPage-C3TJ1NL9.js +0 -1
  119. package/web/assets/AiChatModals-D3scRE3S.js +0 -4
  120. package/web/assets/AiChatModals-EfRuvId7.css +0 -1
  121. package/web/assets/AppLayout-BMQUjPKe.js +0 -1
  122. package/web/assets/AuditPage-CKcExBws.js +0 -1
  123. package/web/assets/CloudAccountPage-B6oPg0BZ.js +0 -1
  124. package/web/assets/CloudMessagingPage-C5fwVe-F.js +0 -2
  125. package/web/assets/DashboardPage-9RV7sx3e.js +0 -1
  126. package/web/assets/DirectoryPickerDialog-C06BXyfJ.js +0 -1
  127. package/web/assets/EnvironmentForm-CAvDoira.js +0 -1
  128. package/web/assets/EnvironmentFormPage-BmAxhN-m.js +0 -1
  129. package/web/assets/EnvironmentListPage-B8uATPBI.js +0 -1
  130. package/web/assets/KanbanBoardPage-CQsOkRUf.js +0 -4
  131. package/web/assets/KanbanListPage-CWZqkjD6.js +0 -1
  132. package/web/assets/LaunchProjectPage-saSmC7Pr.js +0 -1
  133. package/web/assets/LaunchServicesPage-BkKyLMEz.js +0 -1
  134. package/web/assets/LaunchServicesPage-DoLDPqQq.css +0 -1
  135. package/web/assets/LogViewer-CZBYkH9J.js +0 -3
  136. package/web/assets/OrgKeyPrompt-DzQ7pwbs.js +0 -1
  137. package/web/assets/ProcessControls-BEeoNlNV.js +0 -1
  138. package/web/assets/ProcessControls-BlpnVylq.css +0 -1
  139. package/web/assets/ProcessDetailPage-CQgu5uKm.js +0 -1
  140. package/web/assets/ProcessMetrics-Crlfkelj.js +0 -1
  141. package/web/assets/ProjectCreateDialog-BolrhRD-.js +0 -1
  142. package/web/assets/ProjectPublishDialog-DsZNWrfz.js +0 -1
  143. package/web/assets/ScenarioEditorPage-MIAGh2us.js +0 -14
  144. package/web/assets/SendToChatDialog-BxWsyv29.js +0 -1
  145. package/web/assets/ServiceControlsBar-DqDPAZt7.js +0 -1
  146. package/web/assets/ServiceDetailPage-hkSAzZ-l.js +0 -2
  147. package/web/assets/ServiceFormPage-DXqhqXg8.js +0 -1
  148. package/web/assets/SettingsPage-DS2YhMIM.js +0 -2086
  149. package/web/assets/UnlockPage-s8SgVcKk.js +0 -1
  150. package/web/assets/VaultMigrationPage-s0b44MDM.js +0 -1
  151. package/web/assets/WikiView-DtGwIt_Q.js +0 -1
  152. package/web/assets/cloud-chat.store-b-BHGz7I.js +0 -1
  153. package/web/assets/editor.store-DCAZSeVG.js +0 -1
  154. package/web/assets/floating-ui.dom-DWhsZyqT.js +0 -1
  155. package/web/assets/i18n-D0afQEGs.js +0 -1
  156. package/web/assets/index-D8VRbyoP.js +0 -309
  157. package/web/assets/index-DsezNbSM.css +0 -1
  158. package/web/assets/modal-qYgTMkYb.js +0 -5
  159. package/web/assets/scenario.store-YWHL2_wu.js +0 -7
  160. package/web/assets/settings-scope-Bv7QGPrt.js +0 -1
  161. package/web/assets/settings.store-bEsB7oKb.js +0 -1
  162. package/web/assets/src--GO-VSJ0.js +0 -5
  163. package/web/assets/src-C-D_y7ou.js +0 -1
  164. package/web/assets/useEnvVarAutocomplete-BqGTODUv.js +0 -1
  165. package/web/assets/useProjectCatalog-yocjIKt6.js +0 -1
  166. package/web/assets/vue-draggable-plus-BHsveB8w.css +0 -1
  167. package/web/assets/vue-draggable-plus-DX4rTBiu.js +0 -1
  168. /package/{agent-manager-R7BPQO6D.js.map → agent-manager-46C4NIQ2.js.map} +0 -0
  169. /package/{chat-LPML22J5.js.map → chat-CMNDMXIP.js.map} +0 -0
  170. /package/{companion-W3UMZWGH.js.map → companion-XKMNRIJB.js.map} +0 -0
  171. /package/{install-gate-PVMGJDWE.js.map → install-gate-OJJEUU4E.js.map} +0 -0
@@ -1,23 +1,23 @@
1
1
  import {
2
- AmbiguousProjectError,
2
+ projectPathStore,
3
+ projectStore
4
+ } from "./chunk-7QXP4FXD.js";
5
+ import {
6
+ agentManager
7
+ } from "./chunk-5XG3X2TM.js";
8
+ import {
9
+ serviceStore
10
+ } from "./chunk-4D3GCBPO.js";
11
+ import {
3
12
  StoreDiskCoordinator,
4
- agentManager,
5
13
  decryptValue,
6
14
  encryptValue,
7
15
  ensureProjectsLoaded,
8
- environmentStore,
9
- forgetProject,
10
16
  getEncryptionKey,
11
- projectKey,
12
- recordDetached,
13
17
  refOfProjectAmong,
14
- registerProjectLoader,
15
18
  rootOfProjectAmong,
16
- rootOfProjectRef,
17
- serviceStore,
18
- setProjectServices,
19
- setProjectVault
20
- } from "./chunk-KV4GJKXU.js";
19
+ rootOfProjectRef
20
+ } from "./chunk-DFPZ4MTI.js";
21
21
  import {
22
22
  atomicWriteSecure,
23
23
  fingerprintFiles,
@@ -34,977 +34,44 @@ import {
34
34
  } from "./chunk-MD3VW4HQ.js";
35
35
  import {
36
36
  allVaultRoots,
37
- collidesWithMachineRoot,
38
37
  env,
39
- forgetLocalProjectRoot,
40
38
  keyedFilePath,
41
39
  legacyMachineFilePath,
42
- listCloudVaults,
43
- localProjectVaultRoot,
44
- localVaultCollidesWithMachineRoot,
45
40
  localVaultRoot,
46
- machineFilePath,
47
- machineRoot,
48
41
  orgIdOfRoot,
49
- readLocalProjectDirs,
50
- registerLocalProjectDir,
51
- setLocalProjectRoot,
52
- unregisterLocalProjectDir,
53
42
  vaultFor,
54
43
  vaultIdOfRoot
55
44
  } from "./chunk-VPSQJOSN.js";
56
45
  import {
57
46
  COMPANION_RELAY_CLOSE,
58
47
  COMPANION_RELAY_OPEN,
59
- ProjectSchema,
60
48
  extractCompanionRelay
61
49
  } from "./chunk-MZSDTA4A.js";
62
50
 
63
51
  // ../server/src/services/companion.ts
64
- import { EventEmitter as EventEmitter4 } from "events";
52
+ import { EventEmitter as EventEmitter3 } from "events";
65
53
 
66
54
  // ../server/src/services/conversation-store.ts
67
- import { randomUUID as randomUUID3 } from "crypto";
68
- import { readFile as readFile4, readdir, mkdir as mkdir3, rename as rename3, rm as rm2, stat as stat2 } from "fs/promises";
69
- import { dirname as dirname2, join as join4 } from "path";
70
- import { EventEmitter as EventEmitter2 } from "events";
71
-
72
- // ../server/src/services/project-store.ts
73
- import { randomUUID as randomUUID2 } from "crypto";
74
- import { EventEmitter } from "events";
75
- import { mkdir as mkdir2, readFile as readFile3, rename as rename2, rm } from "fs/promises";
76
- import { dirname, join as join3, resolve as resolve2 } from "path";
77
-
78
- // ../server/src/utils/ensure-data-gitignore.ts
79
- import { writeFile } from "fs/promises";
80
- import { join } from "path";
81
- var GITIGNORE_CONTENT = `
82
- #################
83
- # Do not edit this file #
84
- #################
85
-
86
-
87
- # Encryption key (contains secrets \u2014 never commit)
88
- .encryption.key
89
- .encryption.key.enc
90
-
91
-
92
- # Key verification canary \u2014 MUST be committed so new team members can validate their encryption key
93
- !.runeya-verify
94
-
95
- # This machine's cloud session token (encrypted, and specific to this install)
96
- cloud-session.json
97
-
98
- # Backup in local only
99
- backup-*
100
-
101
- # Per-user environment overrides (never commit \u2014 each dev has their own)
102
- environment-overrides.json
103
-
104
- # Private user data (conversations, personal state \u2014 never commit)
105
- *.private
106
- *.private.json*
107
- conversations/
108
- conversations.private.json
109
-
110
-
111
-
112
- # Runtime logs
113
- *.log
114
- *.log.*
115
- `;
116
- async function ensureDataGitignore(dataDir) {
117
- const gitignorePath = join(dataDir, ".gitignore");
118
- await writeFile(gitignorePath, GITIGNORE_CONTENT, "utf-8");
119
- }
120
-
121
- // ../server/src/services/conversation-section-store.ts
122
55
  import { randomUUID } from "crypto";
123
- import { readFile } from "fs/promises";
124
- var FILENAME = "conversation-sections.json";
125
- function isSectionMap(value) {
126
- if (!value || typeof value !== "object" || Array.isArray(value)) return false;
127
- return Object.values(value).every((list) => Array.isArray(list) && list.every((s) => !!s && typeof s === "object" && typeof s.id === "string" && typeof s.name === "string" && typeof s.order === "number"));
128
- }
129
- var ConversationSectionStore = class {
130
- async read() {
131
- try {
132
- const parsed = JSON.parse(await readFile(machineFilePath(FILENAME), "utf-8"));
133
- return isSectionMap(parsed) ? parsed : {};
134
- } catch {
135
- return {};
136
- }
137
- }
138
- async write(map) {
139
- await atomicWriteSecure(machineFilePath(FILENAME), JSON.stringify(map, null, 2));
140
- }
141
- /** Les sections d'un projet, dans l'ordre choisi. */
142
- async list(projectId) {
143
- const map = await this.read();
144
- return (map[projectId] ?? []).slice().sort((a, b) => a.order - b.order);
145
- }
146
- async get(projectId, id) {
147
- return (await this.list(projectId)).find((s) => s.id === id) ?? null;
148
- }
149
- /** Crée une section, posée après les autres. */
150
- async create(projectId, name) {
151
- const map = await this.read();
152
- const list = map[projectId] ?? [];
153
- const order = list.length > 0 ? Math.max(...list.map((s) => s.order)) + 1 : 0;
154
- const section = { id: randomUUID(), name, order };
155
- map[projectId] = [...list, section];
156
- await this.write(map);
157
- return section;
158
- }
159
- async rename(projectId, id, name) {
160
- const map = await this.read();
161
- const section = map[projectId]?.find((s) => s.id === id);
162
- if (!section) return null;
163
- section.name = name;
164
- await this.write(map);
165
- return section;
166
- }
167
- /** Retire une section. Rend `false` si elle n'existait pas. */
168
- async remove(projectId, id) {
169
- const map = await this.read();
170
- const list = map[projectId] ?? [];
171
- const next = list.filter((s) => s.id !== id);
172
- if (next.length === list.length) return false;
173
- if (next.length > 0) map[projectId] = next;
174
- else delete map[projectId];
175
- await this.write(map);
176
- return true;
177
- }
178
- /**
179
- * Range les sections du projet dans l'ordre de `ids`.
180
- *
181
- * Une section absente de `ids` passe après les autres, dans son ordre
182
- * d'avant : un appel parti avant la création d'une section ne la fait pas
183
- * disparaître. Un id inconnu est ignoré.
184
- */
185
- async reorder(projectId, ids) {
186
- const map = await this.read();
187
- const list = (map[projectId] ?? []).slice().sort((a, b) => a.order - b.order);
188
- if (list.length === 0) return;
189
- const rank = new Map(Array.from(new Set(ids)).map((id, i) => [id, i]));
190
- const listed = list.filter((s) => rank.has(s.id)).sort((a, b) => rank.get(a.id) - rank.get(b.id));
191
- const others = list.filter((s) => !rank.has(s.id));
192
- map[projectId] = [...listed, ...others].map((s, i) => ({ ...s, order: i }));
193
- await this.write(map);
194
- }
195
- /** Oublie les sections d'un projet supprimé. */
196
- async forgetProject(projectId) {
197
- const map = await this.read();
198
- if (!(projectId in map)) return;
199
- delete map[projectId];
200
- await this.write(map);
201
- }
202
- };
203
- var conversationSectionStore = new ConversationSectionStore();
204
-
205
- // ../server/src/services/project-path-store.ts
206
- import { readFile as readFile2, writeFile as writeFile2, rename, mkdir, chmod, stat } from "fs/promises";
207
- import { isAbsolute, join as join2, resolve } from "path";
208
- var FILENAME2 = "project-paths.json";
209
- var ProjectPathStore = class {
210
- paths = null;
211
- async load() {
212
- if (this.paths) return this.paths;
213
- try {
214
- const raw = await readFile2(machineFilePath(FILENAME2), "utf-8");
215
- const parsed = JSON.parse(raw);
216
- this.paths = isPathMap(parsed) ? parsed : {};
217
- } catch {
218
- this.paths = {};
219
- }
220
- return this.paths;
221
- }
222
- async persist() {
223
- await mkdir(machineRoot(), { recursive: true });
224
- const filePath = machineFilePath(FILENAME2);
225
- const tmpPath = `${filePath}.tmp`;
226
- await writeFile2(tmpPath, JSON.stringify(this.paths ?? {}, null, 2), "utf-8");
227
- await rename(tmpPath, filePath);
228
- await chmod(filePath, 384);
229
- }
230
- /** The directory chosen for this project, or null while it has none. */
231
- async get(ref) {
232
- const paths = await this.load();
233
- return paths[projectKey(ref)] ?? paths[ref.id] ?? null;
234
- }
235
- async all() {
236
- return { ...await this.load() };
237
- }
238
- /**
239
- * The project already placed in this directory, if any other one is.
240
- *
241
- * Answers with the key as stored — the couple, or the bare id for an entry an
242
- * older version wrote — which is enough to say "taken" and to name it.
243
- *
244
- * `isKnown` écarte les entrées d'un projet que ce poste ne connaît plus : un
245
- * projet disparu ne doit pas continuer de retenir son dossier.
246
- */
247
- async projectAt(path, exclude, isKnown = () => true) {
248
- const target = resolve(path);
249
- const excluded = exclude ? [projectKey(exclude), exclude.id] : [];
250
- for (const [key, dir] of Object.entries(await this.load())) {
251
- if (!excluded.includes(key) && resolve(dir) === target && isKnown(key)) return key;
252
- }
253
- return null;
254
- }
255
- async set(ref, path) {
256
- if (!isAbsolute(path)) throw new Error("Le chemin du projet doit \xEAtre absolu");
257
- const paths = await this.load();
258
- delete paths[ref.id];
259
- paths[projectKey(ref)] = resolve(path);
260
- await this.persist();
261
- }
262
- /**
263
- * Faire suivre le répertoire quand un projet change d'organisation.
264
- *
265
- * La clé est le couple `(organisation, projet)` : publier un projet local le
266
- * fait passer de `local:<id>` à `<org>:<id>`, et l'entrée d'avant cesse alors
267
- * de répondre — `get` ne retombe que sur l'identifiant nu, forme héritée, pas
268
- * sur `local:<id>`. Le projet se retrouvait donc sans emplacement connu juste
269
- * après avoir été publié, alors que ses sources n'avaient pas bougé d'un
270
- * octet.
271
- */
272
- async relocate(from, to) {
273
- const paths = await this.load();
274
- const previous = paths[projectKey(from)] ?? paths[from.id];
275
- if (!previous) return;
276
- delete paths[projectKey(from)];
277
- delete paths[from.id];
278
- paths[projectKey(to)] = previous;
279
- await this.persist();
280
- }
281
- async clear(ref) {
282
- const paths = await this.load();
283
- const keys = [projectKey(ref), ref.id].filter((k) => k in paths);
284
- if (keys.length === 0) return;
285
- for (const key of keys) delete paths[key];
286
- await this.persist();
287
- }
288
- /** Test seam: forget what was read from disk. */
289
- reset() {
290
- this.paths = null;
291
- }
292
- };
293
- function isPathMap(value) {
294
- return !!value && typeof value === "object" && !Array.isArray(value) && Object.values(value).every((v) => typeof v === "string");
295
- }
296
- async function holdsRuneyaProject(path) {
297
- return stat(join2(path, ".runeya", "project.json")).then(() => true, () => false);
298
- }
299
- var projectPathStore = new ProjectPathStore();
300
-
301
- // ../server/src/services/project-store.ts
302
- var FILENAME3 = "project.json";
303
- var LEGACY_FILENAME = "projects.json";
304
- function toDiskForm(project) {
305
- const { activeEnvironmentId: _runtimeOnly, ...rest } = project;
306
- return rest;
307
- }
308
- async function readLocalProject() {
309
- const root = localVaultRoot();
310
- let current = null;
311
- try {
312
- const raw = await readFile3(join3(root, FILENAME3), "utf-8");
313
- const parsed = ProjectSchema.safeParse(JSON.parse(raw));
314
- if (parsed.success) current = parsed.data;
315
- } catch {
316
- }
317
- let legacy;
318
- try {
319
- legacy = JSON.parse(await readFile3(join3(root, LEGACY_FILENAME), "utf-8"));
320
- } catch {
321
- return { local: current, strayCloud: [] };
322
- }
323
- const strayCloud = legacy.filter((p) => p.orgId);
324
- if (current) return { local: current, strayCloud };
325
- const locals = legacy.filter((p) => !p.orgId);
326
- if (locals.length > 1) {
327
- const names = locals.map((p) => `"${p.name}" (${p.id})`).join(", ");
328
- throw new Error(
329
- `${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.`
330
- );
331
- }
332
- return { local: locals[0] ?? null, strayCloud };
333
- }
334
- function isRegistrable(root) {
335
- return localProjectVaultRoot(dirname(root)) === resolve2(root);
336
- }
337
- async function readLocalProjectAt(root) {
338
- try {
339
- const parsed = ProjectSchema.safeParse(JSON.parse(await readFile3(join3(root, FILENAME3), "utf-8")));
340
- return parsed.success && !parsed.data.orgId ? parsed.data : null;
341
- } catch {
342
- return null;
343
- }
344
- }
345
- async function readCloudProjects() {
346
- const found = [];
347
- for (const { orgId, projectId, root } of await listCloudVaults()) {
348
- try {
349
- const raw = await readFile3(join3(root, FILENAME3), "utf-8");
350
- const parsed = ProjectSchema.safeParse({ ...JSON.parse(raw), id: projectId, orgId });
351
- if (parsed.success) found.push({ project: parsed.data, root });
352
- } catch {
353
- }
354
- }
355
- return found;
356
- }
357
- var LocalProjectRefusedError = class extends Error {
358
- };
359
- var LocalProjectExistsError = class extends LocalProjectRefusedError {
360
- constructor(projectName) {
361
- super(
362
- `This directory already holds the local project "${projectName}". A directory holds one local project: use another directory, or create the project in an organization.`
363
- );
364
- this.projectName = projectName;
365
- this.name = "LocalProjectExistsError";
366
- }
367
- code = "LOCAL_PROJECT_EXISTS";
368
- };
369
- var LocalProjectInMachineRootError = class extends LocalProjectRefusedError {
370
- code = "LOCAL_PROJECT_IN_MACHINE_ROOT";
371
- constructor() {
372
- super(
373
- "This directory holds Runeya's own data. A local project here would mix its files with the machine's vaults: launch Runeya from another directory, or create the project in an organization."
374
- );
375
- this.name = "LocalProjectInMachineRootError";
376
- }
377
- };
378
- var LocalProjectCloudLinkedError = class extends LocalProjectRefusedError {
379
- code = "LOCAL_PROJECT_CLOUD_LINKED";
380
- constructor() {
381
- super("This directory is already linked to an organization project. Choose another directory.");
382
- this.name = "LocalProjectCloudLinkedError";
383
- }
384
- };
385
- var LocalProjectNotFoundError = class extends LocalProjectRefusedError {
386
- code = "LOCAL_PROJECT_NOT_FOUND";
387
- constructor() {
388
- super("This directory holds no local Runeya project to import.");
389
- this.name = "LocalProjectNotFoundError";
390
- }
391
- };
392
- var ProjectStore = class {
393
- /**
394
- * Keyed by the (organization, project) couple, never by the project id alone:
395
- * the same id can name a different project in each organization, and keying
396
- * by id let the second one read from disk overwrite the first — then `save()`
397
- * wrote that survivor back into both vaults.
398
- */
399
- projects = /* @__PURE__ */ new Map();
400
- /**
401
- * Les coffres où un projet local a été lu ou écrit. À l'écriture, un coffre
402
- * de cette liste qui n'a plus de projet perd sa fiche : c'est ainsi qu'une
403
- * suppression ou une publication vide le dossier qu'elle quitte.
404
- */
405
- localRoots = /* @__PURE__ */ new Set();
406
- loaded = false;
407
- /**
408
- * L'état du disque tel qu'il était au dernier chargement.
409
- *
410
- * Plusieurs Runeya partagent la racine machine — l'application installée et
411
- * celle qu'on développe, couramment — et créent donc des projets dans les
412
- * mêmes coffres. Un store chargé une fois pour toutes ne voit jamais le
413
- * travail de l'autre : il faudrait le redémarrer pour qu'un projet créé à
414
- * côté existe pour lui.
415
- */
416
- fingerprint = "";
417
- disk = new StoreDiskCoordinator();
418
- /** Emits `'saved'` after every persisted mutation, for the cloud sync bridge. */
419
- events = new EventEmitter();
420
- getFilePath(root = localVaultRoot()) {
421
- return join3(root, FILENAME3);
422
- }
423
- /** Adopt a project read from disk, remembering which vault it came from. */
424
- async absorb(into, p, root) {
425
- const projectEnvs = await environmentStore.getByProjectId(p.id).catch(() => []);
426
- const defaultEnv = projectEnvs.find((e) => e.default);
427
- into.set(projectKey(p), {
428
- ...p,
429
- environmentIds: p.environmentIds ?? [],
430
- groupOrder: p.groupOrder ?? [],
431
- // Prefer the env marked default, fall back to first env
432
- activeEnvironmentId: defaultEnv?.id ?? p.environmentIds?.[0]
433
- });
434
- if (!p.orgId) {
435
- setLocalProjectRoot(p.id, root);
436
- this.localRoots.add(root);
437
- }
438
- setProjectVault(p, root);
439
- setProjectServices(p, p.serviceIds ?? []);
440
- }
441
- /**
442
- * Ce que le disque porte : quels coffres, et quand leur fiche a changé.
443
- *
444
- * Les coffres sont demandés à `listCloudVaults`, qui les découvre déjà par
445
- * balayage — en refaire un ici donnerait deux inventaires à tenir d'accord.
446
- * Seules les dates sont relevées : lire et analyser les fiches, puis les
447
- * absorber (ce qui va interroger les environnements), est justement ce que ce
448
- * repère évite de refaire à chaque appel.
449
- */
450
- async diskFingerprint() {
451
- return fingerprintFiles([
452
- this.getFilePath(),
453
- ...(await readLocalProjectDirs()).map((dir) => this.getFilePath(localProjectVaultRoot(dir))),
454
- ...(await listCloudVaults()).map((v) => this.getFilePath(v.root))
455
- ]);
456
- }
457
- /**
458
- * Read every vault: the launch directory, then each organization's projects.
459
- *
460
- * Cloud vaults are discovered by walking `~/.runeya/orgs/*&#47;projects/*` rather
461
- * than from an index. An index is one more thing that can disagree with the
462
- * disk — and a project present on disk but missing from it is exactly the
463
- * "already here, still shown as downloadable" bug.
464
- *
465
- * Relu quand le disque a bougé, pas une fois pour toutes : une autre instance
466
- * partageant cette racine machine y crée des projets, et ne jamais relire
467
- * reviendrait à exiger un redémarrage pour les voir.
468
- */
469
- async load() {
470
- let migrated = false;
471
- await this.disk.load(async () => {
472
- const fingerprint = await this.diskFingerprint();
473
- if (this.loaded && fingerprint === this.fingerprint) return null;
474
- const next = await this.readVaults();
475
- return () => {
476
- for (const [key, project] of next.projects) {
477
- const before = this.projects.get(key)?.activeEnvironmentId;
478
- if (before && project.environmentIds.includes(before)) {
479
- project.activeEnvironmentId = before;
480
- } else {
481
- this.unsettled.add(key);
482
- }
483
- }
484
- this.projects = next.projects;
485
- this.fingerprint = fingerprint;
486
- this.loaded = true;
487
- migrated = next.migrated;
488
- };
489
- });
490
- if (migrated) {
491
- await this.save();
492
- await serviceStore.relocateAll();
493
- await environmentStore.relocateAll();
494
- }
495
- }
496
- /** Le parcours proprement dit, dans une table neuve que `load` substitue. */
497
- async readVaults() {
498
- const projects = /* @__PURE__ */ new Map();
499
- this.localRoots = /* @__PURE__ */ new Set();
500
- const { local, strayCloud } = await readLocalProject();
501
- if (local) {
502
- await this.absorb(projects, local, localVaultRoot());
503
- if (!localVaultCollidesWithMachineRoot() && isRegistrable(localVaultRoot())) {
504
- await registerLocalProjectDir(dirname(localVaultRoot())).catch(() => {
505
- });
506
- }
507
- }
508
- for (const dir of await readLocalProjectDirs()) {
509
- const root = localProjectVaultRoot(dir);
510
- if (root === localVaultRoot()) continue;
511
- const found = await readLocalProjectAt(root);
512
- if (!found || projects.has(projectKey(found))) continue;
513
- await this.absorb(projects, found, root);
514
- }
515
- let migrated = false;
516
- for (const project of strayCloud) {
517
- try {
518
- await this.absorb(projects, project, vaultFor(project).root);
519
- migrated = true;
520
- } catch {
521
- }
522
- }
523
- for (const { project, root } of await readCloudProjects()) {
524
- await this.absorb(projects, project, root);
525
- }
526
- return { projects, migrated };
527
- }
528
- /**
529
- * Persist every vault this store knows about.
530
- *
531
- * Writing all of them rather than only the one that changed keeps a project
532
- * that moved between vaults — a local project adopted by an org, a cloud
533
- * project pulled for the first time — from lingering in the vault it left.
534
- */
535
- save() {
536
- return this.disk.save(() => this.writeVaults());
537
- }
538
- async writeVaults() {
539
- const locals = [];
540
- const cloud = [];
541
- for (const project of this.projects.values()) {
542
- if (!project.orgId) locals.push(project);
543
- else cloud.push({ project, root: vaultFor(project).root });
544
- }
545
- const written = /* @__PURE__ */ new Map();
546
- for (const local of locals) {
547
- const root = vaultFor(local).root;
548
- const other = written.get(root);
549
- if (other) {
550
- throw new Error(
551
- `Refusing to save two local projects in ${root}: "${other.name}" and "${local.name}". A directory holds a single local project.`
552
- );
553
- }
554
- written.set(root, local);
555
- }
556
- for (const [root, local] of written) {
557
- await mkdir2(root, { recursive: true });
558
- await ensureDataGitignore(root);
559
- await atomicWriteSecure(this.getFilePath(root), JSON.stringify(toDiskForm(local), null, 2));
560
- this.localRoots.add(root);
561
- }
562
- for (const root of this.localRoots) {
563
- if (written.has(root)) continue;
564
- await rm(this.getFilePath(root), { force: true }).catch(() => {
565
- });
566
- if (isRegistrable(root)) await unregisterLocalProjectDir(dirname(root)).catch(() => {
567
- });
568
- this.localRoots.delete(root);
569
- }
570
- await rename2(join3(localVaultRoot(), LEGACY_FILENAME), join3(localVaultRoot(), LEGACY_FILENAME + ".bak")).catch(() => {
571
- });
572
- for (const { project, root } of cloud) {
573
- await atomicWriteSecure(this.getFilePath(root), JSON.stringify(toDiskForm(project), null, 2));
574
- setProjectVault(project, root);
575
- }
576
- this.fingerprint = await this.diskFingerprint();
577
- this.events.emit("saved");
578
- }
579
- /**
580
- * The project a caller means, from a couple or from a bare id.
581
- *
582
- * A ref carrying its `orgId` — even an explicit `null` for a local project —
583
- * names exactly one project and is looked up directly. A bare id is what a
584
- * contained entity (an environment, a conversation) still carries, and it is
585
- * resolved only while it answers for a single project: electing one of two
586
- * would write into the wrong organization's vault.
587
- */
588
- resolveKey(target) {
589
- if (typeof target !== "string" && target.orgId !== void 0) return projectKey(target);
590
- const id = typeof target === "string" ? target : target.id;
591
- const matches = Array.from(this.projects.values()).filter((p) => p.id === id);
592
- if (matches.length > 1) {
593
- throw new AmbiguousProjectError(id, matches.map((p) => p.orgId ?? null));
594
- }
595
- return matches[0] ? projectKey(matches[0]) : null;
596
- }
597
- find(target) {
598
- const key = this.resolveKey(target);
599
- const project = key ? this.projects.get(key) : void 0;
600
- return key && project ? { key, project } : null;
601
- }
602
- /**
603
- * Le projet déjà placé dans ce dossier, parmi ceux que ce poste connaît.
604
- *
605
- * Une entrée de `project-paths.json` peut survivre à son projet (supprimé
606
- * ailleurs, ou avant que la suppression ne la nettoie). Elle ne réserve plus
607
- * rien : le refus qui en découlait nommait un projet introuvable.
608
- */
609
- async projectAtPath(path, exclude) {
610
- await this.load();
611
- const ids = new Set(Array.from(this.projects.values(), (p) => p.id));
612
- return projectPathStore.projectAt(path, exclude, (key) => this.projects.has(key) || ids.has(key));
613
- }
614
- async list() {
615
- await this.load();
616
- await this.settleActiveEnvironments();
617
- return Array.from(this.projects.values());
618
- }
619
- async get(target) {
620
- await this.load();
621
- await this.settleActiveEnvironments();
622
- return this.find(target)?.project ?? null;
623
- }
624
- /**
625
- * Projets dont l'environnement actif vient d'un repli et n'a pas été vérifié.
626
- *
627
- * Au chargement, les environnements ne sont pas encore lisibles : leur magasin
628
- * attend justement la fin de ce chargement. `absorb` retient donc le premier
629
- * id de `environmentIds`, qui peut désigner un environnement disparu. Le
630
- * sélecteur affichait alors cet id brut, absent de sa liste.
631
- */
632
- unsettled = /* @__PURE__ */ new Set();
633
- settling = false;
634
- /** Retenir, pour chaque projet non vérifié, un environnement qui existe. */
635
- async settleActiveEnvironments() {
636
- if (this.settling || this.unsettled.size === 0) return;
637
- this.settling = true;
638
- try {
639
- for (const key of this.unsettled) {
640
- const project = this.projects.get(key);
641
- if (!project) {
642
- this.unsettled.delete(key);
643
- continue;
644
- }
645
- const envs = (await environmentStore.getByProjectId(project.id, project.orgId ?? null).catch(() => [])).filter((e) => e.scope !== "service");
646
- if (envs.length === 0) continue;
647
- const current = envs.find((e) => e.id === project.activeEnvironmentId);
648
- const chosen = envs.find((e) => e.default) ?? current ?? envs[0];
649
- project.activeEnvironmentId = chosen.id;
650
- this.unsettled.delete(key);
651
- }
652
- } finally {
653
- this.settling = false;
654
- }
655
- }
656
- /**
657
- * Vérifier qu'un dossier peut accueillir un projet local, et rendre son coffre.
658
- *
659
- * Refusé si son `.runeya` serait la racine machine, si le dossier est déjà
660
- * l'emplacement d'un projet d'organisation, ou s'il porte déjà un projet.
661
- */
662
- async localRootFor(root) {
663
- if (collidesWithMachineRoot(root)) throw new LocalProjectInMachineRootError();
664
- if (await this.projectAtPath(dirname(root))) throw new LocalProjectCloudLinkedError();
665
- const known = Array.from(this.projects.values()).find((p) => !p.orgId && vaultFor(p).root === root);
666
- if (known) throw new LocalProjectExistsError(known.name);
667
- const onDisk = await readLocalProjectAt(root);
668
- if (onDisk) throw new LocalProjectExistsError(onDisk.name);
669
- if (root !== localVaultRoot() && await holdsRuneyaProject(dirname(root))) throw new LocalProjectExistsError("");
670
- return root;
671
- }
672
- /**
673
- * `localDir` : le dossier d'un projet local. Omis, c'est le dossier de
674
- * lancement, comme avant que les projets locaux aient chacun le leur.
675
- */
676
- async create(input, localDir) {
677
- await this.load();
678
- const localRoot = input.orgId ? null : await this.localRootFor(localDir ? localProjectVaultRoot(localDir) : localVaultRoot());
679
- const project = {
680
- id: randomUUID2(),
681
- orgId: input.orgId ?? null,
682
- name: input.name,
683
- description: input.description ?? "",
684
- serviceIds: input.serviceIds ?? [],
685
- environmentIds: input.environmentIds ?? [],
686
- activeEnvironmentId: input.activeEnvironmentId,
687
- groupOrder: input.groupOrder ?? []
688
- };
689
- this.projects.set(projectKey(project), project);
690
- if (localRoot) {
691
- setLocalProjectRoot(project.id, localRoot);
692
- if (isRegistrable(localRoot)) await registerLocalProjectDir(dirname(localRoot));
693
- }
694
- setProjectVault(project, vaultFor(project).root);
695
- setProjectServices(project, project.serviceIds);
696
- await this.save();
697
- return project;
698
- }
699
- /**
700
- * Ce qu'un dossier permet pour un projet local : en créer un (`available`),
701
- * importer celui qu'il porte (`importable`), rien parce qu'il est déjà dans
702
- * la liste (`listed`), ou rien parce qu'il est refusé.
703
- */
704
- async inspectLocalDir(dir) {
705
- await this.load();
706
- const root = localProjectVaultRoot(dir);
707
- if (collidesWithMachineRoot(root)) return { status: "machine-root", projectName: null };
708
- if (await this.projectAtPath(resolve2(dir))) return { status: "cloud-linked", projectName: null };
709
- const known = Array.from(this.projects.values()).find((p) => !p.orgId && vaultFor(p).root === root);
710
- if (known) return { status: "listed", projectName: known.name };
711
- const onDisk = await readLocalProjectAt(root);
712
- if (onDisk) return { status: "importable", projectName: onDisk.name };
713
- if (await holdsRuneyaProject(dirname(root))) return { status: "unreadable", projectName: null };
714
- return { status: "available", projectName: null };
715
- }
716
- /**
717
- * Ajouter à la liste le projet local déjà présent dans ce dossier.
718
- *
719
- * Son `.runeya` n'est pas touché : le dossier est seulement enregistré, et le
720
- * projet relu comme les autres.
721
- */
722
- async importLocal(dir) {
723
- await this.load();
724
- const root = localProjectVaultRoot(dir);
725
- if (collidesWithMachineRoot(root)) throw new LocalProjectInMachineRootError();
726
- if (await this.projectAtPath(resolve2(dir))) throw new LocalProjectCloudLinkedError();
727
- const found = await readLocalProjectAt(root);
728
- if (!found) throw new LocalProjectNotFoundError();
729
- const known = this.projects.get(projectKey(found));
730
- if (known) return known;
731
- await registerLocalProjectDir(dirname(root));
732
- this.loaded = false;
733
- await this.load();
734
- return this.projects.get(projectKey(found)) ?? found;
735
- }
736
- /**
737
- * `input.orgId` names which project is being updated, it does not move it:
738
- * a project is born local or in an organization and stays there, so treating
739
- * it as a new value would silently relocate a vault instead of editing it.
740
- */
741
- async update(input) {
742
- await this.load();
743
- const found = this.find(input);
744
- if (!found) return null;
745
- const { key, project: existing } = found;
746
- const updated = {
747
- ...existing,
748
- ...input.name !== void 0 && { name: input.name },
749
- ...input.description !== void 0 && { description: input.description },
750
- ...input.serviceIds !== void 0 && { serviceIds: input.serviceIds },
751
- ...input.environmentIds !== void 0 && { environmentIds: input.environmentIds },
752
- ...input.activeEnvironmentId !== void 0 && { activeEnvironmentId: input.activeEnvironmentId },
753
- ...input.groupOrder !== void 0 && { groupOrder: input.groupOrder }
754
- };
755
- recordDetached(existing.id, "serviceIds", existing.serviceIds ?? [], updated.serviceIds ?? []);
756
- recordDetached(existing.id, "environmentIds", existing.environmentIds ?? [], updated.environmentIds ?? []);
757
- this.projects.set(key, updated);
758
- setProjectVault(updated, vaultFor(updated).root);
759
- setProjectServices(updated, updated.serviceIds);
760
- await this.save();
761
- return updated;
762
- }
763
- /**
764
- * Faire passer le projet local de ce dossier dans une organisation.
765
- *
766
- * Le seul déplacement de coffre qu'on s'autorise, et il ne va que dans ce
767
- * sens : le projet quitte `<cwd>/.runeya` pour
768
- * `~/.runeya/orgs/<org>/projects/<id>/`, en gardant son identifiant. Il n'y a
769
- * donc jamais deux copies à réconcilier — c'est ce qui rendait « publier »
770
- * dangereux tant que le projet restait aussi local.
771
- *
772
- * Refusé si l'organisation porte déjà ce projet : la copie d'ici écraserait
773
- * celle du cloud, et ces deux histoires ne sont pas à nous de les fusionner.
774
- *
775
- * Ce qui suit le projet est écrit par les stores eux-mêmes : `save()` réécrit
776
- * chaque coffre — le fichier local disparaît, celui de l'org apparaît — puis
777
- * les contenus sont relogés depuis le registre, qui pointe désormais ailleurs.
778
- */
779
- async moveToOrg(projectId, orgId) {
780
- await this.load();
781
- const local = this.projects.get(projectKey({ id: projectId, orgId: null }));
782
- if (!local) throw new Error("PROJECT_NOT_LOCAL");
783
- if (this.projects.get(projectKey({ id: projectId, orgId }))) {
784
- throw new Error("PROJECT_ALREADY_IN_ORG");
785
- }
786
- const localRoot = vaultFor(local).root;
787
- const localDir = localRoot === localVaultRoot() ? null : dirname(localRoot);
788
- const moved = { ...local, orgId };
789
- this.projects.delete(projectKey(local));
790
- forgetProject(local);
791
- forgetLocalProjectRoot(local.id);
792
- this.projects.set(projectKey(moved), moved);
793
- setProjectVault(moved, vaultFor(moved).root);
794
- setProjectServices(moved, moved.serviceIds ?? []);
795
- await this.save();
796
- if (localDir && !await projectPathStore.get(moved)) {
797
- await projectPathStore.set(moved, localDir);
798
- }
799
- await serviceStore.reassignOrg(moved.serviceIds ?? [], orgId);
800
- await environmentStore.relocateAll();
801
- return moved;
802
- }
803
- async delete(target) {
804
- await this.load();
805
- const found = this.find(target);
806
- if (!found) return false;
807
- const { key, project } = found;
808
- this.projects.delete(key);
809
- await this.save();
810
- await this.discardVault(project);
811
- await projectPathStore.clear(project);
812
- await conversationSectionStore.forgetProject(project.id);
813
- return true;
814
- }
815
- /**
816
- * Remove what a deleted cloud project left behind.
817
- *
818
- * Its vault is a directory of its own, so dropping the project means dropping
819
- * the directory — otherwise `readCloudVaults` would resurrect it on the next
820
- * load. Local projects share their vault with the directory's own files and
821
- * are only removed from the list.
822
- */
823
- async discardVault(project) {
824
- forgetProject(project);
825
- if (!project.orgId) forgetLocalProjectRoot(project.id);
826
- if (!project.orgId) return;
827
- await rm(vaultFor(project).root, { recursive: true, force: true }).catch(() => {
828
- });
829
- }
830
- /**
831
- * Stop holding a project on this machine, leaving the doc alone.
832
- *
833
- * Memory only: the caller removes the vault itself. Its contents are dropped
834
- * too, because a service or environment surviving in memory would be written
835
- * back on the next save — and, its project being unknown by then, into the
836
- * local vault.
837
- */
838
- async forgetLocally(target) {
839
- await this.load();
840
- const found = this.find(target);
841
- if (!found) return;
842
- const { key, project } = found;
843
- this.projects.delete(key);
844
- forgetProject(project);
845
- await serviceStore.forgetLocally(project.serviceIds ?? []);
846
- await environmentStore.forgetByProject(project.id);
847
- await projectPathStore.clear(project);
848
- }
849
- /**
850
- * Le projet qui porte un service, ou `null` s'il n'est rattaché à aucun.
851
- *
852
- * Un service n'appartient qu'à un projet : la recherche s'arrête au premier.
853
- */
854
- async ofService(serviceId) {
855
- await this.load();
856
- for (const project of this.projects.values()) {
857
- if (project.serviceIds.includes(serviceId)) return project;
858
- }
859
- return null;
860
- }
861
- /** All projects linked to a given organization (for the sync bridge). */
862
- async listByOrg(orgId) {
863
- await this.load();
864
- return Array.from(this.projects.values()).filter((p) => p.orgId === orgId);
865
- }
866
- /** Projects of an organization in their on-disk form (what the cloud mirrors). */
867
- async listDiskFormByOrg(orgId) {
868
- return (await this.listByOrg(orgId)).map(toDiskForm);
869
- }
870
- /**
871
- * Apply a project received from the cloud, in its on-disk form.
872
- *
873
- * Validated rather than trusted: a peer on another version — or a tampered
874
- * doc — must not be able to write arbitrary shapes into `projects.json`.
875
- * `activeEnvironmentId` is runtime-only and stays whatever it is locally.
876
- * Diff-guarded: an identical project neither saves nor emits (loop-safe).
877
- * Returns whether a new local project was created, so the caller can
878
- * bootstrap project-local state (e.g. a default environment).
879
- */
880
- async applyRemoteUpsert(id, fields) {
881
- await this.load();
882
- const parsed = ProjectSchema.safeParse({ ...fields, id });
883
- if (!parsed.success) {
884
- console.error("[project-store] Rejected remote project %s:", id, parsed.error.message);
885
- return null;
886
- }
887
- const incoming = toDiskForm(parsed.data);
888
- const key = projectKey(parsed.data);
889
- const existing = this.projects.get(key);
890
- if (existing) {
891
- if (JSON.stringify(toDiskForm(existing)) === JSON.stringify(incoming)) {
892
- return { project: existing, created: false };
893
- }
894
- const updated = { ...incoming, activeEnvironmentId: existing.activeEnvironmentId };
895
- this.projects.set(key, updated);
896
- setProjectVault(updated, vaultFor(updated).root);
897
- setProjectServices(updated, updated.serviceIds);
898
- await this.save();
899
- return { project: updated, created: false };
900
- }
901
- const project = { ...incoming, activeEnvironmentId: void 0 };
902
- this.projects.set(key, project);
903
- setProjectVault(project, vaultFor(project).root);
904
- setProjectServices(project, project.serviceIds);
905
- await this.save();
906
- return { project, created: true };
907
- }
908
- /** Delete a project that was removed on another peer. */
909
- async applyRemoteDelete(target) {
910
- await this.load();
911
- const found = this.find(target);
912
- if (!found) return false;
913
- const { key, project } = found;
914
- this.projects.delete(key);
915
- await this.save();
916
- await this.discardVault(project);
917
- await projectPathStore.clear(project);
918
- return true;
919
- }
920
- async addService(target, serviceId) {
921
- await this.load();
922
- const found = this.find(target);
923
- if (!found) return null;
924
- const { key, project } = found;
925
- if (project.serviceIds.includes(serviceId)) return project;
926
- const updated = { ...project, serviceIds: [...project.serviceIds, serviceId] };
927
- this.projects.set(key, updated);
928
- setProjectServices(updated, updated.serviceIds);
929
- await this.save();
930
- return updated;
931
- }
932
- async removeService(target, serviceId) {
933
- await this.load();
934
- const found = this.find(target);
935
- if (!found) return null;
936
- const { key, project } = found;
937
- const updated = { ...project, serviceIds: project.serviceIds.filter((id) => id !== serviceId) };
938
- recordDetached(project.id, "serviceIds", project.serviceIds, updated.serviceIds);
939
- this.projects.set(key, updated);
940
- setProjectServices(updated, updated.serviceIds);
941
- await this.save();
942
- return updated;
943
- }
944
- async addEnvironment(target, environmentId) {
945
- await this.load();
946
- const found = this.find(target);
947
- if (!found) return null;
948
- const { key, project } = found;
949
- if (project.environmentIds?.includes(environmentId)) return project;
950
- const updated = {
951
- ...project,
952
- environmentIds: [...project.environmentIds ?? [], environmentId]
953
- };
954
- this.projects.set(key, updated);
955
- await this.save();
956
- return updated;
957
- }
958
- async removeEnvironment(target, environmentId) {
959
- await this.load();
960
- const found = this.find(target);
961
- if (!found) return null;
962
- const { key, project } = found;
963
- const updated = {
964
- ...project,
965
- environmentIds: (project.environmentIds ?? []).filter((id) => id !== environmentId),
966
- // Clear activeEnvironmentId if it was the removed environment
967
- ...project.activeEnvironmentId === environmentId && { activeEnvironmentId: void 0 }
968
- };
969
- recordDetached(project.id, "environmentIds", project.environmentIds ?? [], updated.environmentIds);
970
- this.projects.set(key, updated);
971
- await this.save();
972
- return updated;
973
- }
974
- async setActiveEnvironment(target, environmentId) {
975
- await this.load();
976
- const found = this.find(target);
977
- if (!found) return null;
978
- const { key, project } = found;
979
- const updated = {
980
- ...project,
981
- activeEnvironmentId: environmentId
982
- };
983
- this.projects.set(key, updated);
984
- this.unsettled.delete(key);
985
- return updated;
986
- }
987
- };
988
- var projectStore = new ProjectStore();
989
- registerProjectLoader(() => projectStore.list().then(() => void 0));
990
-
991
- // ../server/src/services/conversation-store.ts
992
- var FILENAME4 = "conversations.private.json";
56
+ import { readFile, readdir, mkdir, rename, rm, stat } from "fs/promises";
57
+ import { dirname, join } from "path";
58
+ import { EventEmitter } from "events";
59
+ var FILENAME = "conversations.private.json";
993
60
  var CONVERSATIONS_DIRNAME = "conversations";
994
61
  var FILE_SUFFIX = ".private.json";
995
62
  var SAFE_ID_RE = /^[a-zA-Z0-9][a-zA-Z0-9._-]{0,127}$/;
996
63
  function conversationDir(root) {
997
- return join4(root, CONVERSATIONS_DIRNAME);
64
+ return join(root, CONVERSATIONS_DIRNAME);
998
65
  }
999
66
  function conversationPath(root, id) {
1000
- return join4(root, CONVERSATIONS_DIRNAME, `${id}${FILE_SUFFIX}`);
67
+ return join(root, CONVERSATIONS_DIRNAME, `${id}${FILE_SUFFIX}`);
1001
68
  }
1002
69
  function migratedAggregateName(now) {
1003
70
  return `conversations.migrated-${now.toISOString().replace(/[:.]/g, "-").substring(0, 19)}.private.json`;
1004
71
  }
1005
72
  async function fileStamp(path) {
1006
73
  try {
1007
- const s = await stat2(path);
74
+ const s = await stat(path);
1008
75
  return `${s.mtimeMs}:${s.size}:${s.ino}`;
1009
76
  } catch {
1010
77
  return null;
@@ -1039,7 +106,7 @@ var ConversationStore = class {
1039
106
  * conversation écrite dans l'autre fenêtre.
1040
107
  */
1041
108
  fingerprint = "";
1042
- events = new EventEmitter2().setMaxListeners(0);
109
+ events = new EventEmitter().setMaxListeners(0);
1043
110
  disk = new StoreDiskCoordinator();
1044
111
  /** Le fichier où chaque conversation est écrite aujourd'hui, s'il existe. */
1045
112
  written = /* @__PURE__ */ new Map();
@@ -1070,11 +137,11 @@ var ConversationStore = class {
1070
137
  * root, but a launch directory owning its own key keeps both together.
1071
138
  */
1072
139
  getFilePath() {
1073
- return keyedFilePath(FILENAME4);
140
+ return keyedFilePath(FILENAME);
1074
141
  }
1075
142
  /** Where older versions kept it — read only, so an older Runeya keeps working. */
1076
143
  getLegacyFilePath() {
1077
- return legacyMachineFilePath(FILENAME4);
144
+ return legacyMachineFilePath(FILENAME);
1078
145
  }
1079
146
  /**
1080
147
  * Read the machine file, falling back to the launch directory of old.
@@ -1084,13 +151,13 @@ var ConversationStore = class {
1084
151
  */
1085
152
  async readFromDisk() {
1086
153
  try {
1087
- return await readFile4(await this.getFilePath(), "utf-8");
154
+ return await readFile(await this.getFilePath(), "utf-8");
1088
155
  } catch {
1089
- return readFile4(this.getLegacyFilePath(), "utf-8");
156
+ return readFile(this.getLegacyFilePath(), "utf-8");
1090
157
  }
1091
158
  }
1092
159
  async ensureDir() {
1093
- await mkdir3(dirname2(await this.getFilePath()), { recursive: true });
160
+ await mkdir(dirname(await this.getFilePath()), { recursive: true });
1094
161
  }
1095
162
  encryptMessages(messages, vault) {
1096
163
  return encryptValue(JSON.stringify(messages), vault);
@@ -1197,10 +264,10 @@ ${new Error("trace").stack}`
1197
264
  /** Les fichiers dont ce store tire son contenu : les coffres, et l'hérité. */
1198
265
  async sourcePaths() {
1199
266
  const vaults = await allVaultRoots();
1200
- const legacyPaths = [this.getLegacyFilePath(), await keyedFilePath(FILENAME4)];
267
+ const legacyPaths = [this.getLegacyFilePath(), await keyedFilePath(FILENAME)];
1201
268
  const all = [
1202
269
  ...legacyPaths.map((path) => ({ path, legacy: true })),
1203
- ...vaults.map((root) => ({ path: join4(root, FILENAME4), legacy: false }))
270
+ ...vaults.map((root) => ({ path: join(root, FILENAME), legacy: false }))
1204
271
  ];
1205
272
  return all.filter((source, i) => !all.slice(i + 1).some((later) => later.path === source.path));
1206
273
  }
@@ -1209,7 +276,7 @@ ${new Error("trace").stack}`
1209
276
  * doublon. Le dossier de lancement hérité en fait partie.
1210
277
  */
1211
278
  async sourceRoots() {
1212
- return Array.from(new Set((await this.sourcePaths()).map((s) => dirname2(s.path))));
279
+ return Array.from(new Set((await this.sourcePaths()).map((s) => dirname(s.path))));
1213
280
  }
1214
281
  /**
1215
282
  * L'état du disque : les anciens fichiers, et les dossiers `conversations/`.
@@ -1220,7 +287,7 @@ ${new Error("trace").stack}`
1220
287
  */
1221
288
  async diskFingerprint() {
1222
289
  const paths = (await this.sourcePaths()).map((s) => s.path);
1223
- const dirs = Array.from(new Set(paths.map((path) => conversationDir(dirname2(path)))));
290
+ const dirs = Array.from(new Set(paths.map((path) => conversationDir(dirname(path)))));
1224
291
  return fingerprintFiles([...paths, ...dirs]);
1225
292
  }
1226
293
  async load() {
@@ -1295,7 +362,7 @@ ${new Error("trace").stack}`
1295
362
  }
1296
363
  const now = /* @__PURE__ */ new Date();
1297
364
  for (const { path } of aggregates) {
1298
- await rename3(path, join4(dirname2(path), migratedAggregateName(now)));
365
+ await rename(path, join(dirname(path), migratedAggregateName(now)));
1299
366
  forgetFile(path);
1300
367
  console.log(`[conversation-store] ${path} converti en un fichier par conversation.`);
1301
368
  }
@@ -1323,18 +390,18 @@ ${new Error("trace").stack}`
1323
390
  unreadable.set(root, bucket);
1324
391
  };
1325
392
  const sources = await this.sourcePaths();
1326
- const legacyRoots = new Set(sources.filter((s) => s.legacy).map((s) => dirname2(s.path)));
393
+ const legacyRoots = new Set(sources.filter((s) => s.legacy).map((s) => dirname(s.path)));
1327
394
  const inheritedProject = legacyRoots.size > 0 ? await localProjectId() : null;
1328
395
  for (const { path, legacy } of sources) {
1329
396
  let data;
1330
397
  try {
1331
- data = JSON.parse(await readFile4(path, "utf-8"));
398
+ data = JSON.parse(await readFile(path, "utf-8"));
1332
399
  } catch {
1333
400
  continue;
1334
401
  }
1335
402
  aggregates.push({ path });
1336
403
  const inherited = legacy ? inheritedProject : null;
1337
- const root = dirname2(path);
404
+ const root = dirname(path);
1338
405
  await rememberFile(path);
1339
406
  for (const item of data) {
1340
407
  try {
@@ -1357,14 +424,14 @@ ${new Error("trace").stack}`
1357
424
  continue;
1358
425
  }
1359
426
  for (const name of names) {
1360
- const path = join4(dir, name);
427
+ const path = join(dir, name);
1361
428
  const stamp = await fileStamp(path);
1362
429
  if (!stamp) continue;
1363
430
  let entry = this.parsed.get(path);
1364
431
  if (!entry || entry.stamp !== stamp) {
1365
432
  let item2;
1366
433
  try {
1367
- item2 = JSON.parse(await readFile4(path, "utf-8"));
434
+ item2 = JSON.parse(await readFile(path, "utf-8"));
1368
435
  } catch {
1369
436
  continue;
1370
437
  }
@@ -1479,7 +546,7 @@ ${new Error("trace").stack}`
1479
546
  if (previous && previous !== path) removed.push(previous);
1480
547
  }
1481
548
  for (const path of removed) {
1482
- await rm2(path, { force: true });
549
+ await rm(path, { force: true });
1483
550
  forgetFile(path);
1484
551
  this.parsed.delete(path);
1485
552
  }
@@ -1494,7 +561,7 @@ ${new Error("trace").stack}`
1494
561
  async rootFor(conv) {
1495
562
  const known = this.roots.get(conv.id) ?? null;
1496
563
  if (conv.projectId) return rootOfProjectAmong(conv.projectId, known);
1497
- return known ?? dirname2(await keyedFilePath(FILENAME4));
564
+ return known ?? dirname(await keyedFilePath(FILENAME));
1498
565
  }
1499
566
  /**
1500
567
  * Le projet d'une conversation, organisation comprise.
@@ -1569,7 +636,7 @@ ${new Error("trace").stack}`
1569
636
  await this.load();
1570
637
  const now = Date.now();
1571
638
  const conv = {
1572
- id: randomUUID3(),
639
+ id: randomUUID(),
1573
640
  projectId,
1574
641
  title: title ?? "New conversation",
1575
642
  messages: [],
@@ -1746,13 +813,13 @@ ${new Error("trace").stack}`
1746
813
  var conversationStore = new ConversationStore();
1747
814
 
1748
815
  // ../server/src/services/workspace-cwd.ts
1749
- import { isAbsolute as isAbsolute2, join as join5, dirname as dirname3 } from "path";
816
+ import { isAbsolute, join as join2, dirname as dirname2 } from "path";
1750
817
  async function projectRoot(ref) {
1751
818
  const project = await projectStore.get(ref);
1752
819
  if (!project) return null;
1753
820
  if (!project.orgId) {
1754
821
  const root = vaultFor(project).root;
1755
- return root === localVaultRoot() ? null : dirname3(root);
822
+ return root === localVaultRoot() ? null : dirname2(root);
1756
823
  }
1757
824
  return projectPathStore.get(project);
1758
825
  }
@@ -1777,11 +844,11 @@ async function resolveWorkspaceCwd(target) {
1777
844
  }
1778
845
  function anchor(base, raw) {
1779
846
  if (!raw || raw === ".") return base;
1780
- if (raw.startsWith("~") || raw.startsWith("$") || isAbsolute2(raw)) return raw;
1781
- return join5(base, raw);
847
+ if (raw.startsWith("~") || raw.startsWith("$") || isAbsolute(raw)) return raw;
848
+ return join2(base, raw);
1782
849
  }
1783
850
  function localVaultRootParent() {
1784
- return dirname3(localVaultRoot());
851
+ return dirname2(localVaultRoot());
1785
852
  }
1786
853
  async function resolveAgentCwd(target) {
1787
854
  if (target.serviceId) {
@@ -1809,8 +876,8 @@ async function resolveConversationCwd(conversationId) {
1809
876
  }
1810
877
 
1811
878
  // ../server/src/services/turn-events.ts
1812
- import { EventEmitter as EventEmitter3 } from "events";
1813
- var bus = new EventEmitter3();
879
+ import { EventEmitter as EventEmitter2 } from "events";
880
+ var bus = new EventEmitter2();
1814
881
  bus.setMaxListeners(0);
1815
882
  function emitTurnFinished(conversationId) {
1816
883
  bus.emit("turn-finished", conversationId);
@@ -1823,11 +890,11 @@ function onTurnFinished(listener) {
1823
890
  }
1824
891
 
1825
892
  // ../server/src/services/claude-transcript-sync.ts
1826
- import { readdir as readdir2, access, readFile as readFile5 } from "fs/promises";
893
+ import { readdir as readdir2, access, readFile as readFile2 } from "fs/promises";
1827
894
  import { watch } from "fs";
1828
895
  import { homedir } from "os";
1829
- import { join as join6 } from "path";
1830
- var PROJECTS_DIR = join6(homedir(), ".claude", "projects");
896
+ import { join as join3 } from "path";
897
+ var PROJECTS_DIR = join3(homedir(), ".claude", "projects");
1831
898
  var DEFAULT_TITLES = /* @__PURE__ */ new Set(["", "New conversation", "Nouvelle conversation"]);
1832
899
  async function findClaudeSessionFile(sessionId) {
1833
900
  let entries;
@@ -1838,7 +905,7 @@ async function findClaudeSessionFile(sessionId) {
1838
905
  }
1839
906
  const filename = `${sessionId}.jsonl`;
1840
907
  for (const dir of entries) {
1841
- const candidate = join6(PROJECTS_DIR, dir, filename);
908
+ const candidate = join3(PROJECTS_DIR, dir, filename);
1842
909
  try {
1843
910
  await access(candidate);
1844
911
  return candidate;
@@ -1853,7 +920,7 @@ async function claudeSessionExists(sessionId) {
1853
920
  async function readClaudeTranscript(sessionId) {
1854
921
  const path = await findClaudeSessionFile(sessionId);
1855
922
  if (!path) return { found: false, path: null, entries: [] };
1856
- const raw = await readFile5(path, "utf-8");
923
+ const raw = await readFile2(path, "utf-8");
1857
924
  const entries = [];
1858
925
  for (const line of raw.split("\n")) {
1859
926
  if (!line.trim()) continue;
@@ -2028,7 +1095,7 @@ async function startClaudeTranscriptWatcher() {
2028
1095
  }
2029
1096
  for (const dir of topLevel) {
2030
1097
  try {
2031
- const w = watch(join6(PROJECTS_DIR, dir), (_event, filename) => {
1098
+ const w = watch(join3(PROJECTS_DIR, dir), (_event, filename) => {
2032
1099
  handleFsEvent(filename ? filename.toString() : null);
2033
1100
  });
2034
1101
  w.on("error", () => {
@@ -2053,7 +1120,7 @@ function stopClaudeTranscriptWatcher() {
2053
1120
 
2054
1121
  // ../server/src/services/codex-transcript-sync.ts
2055
1122
  import { watch as watch2 } from "fs";
2056
- import { dirname as dirname4 } from "path";
1123
+ import { dirname as dirname3 } from "path";
2057
1124
  var announced = /* @__PURE__ */ new Map();
2058
1125
  function forgetCodexTurns(conversationId) {
2059
1126
  announced.delete(conversationId);
@@ -2107,7 +1174,7 @@ function startCodexTranscriptWatcher() {
2107
1174
  const db = findThreadDb();
2108
1175
  if (!db) return;
2109
1176
  try {
2110
- watcher = watch2(dirname4(db), (_event, filename) => {
1177
+ watcher = watch2(dirname3(db), (_event, filename) => {
2111
1178
  if (!filename || !/^thread_history_\d+\.sqlite/.test(filename.toString())) return;
2112
1179
  if (debounce) clearTimeout(debounce);
2113
1180
  debounce = setTimeout(() => {
@@ -2220,6 +1287,100 @@ async function findLivePty(sessionId) {
2220
1287
  return null;
2221
1288
  }
2222
1289
 
1290
+ // ../server/src/services/pty-input.ts
1291
+ var PTY_SUBMIT_DELAY_MS = 120;
1292
+ function flattenForPty(message) {
1293
+ return message.split(/\n{2,}/).map((block) => block.replace(/\s*\n\s*/g, " ").trim()).filter(Boolean).join(" \xB7 ").replace(/\s{2,}/g, " ").trim();
1294
+ }
1295
+ async function submitToPty(write, message) {
1296
+ await write(flattenForPty(message));
1297
+ await new Promise((r) => setTimeout(r, PTY_SUBMIT_DELAY_MS));
1298
+ await write("\r");
1299
+ }
1300
+ var PASTE_START = "\x1B[200~";
1301
+ var PASTE_END = "\x1B[201~";
1302
+ async function pasteToPty(write, text, options = {}) {
1303
+ if (options.lead) await write(`${options.lead} `);
1304
+ await write(`${PASTE_START}${text}${PASTE_END}`);
1305
+ const delay = Math.min(PTY_SUBMIT_DELAY_MS + Math.ceil(text.length / 100), 3e3);
1306
+ await new Promise((r) => setTimeout(r, delay));
1307
+ await write("\r");
1308
+ }
1309
+ var PTY_INPUT_MIN_MS = { "claude-code": 1200, codex: 500 };
1310
+ var PTY_INPUT_QUIET_MS = 400;
1311
+ var PTY_INPUT_MAX_MS = 8e3;
1312
+ var PTY_MCP_READY_MAX_MS = 15e3;
1313
+ var PTY_QUIET_AFTER_MCP_MAX_MS = 3e3;
1314
+ async function waitForPtyInput(opts) {
1315
+ const { agentId, spawnId, mcpReadyId, runner, signal } = opts;
1316
+ if (signal?.aborted) return { status: "aborted" };
1317
+ const { agentManager: agentManager2 } = await import("./agent-manager-46C4NIQ2.js");
1318
+ const { waitForMcpReady } = await import("./mcp-ready-TAPBAOZY.js");
1319
+ return new Promise((resolve) => {
1320
+ const startedAt = Date.now();
1321
+ const floor = PTY_INPUT_MIN_MS[runner];
1322
+ const inner = new AbortController();
1323
+ let done = false;
1324
+ let mcpReady;
1325
+ let quietArmed = !mcpReadyId;
1326
+ let quietTimer = null;
1327
+ let maxTimer = null;
1328
+ let unsub = null;
1329
+ const finish = (status) => {
1330
+ if (done) return;
1331
+ done = true;
1332
+ if (quietTimer) clearTimeout(quietTimer);
1333
+ if (maxTimer) clearTimeout(maxTimer);
1334
+ signal?.removeEventListener("abort", onAbort);
1335
+ inner.abort();
1336
+ unsub?.();
1337
+ resolve(mcpReadyId ? { status, mcpReady: mcpReady ?? false } : { status });
1338
+ };
1339
+ const onAbort = () => finish("aborted");
1340
+ const scheduleQuiet = () => {
1341
+ if (!quietArmed || done) return;
1342
+ if (quietTimer) clearTimeout(quietTimer);
1343
+ const wait = Math.max(PTY_INPUT_QUIET_MS, floor - (Date.now() - startedAt));
1344
+ quietTimer = setTimeout(() => finish("ready"), wait);
1345
+ };
1346
+ signal?.addEventListener("abort", onAbort, { once: true });
1347
+ try {
1348
+ unsub = agentManager2.agentSubscribe(
1349
+ agentId,
1350
+ "ai.stream",
1351
+ { spawnId, fromOffset: 0 },
1352
+ (data) => {
1353
+ const chunk = data.chunk;
1354
+ if (chunk?.type === "exit") {
1355
+ finish("exited");
1356
+ return;
1357
+ }
1358
+ scheduleQuiet();
1359
+ },
1360
+ () => {
1361
+ }
1362
+ );
1363
+ } catch {
1364
+ }
1365
+ if (done) {
1366
+ unsub?.();
1367
+ return;
1368
+ }
1369
+ if (mcpReadyId) {
1370
+ void waitForMcpReady(mcpReadyId, PTY_MCP_READY_MAX_MS, inner.signal).then((ready) => {
1371
+ if (done) return;
1372
+ mcpReady = ready;
1373
+ quietArmed = true;
1374
+ scheduleQuiet();
1375
+ maxTimer = setTimeout(() => finish("ready"), PTY_QUIET_AFTER_MCP_MAX_MS);
1376
+ });
1377
+ } else {
1378
+ maxTimer = setTimeout(() => finish("ready"), PTY_INPUT_MAX_MS);
1379
+ }
1380
+ });
1381
+ }
1382
+ var PASTED_TASK_LEAD = "Here is your task. Follow the instructions below:";
1383
+
2223
1384
  // ../server/src/services/active-session-registry.ts
2224
1385
  var MAX_BUFFER = 5e3;
2225
1386
  var PERSIST_DEBOUNCE_MS = 100;
@@ -2637,10 +1798,13 @@ function companionAgentId(livePtyAgentId, lastSessionAgentId) {
2637
1798
  function trace(conversationId, reason) {
2638
1799
  console.log(`[companion] ${conversationId.slice(0, 8)} \u2014 ${reason}`);
2639
1800
  }
1801
+ function companionApplies(conv) {
1802
+ return !conv.isCompanion && conv.workflowType !== "scenario";
1803
+ }
2640
1804
  async function runCompanionForTurn(conversationId) {
2641
1805
  const conv = await conversationStore.get(conversationId);
2642
1806
  if (!conv) return trace(conversationId, "conversation introuvable");
2643
- if (conv.isCompanion) return;
1807
+ if (!companionApplies(conv)) return;
2644
1808
  if (!conv.companionEnabled) return;
2645
1809
  const stopsAtStart = stops.get(conversationId) ?? 0;
2646
1810
  const stopped = () => (stops.get(conversationId) ?? 0) !== stopsAtStart;
@@ -2649,7 +1813,7 @@ async function runCompanionForTurn(conversationId) {
2649
1813
  if (!hasNew) {
2650
1814
  return skip(conversationId, "nothingNew", `rien de neuf \xE0 relire (${messageCount} messages)`);
2651
1815
  }
2652
- const { startCliSession } = await import("./chat-LPML22J5.js");
1816
+ const { startCliSession } = await import("./chat-CMNDMXIP.js");
2653
1817
  const companionConv = await ensureCompanionConversation(conv);
2654
1818
  const exchange = formatExchange(conv.messages, from);
2655
1819
  const baseSha = await ensureCompanionBase(conv);
@@ -2700,11 +1864,7 @@ async function runCompanionForTurn(conversationId) {
2700
1864
  endThinking();
2701
1865
  }
2702
1866
  }
2703
- var PTY_SUBMIT_DELAY_MS = 120;
2704
- function flattenForPty(message) {
2705
- return message.split(/\n{2,}/).map((block) => block.replace(/\s*\n\s*/g, " ").trim()).filter(Boolean).join(" \xB7 ").replace(/\s{2,}/g, " ").trim();
2706
- }
2707
- var bus2 = new EventEmitter4();
1867
+ var bus2 = new EventEmitter3();
2708
1868
  bus2.setMaxListeners(0);
2709
1869
  var thinking = /* @__PURE__ */ new Map();
2710
1870
  function companionThinkingIds() {
@@ -2768,7 +1928,7 @@ async function deliverToMainConversation(conv, message) {
2768
1928
  async function attemptDelivery(conv, message) {
2769
1929
  if (await writeToLivePty(conv.id, message)) return "pty";
2770
1930
  try {
2771
- const { startCliSession } = await import("./chat-LPML22J5.js");
1931
+ const { startCliSession } = await import("./chat-CMNDMXIP.js");
2772
1932
  await startCliSession(conv.runner, conv.id, message, { jwt: await signJwt() });
2773
1933
  return "headless";
2774
1934
  } catch (err) {
@@ -2781,9 +1941,7 @@ async function writeToLivePty(conversationId, message) {
2781
1941
  if (!pty) return false;
2782
1942
  const write = (data) => agentManager.agentMutation(pty.agentId, "ai.writePty", { spawnId: pty.spawnId, data });
2783
1943
  try {
2784
- await write(flattenForPty(message));
2785
- await new Promise((r) => setTimeout(r, PTY_SUBMIT_DELAY_MS));
2786
- await write("\r");
1944
+ await submitToPty(write, message);
2787
1945
  return true;
2788
1946
  } catch {
2789
1947
  return false;
@@ -2903,7 +2061,7 @@ var capturingBase = /* @__PURE__ */ new Set();
2903
2061
  var baseTriedOnChange = /* @__PURE__ */ new Set();
2904
2062
  async function ensureCompanionBase(conv) {
2905
2063
  if (conv.companionBaseSha) return conv.companionBaseSha;
2906
- if (conv.isCompanion || !conv.companionEnabled || capturingBase.has(conv.id)) return void 0;
2064
+ if (!companionApplies(conv) || !conv.companionEnabled || capturingBase.has(conv.id)) return void 0;
2907
2065
  capturingBase.add(conv.id);
2908
2066
  try {
2909
2067
  const agentId = conv.agentId ?? agentManager.getDefaultAgentId();
@@ -2948,12 +2106,6 @@ export {
2948
2106
  signJwt,
2949
2107
  setMcpClientCheck,
2950
2108
  verifyJwt,
2951
- ensureDataGitignore,
2952
- conversationSectionStore,
2953
- holdsRuneyaProject,
2954
- projectPathStore,
2955
- LocalProjectRefusedError,
2956
- projectStore,
2957
2109
  CONVERSATIONS_DIRNAME,
2958
2110
  conversationStore,
2959
2111
  resolveWorkspaceCwd,
@@ -2973,6 +2125,11 @@ export {
2973
2125
  noteSpawnConversation,
2974
2126
  observePtyChunk,
2975
2127
  forgetSpawn,
2128
+ flattenForPty,
2129
+ pasteToPty,
2130
+ PTY_MCP_READY_MAX_MS,
2131
+ waitForPtyInput,
2132
+ PASTED_TASK_LEAD,
2976
2133
  activeSessionRegistry,
2977
2134
  MAX_CONSECUTIVE_RELAUNCHES,
2978
2135
  MAX_EXCHANGE_CHARS,
@@ -2989,7 +2146,7 @@ export {
2989
2146
  buildRelaunchMessage,
2990
2147
  companionSystemNote,
2991
2148
  companionAgentId,
2992
- flattenForPty,
2149
+ companionApplies,
2993
2150
  companionThinkingIds,
2994
2151
  isCompanionThinking,
2995
2152
  onCompanionThinkingChanged,
@@ -3007,4 +2164,4 @@ export {
3007
2164
  headCommit,
3008
2165
  initCompanion
3009
2166
  };
3010
- //# sourceMappingURL=chunk-UD42LACH.js.map
2167
+ //# sourceMappingURL=chunk-JNK6H5KH.js.map