@runeya/runeya 2.0.121 → 2.0.123

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.
@@ -1,8 +1,9 @@
1
1
  import {
2
+ atomicWriteSecure,
2
3
  hasChangedSinceRead,
3
4
  rememberFile,
4
5
  wasRead
5
- } from "./chunk-YZU5FNXR.js";
6
+ } from "./chunk-HK5QNU6P.js";
6
7
  import {
7
8
  SchemaVersionSchema,
8
9
  SettingsPrivateSchema,
@@ -17,7 +18,7 @@ import {
17
18
  } from "./chunk-ZFELQG4B.js";
18
19
 
19
20
  // ../server/src/services/settings-manager.ts
20
- import { readFile, writeFile, rename, mkdir, chmod } from "fs/promises";
21
+ import { readFile } from "fs/promises";
21
22
  import { join } from "path";
22
23
  var SETTINGS_FILE = "settings.json";
23
24
  var SETTINGS_PRIVATE_FILE = "settings.private.json";
@@ -42,13 +43,8 @@ var SettingsManager = class {
42
43
  return vault ?? "";
43
44
  }
44
45
  async atomicWrite(filename, data, vault) {
45
- const root = this.rootOf(vault);
46
- await mkdir(root, { recursive: true });
47
- const filePath = join(root, filename);
48
- const tmpPath = filePath + ".tmp";
49
- await writeFile(tmpPath, JSON.stringify(data, null, 2), "utf-8");
50
- await rename(tmpPath, filePath);
51
- await chmod(filePath, 384);
46
+ const filePath = join(this.rootOf(vault), filename);
47
+ await atomicWriteSecure(filePath, JSON.stringify(data, null, 2));
52
48
  await rememberFile(filePath);
53
49
  }
54
50
  /**
@@ -164,12 +160,7 @@ var SettingsManager = class {
164
160
  }
165
161
  async saveSchemaVersion(sv) {
166
162
  this.schemaVersion = sv;
167
- await mkdir(machineRoot(), { recursive: true });
168
- const filePath = machineFilePath(SCHEMA_VERSION_FILE);
169
- const tmpPath = filePath + ".tmp";
170
- await writeFile(tmpPath, JSON.stringify(sv, null, 2), "utf-8");
171
- await rename(tmpPath, filePath);
172
- await chmod(filePath, 384);
163
+ await atomicWriteSecure(machineFilePath(SCHEMA_VERSION_FILE), JSON.stringify(sv, null, 2));
173
164
  }
174
165
  getCurrentSchemaVersion() {
175
166
  return CURRENT_SCHEMA_VERSION;
@@ -180,4 +171,4 @@ var settingsManager = new SettingsManager();
180
171
  export {
181
172
  settingsManager
182
173
  };
183
- //# sourceMappingURL=chunk-3N55WR7F.js.map
174
+ //# sourceMappingURL=chunk-N3ZI35SC.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../server/src/services/settings-manager.ts"],"sourcesContent":["import { readFile } from 'node:fs/promises';\nimport { env } from '../config/env.js';\nimport { join } from 'node:path';\nimport { machineFilePath, legacyMachineFilePath, machineRoot, orgVaultRoot } from './vault-resolver.js';\nimport { SettingsSchema, SchemaVersionSchema, SettingsPrivateSchema } from '@runeya/packages-shared';\nimport type { Settings, SchemaVersion, SettingsPrivate } from '@runeya/packages-shared';\nimport { rememberFile, hasChangedSinceRead, wasRead } from './disk-freshness.js';\nimport { atomicWriteSecure } from './run-store-utils.js';\n\nconst SETTINGS_FILE = 'settings.json';\nconst SETTINGS_PRIVATE_FILE = 'settings.private.json';\nconst SCHEMA_VERSION_FILE = 'schema-version.json';\nconst CURRENT_SCHEMA_VERSION = 4;\n\n/**\n * Les réglages vivent dans le coffre de ce qu'ils configurent.\n *\n * Ils étaient au niveau machine — un fichier pour tout le poste — alors qu'ils\n * décrivent des choses qui appartiennent à des organisations différentes : les\n * fournisseurs d'IA qui paient le travail d'une équipe, les scénarios qui\n * décrivent ses services. Un réglage global obligeait donc à partager la clé\n * machine pour partager un secret d'organisation.\n *\n * `vault` désigne le coffre : l'identifiant d'une organisation, ou l'absence\n * pour le répertoire de lancement — celui d'un projet local, qui n'appartient à\n * personne d'autre. Rien ne reste au niveau machine sinon la session cloud,\n * irréductible puisque c'est elle qui donne accès aux organisations.\n */\nclass SettingsManager {\n private settings = new Map<string, Settings>();\n private settingsPrivate = new Map<string, SettingsPrivate>();\n private schemaVersion: SchemaVersion | null = null;\n\n /**\n * Le coffre visé, sous forme de racine sur le disque.\n *\n * Sans organisation, la racine machine — pas le répertoire de lancement. Ce\n * n'est pas un reste de l'ancien monde : y écrire forcerait un `.runeya` à\n * exister chez quelqu'un qui ne travaille que sur des projets cloud, ce que\n * les coffres ont justement supprimé (et ce qu'un test garde).\n */\n private rootOf(vault?: string): string {\n return vault ? orgVaultRoot(vault) : machineRoot();\n }\n\n private cacheKey(vault?: string): string {\n return vault ?? '';\n }\n\n private async atomicWrite(filename: string, data: unknown, vault?: string): Promise<void> {\n const filePath = join(this.rootOf(vault), filename);\n await atomicWriteSecure(filePath, JSON.stringify(data, null, 2));\n // Notre propre écriture devient la référence : sans elle, la lecture\n // suivante prendrait ce fichier pour l'œuvre d'une autre instance et\n // relirait pour rien.\n await rememberFile(filePath);\n }\n\n /**\n * Le cache de ce coffre fait-il encore foi ?\n *\n * Plusieurs Runeya partagent la racine machine — l'application installée et\n * celle qu'on développe, couramment — et écrivent donc le même fichier. Un\n * cache peuplé une fois et jamais revu rend l'état du démarrage pour\n * toujours, et la sauvegarde suivante le réinstalle sur le disque : ce que\n * l'autre instance avait écrit entre-temps disparaît. Les réglages sont\n * l'endroit où cela coûte le plus cher, parce qu'un coffre n'a qu'un fichier\n * pour toute l'organisation — un projet, lui, a le sien.\n *\n * Un fichier jamais lu n'est pas périmé : c'est un coffre neuf, dont les\n * défauts viennent d'être écrits.\n */\n private async isStale(filename: string, vault?: string): Promise<boolean> {\n const path = join(this.rootOf(vault), filename);\n return wasRead(path) && await hasChangedSinceRead(path);\n }\n\n /**\n * Lire le fichier de CE coffre, et rien d'autre.\n *\n * Pas de repli sur un autre emplacement : un réglage lu ailleurs que là où il\n * est écrit fait deux vérités pour un même fait, et l'accord entre les deux\n * devient une chose de plus à tenir. Ce que les versions précédentes ont\n * écrit ailleurs est déplacé une fois pour toutes par\n * `migrateSettingsIntoVaults`, pas relu indéfiniment.\n */\n private async readJson<T>(filename: string, vault?: string): Promise<T | null> {\n const path = join(this.rootOf(vault), filename);\n try {\n const parsed = JSON.parse(await readFile(path, 'utf-8')) as T;\n // Repère pris après coup : c'est cette lecture-là que le cache reflète.\n await rememberFile(path);\n return parsed;\n } catch {\n return null;\n }\n }\n\n /** Vide les caches — après un déplacement de fichiers sous nos pieds. */\n forget(): void {\n this.settings.clear();\n this.settingsPrivate.clear();\n }\n\n /** Un fichier qui appartient au poste, pas à un coffre. */\n private async readMachineJson<T>(filename: string): Promise<T | null> {\n for (const path of [machineFilePath(filename), legacyMachineFilePath(filename)]) {\n try {\n return JSON.parse(await readFile(path, 'utf-8')) as T;\n } catch {\n // Try the next location.\n }\n }\n return null;\n }\n\n async getSettings(vault?: string): Promise<Settings> {\n const key = this.cacheKey(vault);\n let current = this.settings.get(key);\n if (current && await this.isStale(SETTINGS_FILE, vault)) current = undefined;\n if (!current) {\n const raw = await this.readJson<unknown>(SETTINGS_FILE, vault);\n const parsed = raw ? SettingsSchema.safeParse(raw) : null;\n if (parsed?.success) {\n current = parsed.data;\n this.settings.set(key, current);\n } else {\n current = SettingsSchema.parse({ mode: env.MODE, cloudUrl: env.CLOUD_URL ?? null });\n await this.saveSettings(current, vault);\n }\n }\n return current;\n }\n\n async saveSettings(settings: Settings, vault?: string): Promise<void> {\n this.settings.set(this.cacheKey(vault), settings);\n await this.atomicWrite(SETTINGS_FILE, settings, vault);\n }\n\n async getPrivateSettings(vault?: string): Promise<SettingsPrivate> {\n const key = this.cacheKey(vault);\n let current = this.settingsPrivate.get(key);\n if (current && await this.isStale(SETTINGS_PRIVATE_FILE, vault)) current = undefined;\n if (!current) {\n const raw = await this.readJson<unknown>(SETTINGS_PRIVATE_FILE, vault);\n const parsed = raw ? SettingsPrivateSchema.safeParse(raw) : null;\n if (parsed?.success) {\n current = parsed.data;\n this.settingsPrivate.set(key, current);\n } else {\n current = SettingsPrivateSchema.parse({});\n await this.savePrivateSettings(current, vault);\n }\n }\n return current;\n }\n\n async savePrivateSettings(settings: SettingsPrivate, vault?: string): Promise<void> {\n this.settingsPrivate.set(this.cacheKey(vault), settings);\n await this.atomicWrite(SETTINGS_PRIVATE_FILE, settings, vault);\n }\n\n /**\n * La version de schéma reste au niveau machine, et ce n'est pas un oubli :\n * elle ne configure rien, elle date les migrations des données de ce poste.\n */\n async getSchemaVersion(): Promise<SchemaVersion> {\n if (!this.schemaVersion) {\n const raw = await this.readMachineJson<unknown>(SCHEMA_VERSION_FILE);\n const parsed = raw ? SchemaVersionSchema.safeParse(raw) : null;\n if (parsed?.success) {\n this.schemaVersion = parsed.data;\n } else {\n this.schemaVersion = { version: CURRENT_SCHEMA_VERSION, migratedAt: new Date() };\n await this.saveSchemaVersion(this.schemaVersion);\n }\n }\n return this.schemaVersion;\n }\n\n async saveSchemaVersion(sv: SchemaVersion): Promise<void> {\n this.schemaVersion = sv;\n await atomicWriteSecure(machineFilePath(SCHEMA_VERSION_FILE), JSON.stringify(sv, null, 2));\n }\n\n getCurrentSchemaVersion(): number {\n return CURRENT_SCHEMA_VERSION;\n }\n}\n\nexport const settingsManager = new SettingsManager();\n"],"mappings":"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","names":[]}
@@ -6,6 +6,7 @@ import {
6
6
  } from "./chunk-MD3VW4HQ.js";
7
7
  import {
8
8
  AmbiguousProjectError,
9
+ StoreDiskCoordinator,
9
10
  agentManager,
10
11
  decryptValue,
11
12
  encryptValue,
@@ -21,13 +22,14 @@ import {
21
22
  serviceStore,
22
23
  setProjectServices,
23
24
  setProjectVault
24
- } from "./chunk-CNTL6NVU.js";
25
+ } from "./chunk-MSLFR2EN.js";
25
26
  import {
27
+ atomicWriteSecure,
26
28
  fingerprintFiles,
27
29
  hasChangedSinceRead,
28
30
  rememberFile,
29
31
  wasRead
30
- } from "./chunk-YZU5FNXR.js";
32
+ } from "./chunk-HK5QNU6P.js";
31
33
  import {
32
34
  COMPANION_RELAY_CLOSE,
33
35
  COMPANION_RELAY_OPEN,
@@ -53,14 +55,14 @@ import { EventEmitter as EventEmitter4 } from "events";
53
55
 
54
56
  // ../server/src/services/conversation-store.ts
55
57
  import { randomUUID as randomUUID2 } from "crypto";
56
- import { readFile as readFile3, writeFile as writeFile4, rename as rename3, mkdir as mkdir3, chmod as chmod3 } from "fs/promises";
58
+ import { readFile as readFile3, mkdir as mkdir2 } from "fs/promises";
57
59
  import { dirname, join as join4 } from "path";
58
60
  import { EventEmitter as EventEmitter2 } from "events";
59
61
 
60
62
  // ../server/src/services/project-store.ts
61
63
  import { randomUUID } from "crypto";
62
64
  import { EventEmitter } from "events";
63
- import { readFile as readFile2, writeFile as writeFile3, rename as rename2, mkdir as mkdir2, chmod as chmod2, rm } from "fs/promises";
65
+ import { readFile as readFile2, rename as rename2, rm } from "fs/promises";
64
66
  import { join as join3 } from "path";
65
67
 
66
68
  // ../server/src/utils/ensure-data-gitignore.ts
@@ -280,22 +282,17 @@ var ProjectStore = class {
280
282
  * côté existe pour lui.
281
283
  */
282
284
  fingerprint = "";
285
+ disk = new StoreDiskCoordinator();
283
286
  /** Emits `'saved'` after every persisted mutation, for the cloud sync bridge. */
284
287
  events = new EventEmitter();
285
288
  getFilePath(root = localVaultRoot()) {
286
289
  return join3(root, FILENAME2);
287
290
  }
288
- async writeAtomic(filePath, data) {
289
- const tmpPath = filePath + ".tmp";
290
- await writeFile3(tmpPath, data, "utf-8");
291
- await rename2(tmpPath, filePath);
292
- await chmod2(filePath, 384);
293
- }
294
291
  /** Adopt a project read from disk, remembering which vault it came from. */
295
- async absorb(p, root) {
292
+ async absorb(into, p, root) {
296
293
  const projectEnvs = await environmentStore.getByProjectId(p.id).catch(() => []);
297
294
  const defaultEnv = projectEnvs.find((e) => e.default);
298
- this.projects.set(projectKey(p), {
295
+ into.set(projectKey(p), {
299
296
  ...p,
300
297
  environmentIds: p.environmentIds ?? [],
301
298
  groupOrder: p.groupOrder ?? [],
@@ -333,46 +330,47 @@ var ProjectStore = class {
333
330
  * reviendrait à exiger un redémarrage pour les voir.
334
331
  */
335
332
  async load() {
336
- const fingerprint = await this.diskFingerprint();
337
- if (this.loaded && fingerprint === this.fingerprint) return;
338
- const previous = this.projects;
339
- this.projects = /* @__PURE__ */ new Map();
340
- this.loaded = true;
341
- this.fingerprint = fingerprint;
342
- try {
343
- await this.readVaults(previous);
344
- } catch (err) {
345
- this.projects = previous;
346
- this.fingerprint = "";
347
- throw err;
333
+ let migrated = false;
334
+ await this.disk.load(async () => {
335
+ const fingerprint = await this.diskFingerprint();
336
+ if (this.loaded && fingerprint === this.fingerprint) return null;
337
+ const next = await this.readVaults();
338
+ return () => {
339
+ for (const [key, project] of next.projects) {
340
+ const before = this.projects.get(key)?.activeEnvironmentId;
341
+ if (before && project.environmentIds.includes(before)) {
342
+ project.activeEnvironmentId = before;
343
+ }
344
+ }
345
+ this.projects = next.projects;
346
+ this.fingerprint = fingerprint;
347
+ this.loaded = true;
348
+ migrated = next.migrated;
349
+ };
350
+ });
351
+ if (migrated) {
352
+ await this.save();
353
+ await serviceStore.relocateAll();
354
+ await environmentStore.relocateAll();
348
355
  }
349
356
  }
350
- /** Le parcours proprement dit, isolé pour que `load` puisse le rattraper. */
351
- async readVaults(previous) {
357
+ /** Le parcours proprement dit, dans une table neuve que `load` substitue. */
358
+ async readVaults() {
359
+ const projects = /* @__PURE__ */ new Map();
352
360
  const { local, strayCloud } = await readLocalProject();
353
- if (local) await this.absorb(local, localVaultRoot());
361
+ if (local) await this.absorb(projects, local, localVaultRoot());
354
362
  let migrated = false;
355
363
  for (const project of strayCloud) {
356
364
  try {
357
- await this.absorb(project, vaultFor(project).root);
365
+ await this.absorb(projects, project, vaultFor(project).root);
358
366
  migrated = true;
359
367
  } catch {
360
368
  }
361
369
  }
362
370
  for (const { project, root } of await readCloudProjects()) {
363
- await this.absorb(project, root);
364
- }
365
- for (const [key, project] of this.projects) {
366
- const before = previous.get(key)?.activeEnvironmentId;
367
- if (before && project.environmentIds.includes(before)) {
368
- project.activeEnvironmentId = before;
369
- }
370
- }
371
- if (migrated) {
372
- await this.save();
373
- await serviceStore.relocateAll();
374
- await environmentStore.relocateAll();
371
+ await this.absorb(projects, project, root);
375
372
  }
373
+ return { projects, migrated };
376
374
  }
377
375
  /**
378
376
  * Persist every vault this store knows about.
@@ -381,7 +379,10 @@ var ProjectStore = class {
381
379
  * that moved between vaults — a local project adopted by an org, a cloud
382
380
  * project pulled for the first time — from lingering in the vault it left.
383
381
  */
384
- async save() {
382
+ save() {
383
+ return this.disk.save(() => this.writeVaults());
384
+ }
385
+ async writeVaults() {
385
386
  const locals = [];
386
387
  const cloud = [];
387
388
  for (const project of this.projects.values()) {
@@ -398,7 +399,7 @@ var ProjectStore = class {
398
399
  if (local) {
399
400
  await ensureLocalVault();
400
401
  await ensureDataGitignore(localVaultRoot());
401
- await this.writeAtomic(this.getFilePath(), JSON.stringify(toDiskForm(local), null, 2));
402
+ await atomicWriteSecure(this.getFilePath(), JSON.stringify(toDiskForm(local), null, 2));
402
403
  } else {
403
404
  await rm(this.getFilePath(), { force: true }).catch(() => {
404
405
  });
@@ -406,8 +407,7 @@ var ProjectStore = class {
406
407
  await rename2(join3(localVaultRoot(), LEGACY_FILENAME), join3(localVaultRoot(), LEGACY_FILENAME + ".bak")).catch(() => {
407
408
  });
408
409
  for (const { project, root } of cloud) {
409
- await mkdir2(root, { recursive: true });
410
- await this.writeAtomic(this.getFilePath(root), JSON.stringify(toDiskForm(project), null, 2));
410
+ await atomicWriteSecure(this.getFilePath(root), JSON.stringify(toDiskForm(project), null, 2));
411
411
  setProjectVault(project, root);
412
412
  }
413
413
  this.fingerprint = await this.diskFingerprint();
@@ -741,7 +741,7 @@ var ConversationStore = class {
741
741
  */
742
742
  fingerprint = "";
743
743
  events = new EventEmitter2().setMaxListeners(0);
744
- saveQueue = Promise.resolve();
744
+ disk = new StoreDiskCoordinator();
745
745
  snapshotConversation(conv) {
746
746
  return structuredClone(conv);
747
747
  }
@@ -770,7 +770,7 @@ var ConversationStore = class {
770
770
  }
771
771
  }
772
772
  async ensureDir() {
773
- await mkdir3(dirname(await this.getFilePath()), { recursive: true });
773
+ await mkdir2(dirname(await this.getFilePath()), { recursive: true });
774
774
  }
775
775
  encryptMessages(messages, vault) {
776
776
  return encryptValue(JSON.stringify(messages), vault);
@@ -878,16 +878,26 @@ ${new Error("trace").stack}`
878
878
  if (!getEncryptionKey()) {
879
879
  return;
880
880
  }
881
- const fingerprint = await this.diskFingerprint();
882
- if (this.loaded && fingerprint === this.fingerprint) return;
883
- this.loaded = true;
884
- this.fingerprint = fingerprint;
885
- this.unreadable.clear();
886
- await ensureProjectsLoaded();
887
- const sources = await this.sourcePaths();
888
- this.conversations.clear();
889
- this.roots.clear();
890
- for (const { path, legacy } of sources) {
881
+ await this.disk.load(async () => {
882
+ const fingerprint = await this.diskFingerprint();
883
+ if (this.loaded && fingerprint === this.fingerprint) return null;
884
+ await ensureProjectsLoaded();
885
+ const next = await this.readSources();
886
+ return () => {
887
+ this.conversations = next.conversations;
888
+ this.roots = next.roots;
889
+ this.unreadable = next.unreadable;
890
+ this.fingerprint = fingerprint;
891
+ this.loaded = true;
892
+ };
893
+ });
894
+ }
895
+ /** Lire toutes les sources dans des tables neuves, que `load` substitue d'un coup. */
896
+ async readSources() {
897
+ const conversations = /* @__PURE__ */ new Map();
898
+ const roots = /* @__PURE__ */ new Map();
899
+ const unreadable = /* @__PURE__ */ new Map();
900
+ for (const { path, legacy } of await this.sourcePaths()) {
891
901
  let data;
892
902
  try {
893
903
  data = JSON.parse(await readFile3(path, "utf-8"));
@@ -901,15 +911,16 @@ ${new Error("trace").stack}`
901
911
  try {
902
912
  const conv = this.fromDisk(item, vaultIdOfRoot(root));
903
913
  if (!conv.projectId && inherited) conv.projectId = inherited;
904
- this.conversations.set(item.id, conv);
905
- this.roots.set(item.id, root);
914
+ conversations.set(item.id, conv);
915
+ roots.set(item.id, root);
906
916
  } catch {
907
- const bucket = this.unreadable.get(root) ?? [];
917
+ const bucket = unreadable.get(root) ?? [];
908
918
  bucket.push(item);
909
- this.unreadable.set(root, bucket);
919
+ unreadable.set(root, bucket);
910
920
  }
911
921
  }
912
922
  }
923
+ return { conversations, roots, unreadable };
913
924
  }
914
925
  /**
915
926
  * Oublier ce qui a été lu, pour tout relire sous la clé courante.
@@ -976,11 +987,7 @@ ${new Error("trace").stack}`
976
987
  );
977
988
  continue;
978
989
  }
979
- await mkdir3(root, { recursive: true });
980
- const tmpPath = `${filePath}.tmp`;
981
- await writeFile4(tmpPath, JSON.stringify(data, null, 2), "utf-8");
982
- await rename3(tmpPath, filePath);
983
- await chmod3(filePath, 384);
990
+ await atomicWriteSecure(filePath, JSON.stringify(data, null, 2));
984
991
  await rememberFile(filePath);
985
992
  }
986
993
  this.fingerprint = await this.diskFingerprint();
@@ -1019,11 +1026,8 @@ ${new Error("trace").stack}`
1019
1026
  await this.load();
1020
1027
  await this.save();
1021
1028
  }
1022
- async save() {
1023
- const write = this.saveQueue.then(() => this.saveInternal());
1024
- this.saveQueue = write.catch(() => {
1025
- });
1026
- return write;
1029
+ save() {
1030
+ return this.disk.save(() => this.saveInternal());
1027
1031
  }
1028
1032
  async list() {
1029
1033
  await this.load();
@@ -2018,7 +2022,7 @@ async function runCompanionForTurn(conversationId) {
2018
2022
  if (!hasNew) {
2019
2023
  return skip(conversationId, "nothingNew", `rien de neuf \xE0 relire (${messageCount} messages)`);
2020
2024
  }
2021
- const { startCliSession } = await import("./chat-VL6BIVO5.js");
2025
+ const { startCliSession } = await import("./chat-MOAVENBI.js");
2022
2026
  const companionConv = await ensureCompanionConversation(conv);
2023
2027
  const isFirstTurn = (companionConv.messages?.length ?? 0) === 0;
2024
2028
  const exchange = formatExchange(conv.messages, from);
@@ -2132,7 +2136,7 @@ async function deliverToMainConversation(conv, message) {
2132
2136
  async function attemptDelivery(conv, message) {
2133
2137
  if (await writeToLivePty(conv.id, message)) return "pty";
2134
2138
  try {
2135
- const { startCliSession } = await import("./chat-VL6BIVO5.js");
2139
+ const { startCliSession } = await import("./chat-MOAVENBI.js");
2136
2140
  await startCliSession(conv.runner, conv.id, message, { jwt: await signJwt() });
2137
2141
  return "headless";
2138
2142
  } catch (err) {
@@ -2155,7 +2159,7 @@ async function writeToLivePty(conversationId, message) {
2155
2159
  }
2156
2160
  async function companionSettings() {
2157
2161
  try {
2158
- const { settingsManager } = await import("./settings-manager-ROWQ5GC2.js");
2162
+ const { settingsManager } = await import("./settings-manager-MAVD4GAS.js");
2159
2163
  const settings = await settingsManager.getSettings();
2160
2164
  return settings.companion ?? { enabledByDefault: false };
2161
2165
  } catch {
@@ -2284,4 +2288,4 @@ export {
2284
2288
  runCompanionNow,
2285
2289
  initCompanion
2286
2290
  };
2287
- //# sourceMappingURL=chunk-OVV52N5F.js.map
2291
+ //# sourceMappingURL=chunk-TNJFDWJX.js.map