@askexenow/exe-os 0.9.64 → 0.9.66

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 (105) hide show
  1. package/deploy/stack-manifests/v0.9.json +4 -4
  2. package/dist/bin/backfill-conversations.js +22 -0
  3. package/dist/bin/backfill-responses.js +22 -0
  4. package/dist/bin/backfill-vectors.js +22 -0
  5. package/dist/bin/cleanup-stale-review-tasks.js +22 -0
  6. package/dist/bin/cli.js +2280 -1199
  7. package/dist/bin/exe-agent-config.js +4 -0
  8. package/dist/bin/exe-agent.js +16 -0
  9. package/dist/bin/exe-assign.js +22 -0
  10. package/dist/bin/exe-boot.js +116 -7
  11. package/dist/bin/exe-call.js +16 -0
  12. package/dist/bin/exe-cloud.js +6671 -464
  13. package/dist/bin/exe-dispatch.js +24 -0
  14. package/dist/bin/exe-doctor.js +2845 -1223
  15. package/dist/bin/exe-export-behaviors.js +24 -0
  16. package/dist/bin/exe-forget.js +22 -0
  17. package/dist/bin/exe-gateway.js +24 -0
  18. package/dist/bin/exe-heartbeat.js +23 -0
  19. package/dist/bin/exe-kill.js +22 -0
  20. package/dist/bin/exe-launch-agent.js +24 -0
  21. package/dist/bin/exe-link.js +310 -178
  22. package/dist/bin/exe-new-employee.js +127 -1
  23. package/dist/bin/exe-pending-messages.js +22 -0
  24. package/dist/bin/exe-pending-notifications.js +22 -0
  25. package/dist/bin/exe-pending-reviews.js +22 -0
  26. package/dist/bin/exe-rename.js +22 -0
  27. package/dist/bin/exe-review.js +22 -0
  28. package/dist/bin/exe-search.js +24 -0
  29. package/dist/bin/exe-session-cleanup.js +24 -0
  30. package/dist/bin/exe-settings.js +10 -0
  31. package/dist/bin/exe-start-codex.js +135 -1
  32. package/dist/bin/exe-start-opencode.js +149 -1
  33. package/dist/bin/exe-status.js +22 -0
  34. package/dist/bin/exe-team.js +22 -0
  35. package/dist/bin/git-sweep.js +24 -0
  36. package/dist/bin/graph-backfill.js +22 -0
  37. package/dist/bin/graph-export.js +22 -0
  38. package/dist/bin/install.js +115 -1
  39. package/dist/bin/intercom-check.js +24 -0
  40. package/dist/bin/scan-tasks.js +24 -0
  41. package/dist/bin/setup.js +412 -157
  42. package/dist/bin/shard-migrate.js +22 -0
  43. package/dist/bin/update.js +4 -0
  44. package/dist/gateway/index.js +24 -0
  45. package/dist/hooks/bug-report-worker.js +135 -42
  46. package/dist/hooks/codex-stop-task-finalizer.js +24 -0
  47. package/dist/hooks/commit-complete.js +24 -0
  48. package/dist/hooks/error-recall.js +24 -0
  49. package/dist/hooks/exe-heartbeat-hook.js +4 -0
  50. package/dist/hooks/ingest-worker.js +4 -0
  51. package/dist/hooks/ingest.js +23 -0
  52. package/dist/hooks/instructions-loaded.js +22 -0
  53. package/dist/hooks/notification.js +22 -0
  54. package/dist/hooks/post-compact.js +22 -0
  55. package/dist/hooks/post-tool-combined.js +24 -0
  56. package/dist/hooks/pre-compact.js +260 -109
  57. package/dist/hooks/pre-tool-use.js +22 -0
  58. package/dist/hooks/prompt-submit.js +24 -0
  59. package/dist/hooks/session-end.js +161 -122
  60. package/dist/hooks/session-start.js +142 -0
  61. package/dist/hooks/stop.js +23 -0
  62. package/dist/hooks/subagent-stop.js +22 -0
  63. package/dist/hooks/summary-worker.js +195 -79
  64. package/dist/index.js +24 -0
  65. package/dist/lib/agent-config.js +4 -0
  66. package/dist/lib/cloud-sync.js +50 -6
  67. package/dist/lib/config.js +12 -0
  68. package/dist/lib/consolidation.js +4 -0
  69. package/dist/lib/database.js +4 -0
  70. package/dist/lib/db-daemon-client.js +4 -0
  71. package/dist/lib/db.js +4 -0
  72. package/dist/lib/device-registry.js +4 -0
  73. package/dist/lib/embedder.js +12 -0
  74. package/dist/lib/employee-templates.js +16 -0
  75. package/dist/lib/employees.js +4 -0
  76. package/dist/lib/exe-daemon-client.js +4 -0
  77. package/dist/lib/exe-daemon.js +1144 -480
  78. package/dist/lib/hybrid-search.js +24 -0
  79. package/dist/lib/identity.js +4 -0
  80. package/dist/lib/license.js +4 -0
  81. package/dist/lib/messaging.js +4 -0
  82. package/dist/lib/reminders.js +4 -0
  83. package/dist/lib/schedules.js +22 -0
  84. package/dist/lib/skill-learning.js +12 -0
  85. package/dist/lib/status-brief.js +39 -0
  86. package/dist/lib/store.js +22 -0
  87. package/dist/lib/task-router.js +4 -0
  88. package/dist/lib/tasks.js +12 -0
  89. package/dist/lib/tmux-routing.js +12 -0
  90. package/dist/lib/token-spend.js +4 -0
  91. package/dist/mcp/server.js +1045 -427
  92. package/dist/mcp/tools/complete-reminder.js +4 -0
  93. package/dist/mcp/tools/create-reminder.js +4 -0
  94. package/dist/mcp/tools/create-task.js +12 -0
  95. package/dist/mcp/tools/deactivate-behavior.js +4 -0
  96. package/dist/mcp/tools/list-reminders.js +4 -0
  97. package/dist/mcp/tools/list-tasks.js +4 -0
  98. package/dist/mcp/tools/send-message.js +4 -0
  99. package/dist/mcp/tools/update-task.js +12 -0
  100. package/dist/runtime/index.js +24 -0
  101. package/dist/tui/App.js +24 -0
  102. package/package.json +3 -2
  103. package/src/commands/exe/cloud.md +15 -8
  104. package/src/commands/exe/link.md +7 -6
  105. package/stack.release.json +2 -2
@@ -137,8 +137,8 @@ function deriveMachineKey() {
137
137
  }
138
138
  function readMachineId() {
139
139
  try {
140
- const { readFileSync: readFileSync8 } = __require("fs");
141
- return readFileSync8("/etc/machine-id", "utf-8").trim();
140
+ const { readFileSync: readFileSync9 } = __require("fs");
141
+ return readFileSync9("/etc/machine-id", "utf-8").trim();
142
142
  } catch {
143
143
  return "";
144
144
  }
@@ -460,6 +460,11 @@ function normalizeAutoUpdate(raw) {
460
460
  const userAU = raw.autoUpdate ?? {};
461
461
  raw.autoUpdate = { ...defaultAU, ...userAU };
462
462
  }
463
+ function normalizeOrchestration(raw) {
464
+ const defaultOrg = DEFAULT_CONFIG.orchestration;
465
+ const userOrg = raw.orchestration ?? {};
466
+ raw.orchestration = { ...defaultOrg, ...userOrg };
467
+ }
463
468
  async function loadConfig() {
464
469
  const dir = process.env.EXE_OS_DIR ?? process.env.EXE_MEM_DIR ?? EXE_AI_DIR;
465
470
  await ensurePrivateDir(dir);
@@ -484,6 +489,7 @@ async function loadConfig() {
484
489
  normalizeScalingRoadmap(migratedCfg);
485
490
  normalizeSessionLifecycle(migratedCfg);
486
491
  normalizeAutoUpdate(migratedCfg);
492
+ normalizeOrchestration(migratedCfg);
487
493
  const config = { ...DEFAULT_CONFIG, dbPath: path2.join(dir, "memories.db"), ...migratedCfg };
488
494
  if (config.dbPath.startsWith("~")) {
489
495
  config.dbPath = config.dbPath.replace(/^~/, os2.homedir());
@@ -507,6 +513,7 @@ function loadConfigSync() {
507
513
  normalizeScalingRoadmap(migratedCfg);
508
514
  normalizeSessionLifecycle(migratedCfg);
509
515
  normalizeAutoUpdate(migratedCfg);
516
+ normalizeOrchestration(migratedCfg);
510
517
  return { ...DEFAULT_CONFIG, dbPath: path2.join(dir, "memories.db"), ...migratedCfg };
511
518
  } catch {
512
519
  return { ...DEFAULT_CONFIG, dbPath: path2.join(dir, "memories.db") };
@@ -528,6 +535,7 @@ async function loadConfigFrom(configPath) {
528
535
  normalizeScalingRoadmap(migratedCfg);
529
536
  normalizeSessionLifecycle(migratedCfg);
530
537
  normalizeAutoUpdate(migratedCfg);
538
+ normalizeOrchestration(migratedCfg);
531
539
  return { ...DEFAULT_CONFIG, ...migratedCfg };
532
540
  } catch {
533
541
  return { ...DEFAULT_CONFIG };
@@ -599,6 +607,10 @@ var init_config = __esm({
599
607
  checkOnBoot: true,
600
608
  autoInstall: false,
601
609
  checkIntervalMs: 24 * 60 * 60 * 1e3
610
+ },
611
+ orchestration: {
612
+ phase: "phase_1_coo",
613
+ phaseSetBy: "default"
602
614
  }
603
615
  };
604
616
  CONFIG_MIGRATIONS = [
@@ -614,6 +626,47 @@ var init_config = __esm({
614
626
  }
615
627
  });
616
628
 
629
+ // src/lib/key-backup-status.ts
630
+ var key_backup_status_exports = {};
631
+ __export(key_backup_status_exports, {
632
+ getKeyBackupStatus: () => getKeyBackupStatus,
633
+ keyBackupMarkerPath: () => keyBackupMarkerPath,
634
+ markKeyBackupConfirmed: () => markKeyBackupConfirmed
635
+ });
636
+ import { existsSync as existsSync4, mkdirSync as mkdirSync2, readFileSync as readFileSync2, writeFileSync } from "fs";
637
+ import path3 from "path";
638
+ function keyBackupMarkerPath() {
639
+ return path3.join(EXE_AI_DIR, "key-backup-confirmed.json");
640
+ }
641
+ function getKeyBackupStatus() {
642
+ const marker = keyBackupMarkerPath();
643
+ if (!existsSync4(marker)) return { exists: false };
644
+ try {
645
+ const parsed = JSON.parse(readFileSync2(marker, "utf8"));
646
+ return {
647
+ exists: true,
648
+ confirmedAt: parsed.confirmedAt,
649
+ source: parsed.source
650
+ };
651
+ } catch {
652
+ return { exists: true };
653
+ }
654
+ }
655
+ function markKeyBackupConfirmed(source) {
656
+ mkdirSync2(EXE_AI_DIR, { recursive: true, mode: 448 });
657
+ writeFileSync(
658
+ keyBackupMarkerPath(),
659
+ JSON.stringify({ confirmedAt: (/* @__PURE__ */ new Date()).toISOString(), source }, null, 2) + "\n",
660
+ { mode: 384 }
661
+ );
662
+ }
663
+ var init_key_backup_status = __esm({
664
+ "src/lib/key-backup-status.ts"() {
665
+ "use strict";
666
+ init_config();
667
+ }
668
+ });
669
+
617
670
  // src/lib/crypto.ts
618
671
  var crypto_exports = {};
619
672
  __export(crypto_exports, {
@@ -730,9 +783,9 @@ var init_db_retry = __esm({
730
783
 
731
784
  // src/lib/employees.ts
732
785
  import { readFile as readFile3, writeFile as writeFile3, mkdir as mkdir3 } from "fs/promises";
733
- import { existsSync as existsSync4, symlinkSync, readlinkSync, readFileSync as readFileSync2, renameSync as renameSync2, unlinkSync, writeFileSync } from "fs";
786
+ import { existsSync as existsSync5, symlinkSync, readlinkSync, readFileSync as readFileSync3, renameSync as renameSync2, unlinkSync, writeFileSync as writeFileSync2 } from "fs";
734
787
  import { execSync as execSync2 } from "child_process";
735
- import path3 from "path";
788
+ import path4 from "path";
736
789
  import os3 from "os";
737
790
  function normalizeRole(role) {
738
791
  return (role ?? "").trim().toLowerCase();
@@ -747,7 +800,7 @@ function getCoordinatorName(employees = loadEmployeesSync()) {
747
800
  return getCoordinatorEmployee(employees)?.name ?? DEFAULT_COORDINATOR_TEMPLATE_NAME;
748
801
  }
749
802
  async function loadEmployees(employeesPath = EMPLOYEES_PATH) {
750
- if (!existsSync4(employeesPath)) {
803
+ if (!existsSync5(employeesPath)) {
751
804
  return [];
752
805
  }
753
806
  const raw = await readFile3(employeesPath, "utf-8");
@@ -758,13 +811,13 @@ async function loadEmployees(employeesPath = EMPLOYEES_PATH) {
758
811
  }
759
812
  }
760
813
  async function saveEmployees(employees, employeesPath = EMPLOYEES_PATH) {
761
- await mkdir3(path3.dirname(employeesPath), { recursive: true });
814
+ await mkdir3(path4.dirname(employeesPath), { recursive: true });
762
815
  await writeFile3(employeesPath, JSON.stringify(employees, null, 2) + "\n", "utf-8");
763
816
  }
764
817
  function loadEmployeesSync(employeesPath = EMPLOYEES_PATH) {
765
- if (!existsSync4(employeesPath)) return [];
818
+ if (!existsSync5(employeesPath)) return [];
766
819
  try {
767
- return JSON.parse(readFileSync2(employeesPath, "utf-8"));
820
+ return JSON.parse(readFileSync3(employeesPath, "utf-8"));
768
821
  } catch {
769
822
  return [];
770
823
  }
@@ -785,7 +838,7 @@ function registerBinSymlinks(name) {
785
838
  errors.push("Could not find 'exe-os' in PATH");
786
839
  return { created, skipped, errors };
787
840
  }
788
- const binDir = path3.dirname(exeBinPath);
841
+ const binDir = path4.dirname(exeBinPath);
789
842
  let target;
790
843
  try {
791
844
  target = readlinkSync(exeBinPath);
@@ -795,8 +848,8 @@ function registerBinSymlinks(name) {
795
848
  }
796
849
  for (const suffix of ["", "-opencode"]) {
797
850
  const linkName = `${name}${suffix}`;
798
- const linkPath = path3.join(binDir, linkName);
799
- if (existsSync4(linkPath)) {
851
+ const linkPath = path4.join(binDir, linkName);
852
+ if (existsSync5(linkPath)) {
800
853
  skipped.push(linkName);
801
854
  continue;
802
855
  }
@@ -814,16 +867,16 @@ var init_employees = __esm({
814
867
  "src/lib/employees.ts"() {
815
868
  "use strict";
816
869
  init_config();
817
- EMPLOYEES_PATH = path3.join(EXE_AI_DIR, "exe-employees.json");
870
+ EMPLOYEES_PATH = path4.join(EXE_AI_DIR, "exe-employees.json");
818
871
  DEFAULT_COORDINATOR_TEMPLATE_NAME = "exe";
819
872
  COORDINATOR_ROLE = "COO";
820
- IDENTITY_DIR = path3.join(EXE_AI_DIR, "identity");
873
+ IDENTITY_DIR = path4.join(EXE_AI_DIR, "identity");
821
874
  }
822
875
  });
823
876
 
824
877
  // src/lib/database-adapter.ts
825
878
  import os4 from "os";
826
- import path4 from "path";
879
+ import path5 from "path";
827
880
  import { createRequire } from "module";
828
881
  import { pathToFileURL } from "url";
829
882
  function quotedIdentifier(identifier) {
@@ -1134,8 +1187,8 @@ async function loadPrismaClient() {
1134
1187
  }
1135
1188
  return new PrismaClient2();
1136
1189
  }
1137
- const exeDbRoot = process.env.EXE_DB_ROOT ?? path4.join(os4.homedir(), "exe-db");
1138
- const requireFromExeDb = createRequire(path4.join(exeDbRoot, "package.json"));
1190
+ const exeDbRoot = process.env.EXE_DB_ROOT ?? path5.join(os4.homedir(), "exe-db");
1191
+ const requireFromExeDb = createRequire(path5.join(exeDbRoot, "package.json"));
1139
1192
  const prismaEntry = requireFromExeDb.resolve("@prisma/client");
1140
1193
  const module = await import(pathToFileURL(prismaEntry).href);
1141
1194
  const PrismaClient = module.PrismaClient ?? module.default?.PrismaClient;
@@ -1416,8 +1469,8 @@ var init_memory = __esm({
1416
1469
 
1417
1470
  // src/lib/daemon-auth.ts
1418
1471
  import crypto2 from "crypto";
1419
- import path5 from "path";
1420
- import { existsSync as existsSync5, readFileSync as readFileSync3, writeFileSync as writeFileSync2 } from "fs";
1472
+ import path6 from "path";
1473
+ import { existsSync as existsSync6, readFileSync as readFileSync4, writeFileSync as writeFileSync3 } from "fs";
1421
1474
  function normalizeToken(token) {
1422
1475
  if (!token) return null;
1423
1476
  const trimmed = token.trim();
@@ -1425,8 +1478,8 @@ function normalizeToken(token) {
1425
1478
  }
1426
1479
  function readDaemonToken() {
1427
1480
  try {
1428
- if (!existsSync5(DAEMON_TOKEN_PATH)) return null;
1429
- return normalizeToken(readFileSync3(DAEMON_TOKEN_PATH, "utf8"));
1481
+ if (!existsSync6(DAEMON_TOKEN_PATH)) return null;
1482
+ return normalizeToken(readFileSync4(DAEMON_TOKEN_PATH, "utf8"));
1430
1483
  } catch {
1431
1484
  return null;
1432
1485
  }
@@ -1436,7 +1489,7 @@ function ensureDaemonToken(seed) {
1436
1489
  if (existing) return existing;
1437
1490
  const token = normalizeToken(seed) ?? crypto2.randomBytes(32).toString("hex");
1438
1491
  ensurePrivateDirSync(EXE_AI_DIR);
1439
- writeFileSync2(DAEMON_TOKEN_PATH, `${token}
1492
+ writeFileSync3(DAEMON_TOKEN_PATH, `${token}
1440
1493
  `, "utf8");
1441
1494
  enforcePrivateFileSync(DAEMON_TOKEN_PATH);
1442
1495
  return token;
@@ -1447,7 +1500,7 @@ var init_daemon_auth = __esm({
1447
1500
  "use strict";
1448
1501
  init_config();
1449
1502
  init_secure_files();
1450
- DAEMON_TOKEN_PATH = path5.join(EXE_AI_DIR, "exed.token");
1503
+ DAEMON_TOKEN_PATH = path6.join(EXE_AI_DIR, "exed.token");
1451
1504
  }
1452
1505
  });
1453
1506
 
@@ -1456,8 +1509,8 @@ import net from "net";
1456
1509
  import os5 from "os";
1457
1510
  import { spawn } from "child_process";
1458
1511
  import { randomUUID } from "crypto";
1459
- import { existsSync as existsSync6, unlinkSync as unlinkSync2, readFileSync as readFileSync4, openSync, closeSync, statSync } from "fs";
1460
- import path6 from "path";
1512
+ import { existsSync as existsSync7, unlinkSync as unlinkSync2, readFileSync as readFileSync5, openSync, closeSync, statSync } from "fs";
1513
+ import path7 from "path";
1461
1514
  import { fileURLToPath as fileURLToPath2 } from "url";
1462
1515
  function handleData(chunk) {
1463
1516
  _buffer += chunk.toString();
@@ -1485,9 +1538,9 @@ function handleData(chunk) {
1485
1538
  }
1486
1539
  }
1487
1540
  function cleanupStaleFiles() {
1488
- if (existsSync6(PID_PATH)) {
1541
+ if (existsSync7(PID_PATH)) {
1489
1542
  try {
1490
- const pid = parseInt(readFileSync4(PID_PATH, "utf8").trim(), 10);
1543
+ const pid = parseInt(readFileSync5(PID_PATH, "utf8").trim(), 10);
1491
1544
  if (pid > 0) {
1492
1545
  try {
1493
1546
  process.kill(pid, 0);
@@ -1508,11 +1561,11 @@ function cleanupStaleFiles() {
1508
1561
  }
1509
1562
  }
1510
1563
  function findPackageRoot() {
1511
- let dir = path6.dirname(fileURLToPath2(import.meta.url));
1512
- const { root } = path6.parse(dir);
1564
+ let dir = path7.dirname(fileURLToPath2(import.meta.url));
1565
+ const { root } = path7.parse(dir);
1513
1566
  while (dir !== root) {
1514
- if (existsSync6(path6.join(dir, "package.json"))) return dir;
1515
- dir = path6.dirname(dir);
1567
+ if (existsSync7(path7.join(dir, "package.json"))) return dir;
1568
+ dir = path7.dirname(dir);
1516
1569
  }
1517
1570
  return null;
1518
1571
  }
@@ -1559,8 +1612,8 @@ function spawnDaemon() {
1559
1612
  process.stderr.write("[exed-client] WARN: cannot find package root\n");
1560
1613
  return;
1561
1614
  }
1562
- const daemonPath = path6.join(pkgRoot, "dist", "lib", "exe-daemon.js");
1563
- if (!existsSync6(daemonPath)) {
1615
+ const daemonPath = path7.join(pkgRoot, "dist", "lib", "exe-daemon.js");
1616
+ if (!existsSync7(daemonPath)) {
1564
1617
  process.stderr.write(`[exed-client] WARN: daemon script not found at ${daemonPath}
1565
1618
  `);
1566
1619
  return;
@@ -1569,7 +1622,7 @@ function spawnDaemon() {
1569
1622
  const daemonToken = ensureDaemonToken(process.env[DAEMON_TOKEN_ENV] ?? null);
1570
1623
  process.stderr.write(`[exed-client] Spawning daemon: ${resolvedPath}
1571
1624
  `);
1572
- const logPath = path6.join(path6.dirname(SOCKET_PATH), "exed.log");
1625
+ const logPath = path7.join(path7.dirname(SOCKET_PATH), "exed.log");
1573
1626
  let stderrFd = "ignore";
1574
1627
  try {
1575
1628
  stderrFd = openSync(logPath, "a");
@@ -1719,9 +1772,9 @@ var init_exe_daemon_client = __esm({
1719
1772
  "use strict";
1720
1773
  init_config();
1721
1774
  init_daemon_auth();
1722
- SOCKET_PATH = process.env.EXE_DAEMON_SOCK ?? process.env.EXE_EMBED_SOCK ?? path6.join(EXE_AI_DIR, "exed.sock");
1723
- PID_PATH = process.env.EXE_DAEMON_PID ?? process.env.EXE_EMBED_PID ?? path6.join(EXE_AI_DIR, "exed.pid");
1724
- SPAWN_LOCK_PATH = path6.join(EXE_AI_DIR, "exed-spawn.lock");
1775
+ SOCKET_PATH = process.env.EXE_DAEMON_SOCK ?? process.env.EXE_EMBED_SOCK ?? path7.join(EXE_AI_DIR, "exed.sock");
1776
+ PID_PATH = process.env.EXE_DAEMON_PID ?? process.env.EXE_EMBED_PID ?? path7.join(EXE_AI_DIR, "exed.pid");
1777
+ SPAWN_LOCK_PATH = path7.join(EXE_AI_DIR, "exed-spawn.lock");
1725
1778
  SPAWN_LOCK_STALE_MS = 3e4;
1726
1779
  CONNECT_TIMEOUT_MS = 15e3;
1727
1780
  REQUEST_TIMEOUT_MS = 3e4;
@@ -3209,32 +3262,32 @@ var init_compress = __esm({
3209
3262
  });
3210
3263
 
3211
3264
  // src/lib/license.ts
3212
- import { readFileSync as readFileSync5, writeFileSync as writeFileSync3, existsSync as existsSync7, mkdirSync as mkdirSync2 } from "fs";
3265
+ import { readFileSync as readFileSync6, writeFileSync as writeFileSync4, existsSync as existsSync8, mkdirSync as mkdirSync3 } from "fs";
3213
3266
  import { randomUUID as randomUUID2 } from "crypto";
3214
3267
  import { createRequire as createRequire2 } from "module";
3215
3268
  import { pathToFileURL as pathToFileURL2 } from "url";
3216
3269
  import os6 from "os";
3217
- import path7 from "path";
3270
+ import path8 from "path";
3218
3271
  import { jwtVerify, importSPKI } from "jose";
3219
3272
  function loadDeviceId() {
3220
- const deviceJsonPath = path7.join(EXE_AI_DIR, "device.json");
3273
+ const deviceJsonPath = path8.join(EXE_AI_DIR, "device.json");
3221
3274
  try {
3222
- if (existsSync7(deviceJsonPath)) {
3223
- const data = JSON.parse(readFileSync5(deviceJsonPath, "utf8"));
3275
+ if (existsSync8(deviceJsonPath)) {
3276
+ const data = JSON.parse(readFileSync6(deviceJsonPath, "utf8"));
3224
3277
  if (data.deviceId) return data.deviceId;
3225
3278
  }
3226
3279
  } catch {
3227
3280
  }
3228
3281
  try {
3229
- if (existsSync7(DEVICE_ID_PATH)) {
3230
- const id2 = readFileSync5(DEVICE_ID_PATH, "utf8").trim();
3282
+ if (existsSync8(DEVICE_ID_PATH)) {
3283
+ const id2 = readFileSync6(DEVICE_ID_PATH, "utf8").trim();
3231
3284
  if (id2) return id2;
3232
3285
  }
3233
3286
  } catch {
3234
3287
  }
3235
3288
  const id = randomUUID2();
3236
- mkdirSync2(EXE_AI_DIR, { recursive: true });
3237
- writeFileSync3(DEVICE_ID_PATH, id, "utf8");
3289
+ mkdirSync3(EXE_AI_DIR, { recursive: true });
3290
+ writeFileSync4(DEVICE_ID_PATH, id, "utf8");
3238
3291
  return id;
3239
3292
  }
3240
3293
  var LICENSE_PATH, CACHE_PATH, DEVICE_ID_PATH;
@@ -3242,9 +3295,9 @@ var init_license = __esm({
3242
3295
  "src/lib/license.ts"() {
3243
3296
  "use strict";
3244
3297
  init_config();
3245
- LICENSE_PATH = path7.join(EXE_AI_DIR, "license.key");
3246
- CACHE_PATH = path7.join(EXE_AI_DIR, "license-cache.json");
3247
- DEVICE_ID_PATH = path7.join(EXE_AI_DIR, "device-id");
3298
+ LICENSE_PATH = path8.join(EXE_AI_DIR, "license.key");
3299
+ CACHE_PATH = path8.join(EXE_AI_DIR, "license-cache.json");
3300
+ DEVICE_ID_PATH = path8.join(EXE_AI_DIR, "device-id");
3248
3301
  }
3249
3302
  });
3250
3303
 
@@ -3267,8 +3320,8 @@ __export(crdt_sync_exports, {
3267
3320
  rebuildFromDb: () => rebuildFromDb
3268
3321
  });
3269
3322
  import * as Y from "yjs";
3270
- import { readFileSync as readFileSync6, writeFileSync as writeFileSync4, existsSync as existsSync8, mkdirSync as mkdirSync3, unlinkSync as unlinkSync3 } from "fs";
3271
- import path8 from "path";
3323
+ import { readFileSync as readFileSync7, writeFileSync as writeFileSync5, existsSync as existsSync9, mkdirSync as mkdirSync4, unlinkSync as unlinkSync3 } from "fs";
3324
+ import path9 from "path";
3272
3325
  import { homedir } from "os";
3273
3326
  function getStatePath() {
3274
3327
  return _statePathOverride ?? DEFAULT_STATE_PATH;
@@ -3280,9 +3333,9 @@ function initCrdtDoc() {
3280
3333
  if (doc) return doc;
3281
3334
  doc = new Y.Doc();
3282
3335
  const sp = getStatePath();
3283
- if (existsSync8(sp)) {
3336
+ if (existsSync9(sp)) {
3284
3337
  try {
3285
- const state = readFileSync6(sp);
3338
+ const state = readFileSync7(sp);
3286
3339
  Y.applyUpdate(doc, new Uint8Array(state));
3287
3340
  } catch {
3288
3341
  console.warn("[crdt-sync] WARN: corrupted state file, rebuilding from DB");
@@ -3424,10 +3477,10 @@ function persistState() {
3424
3477
  if (!doc) return;
3425
3478
  try {
3426
3479
  const sp = getStatePath();
3427
- const dir = path8.dirname(sp);
3428
- if (!existsSync8(dir)) mkdirSync3(dir, { recursive: true });
3480
+ const dir = path9.dirname(sp);
3481
+ if (!existsSync9(dir)) mkdirSync4(dir, { recursive: true });
3429
3482
  const state = Y.encodeStateAsUpdate(doc);
3430
- writeFileSync4(sp, Buffer.from(state));
3483
+ writeFileSync5(sp, Buffer.from(state));
3431
3484
  } catch {
3432
3485
  }
3433
3486
  }
@@ -3468,7 +3521,7 @@ var DEFAULT_STATE_PATH, _statePathOverride, doc;
3468
3521
  var init_crdt_sync = __esm({
3469
3522
  "src/lib/crdt-sync.ts"() {
3470
3523
  "use strict";
3471
- DEFAULT_STATE_PATH = path8.join(homedir(), ".exe-os", "crdt-state.bin");
3524
+ DEFAULT_STATE_PATH = path9.join(homedir(), ".exe-os", "crdt-state.bin");
3472
3525
  _statePathOverride = null;
3473
3526
  doc = null;
3474
3527
  }
@@ -3485,33 +3538,33 @@ __export(db_backup_exports, {
3485
3538
  listBackups: () => listBackups,
3486
3539
  rotateBackups: () => rotateBackups
3487
3540
  });
3488
- import { copyFileSync, existsSync as existsSync9, mkdirSync as mkdirSync4, readdirSync, unlinkSync as unlinkSync4, statSync as statSync2 } from "fs";
3489
- import path9 from "path";
3541
+ import { copyFileSync, existsSync as existsSync10, mkdirSync as mkdirSync5, readdirSync, unlinkSync as unlinkSync4, statSync as statSync2 } from "fs";
3542
+ import path10 from "path";
3490
3543
  function findActiveDb() {
3491
3544
  for (const name of DB_NAMES) {
3492
- const p = path9.join(EXE_AI_DIR, name);
3493
- if (existsSync9(p)) return p;
3545
+ const p = path10.join(EXE_AI_DIR, name);
3546
+ if (existsSync10(p)) return p;
3494
3547
  }
3495
3548
  return null;
3496
3549
  }
3497
3550
  function createBackup(reason = "manual") {
3498
3551
  const dbPath = findActiveDb();
3499
3552
  if (!dbPath) return null;
3500
- mkdirSync4(BACKUP_DIR, { recursive: true });
3501
- const dbName = path9.basename(dbPath, ".db");
3553
+ mkdirSync5(BACKUP_DIR, { recursive: true });
3554
+ const dbName = path10.basename(dbPath, ".db");
3502
3555
  const timestamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-").slice(0, 19);
3503
3556
  const backupName = `${dbName}-${reason}-${timestamp}.db`;
3504
- const backupPath = path9.join(BACKUP_DIR, backupName);
3557
+ const backupPath = path10.join(BACKUP_DIR, backupName);
3505
3558
  copyFileSync(dbPath, backupPath);
3506
3559
  const walPath = dbPath + "-wal";
3507
- if (existsSync9(walPath)) {
3560
+ if (existsSync10(walPath)) {
3508
3561
  try {
3509
3562
  copyFileSync(walPath, backupPath + "-wal");
3510
3563
  } catch {
3511
3564
  }
3512
3565
  }
3513
3566
  const shmPath = dbPath + "-shm";
3514
- if (existsSync9(shmPath)) {
3567
+ if (existsSync10(shmPath)) {
3515
3568
  try {
3516
3569
  copyFileSync(shmPath, backupPath + "-shm");
3517
3570
  } catch {
@@ -3520,14 +3573,14 @@ function createBackup(reason = "manual") {
3520
3573
  return backupPath;
3521
3574
  }
3522
3575
  function rotateBackups(keepDays = DEFAULT_KEEP_DAYS) {
3523
- if (!existsSync9(BACKUP_DIR)) return 0;
3576
+ if (!existsSync10(BACKUP_DIR)) return 0;
3524
3577
  const cutoff = Date.now() - keepDays * 24 * 60 * 60 * 1e3;
3525
3578
  let deleted = 0;
3526
3579
  try {
3527
3580
  const files = readdirSync(BACKUP_DIR);
3528
3581
  for (const file of files) {
3529
3582
  if (!file.endsWith(".db") && !file.endsWith(".db-wal") && !file.endsWith(".db-shm")) continue;
3530
- const filePath = path9.join(BACKUP_DIR, file);
3583
+ const filePath = path10.join(BACKUP_DIR, file);
3531
3584
  try {
3532
3585
  const stat = statSync2(filePath);
3533
3586
  if (stat.mtimeMs < cutoff) {
@@ -3542,11 +3595,11 @@ function rotateBackups(keepDays = DEFAULT_KEEP_DAYS) {
3542
3595
  return deleted;
3543
3596
  }
3544
3597
  function listBackups() {
3545
- if (!existsSync9(BACKUP_DIR)) return [];
3598
+ if (!existsSync10(BACKUP_DIR)) return [];
3546
3599
  try {
3547
3600
  const files = readdirSync(BACKUP_DIR).filter((f) => f.endsWith(".db") && !f.endsWith("-wal") && !f.endsWith("-shm"));
3548
3601
  return files.map((name) => {
3549
- const p = path9.join(BACKUP_DIR, name);
3602
+ const p = path10.join(BACKUP_DIR, name);
3550
3603
  const stat = statSync2(p);
3551
3604
  return { path: p, name, size: stat.size, date: stat.mtime };
3552
3605
  }).sort((a, b) => b.date.getTime() - a.date.getTime());
@@ -3571,7 +3624,7 @@ var init_db_backup = __esm({
3571
3624
  "src/lib/db-backup.ts"() {
3572
3625
  "use strict";
3573
3626
  init_config();
3574
- BACKUP_DIR = path9.join(EXE_AI_DIR, "backups");
3627
+ BACKUP_DIR = path10.join(EXE_AI_DIR, "backups");
3575
3628
  DEFAULT_KEEP_DAYS = 3;
3576
3629
  DB_NAMES = ["memories.db", "exe-mem.db", "exe-os.db", "exe.db"];
3577
3630
  }
@@ -3580,8 +3633,10 @@ var init_db_backup = __esm({
3580
3633
  // src/lib/cloud-sync.ts
3581
3634
  var cloud_sync_exports = {};
3582
3635
  __export(cloud_sync_exports, {
3636
+ CLOUD_RELINK_REQUIRED_MESSAGE: () => CLOUD_RELINK_REQUIRED_MESSAGE,
3583
3637
  assertSecureEndpoint: () => assertSecureEndpoint,
3584
3638
  buildRosterBlob: () => buildRosterBlob,
3639
+ clearCloudRelinkRequired: () => clearCloudRelinkRequired,
3585
3640
  cloudPull: () => cloudPull,
3586
3641
  cloudPullBehaviors: () => cloudPullBehaviors,
3587
3642
  cloudPullBlob: () => cloudPullBlob,
@@ -3601,53 +3656,66 @@ __export(cloud_sync_exports, {
3601
3656
  cloudPushRoster: () => cloudPushRoster,
3602
3657
  cloudPushTasks: () => cloudPushTasks,
3603
3658
  cloudSync: () => cloudSync,
3659
+ getCloudRelinkRequired: () => getCloudRelinkRequired,
3604
3660
  mergeConfig: () => mergeConfig,
3605
3661
  mergeRosterFromRemote: () => mergeRosterFromRemote,
3606
3662
  pushToPostgres: () => pushToPostgres,
3607
3663
  recordRosterDeletion: () => recordRosterDeletion
3608
3664
  });
3609
- import { readFileSync as readFileSync7, writeFileSync as writeFileSync5, existsSync as existsSync10, readdirSync as readdirSync2, mkdirSync as mkdirSync5, appendFileSync, unlinkSync as unlinkSync5, openSync as openSync2, closeSync as closeSync2, statSync as statSync3 } from "fs";
3665
+ import { readFileSync as readFileSync8, writeFileSync as writeFileSync6, existsSync as existsSync11, readdirSync as readdirSync2, mkdirSync as mkdirSync6, appendFileSync, unlinkSync as unlinkSync5, openSync as openSync2, closeSync as closeSync2, statSync as statSync3 } from "fs";
3610
3666
  import crypto3 from "crypto";
3611
- import path10 from "path";
3667
+ import path11 from "path";
3612
3668
  import { homedir as homedir2 } from "os";
3613
3669
  function sqlSafe(v) {
3614
3670
  return v === void 0 ? null : v;
3615
3671
  }
3616
3672
  function logError(msg) {
3617
3673
  try {
3618
- const logPath = path10.join(homedir2(), ".exe-os", "workers.log");
3674
+ const logPath = path11.join(homedir2(), ".exe-os", "workers.log");
3619
3675
  appendFileSync(logPath, `${(/* @__PURE__ */ new Date()).toISOString()} ${msg}
3620
3676
  `);
3621
3677
  } catch {
3622
3678
  }
3623
3679
  }
3680
+ function isTruthyEnv(value) {
3681
+ return /^(1|true|yes|on)$/i.test(value ?? "");
3682
+ }
3624
3683
  function loadPgClient() {
3625
3684
  if (_pgFailed) return null;
3626
- const postgresUrl = process.env.DATABASE_URL;
3627
- const configPath = path10.join(EXE_AI_DIR, "config.json");
3685
+ const configPath = path11.join(EXE_AI_DIR, "config.json");
3628
3686
  let cloudPostgresUrl;
3687
+ let configEnabled = false;
3629
3688
  try {
3630
- if (existsSync10(configPath)) {
3631
- const cfg = JSON.parse(readFileSync7(configPath, "utf8"));
3689
+ if (existsSync11(configPath)) {
3690
+ const cfg = JSON.parse(readFileSync8(configPath, "utf8"));
3632
3691
  cloudPostgresUrl = cfg.cloud?.postgresUrl;
3633
- if (cfg.cloud?.syncToPostgres === false) {
3634
- _pgFailed = true;
3635
- return null;
3636
- }
3692
+ configEnabled = cfg.cloud?.syncToPostgres === true;
3637
3693
  }
3638
3694
  } catch {
3639
3695
  }
3640
- const url = postgresUrl || cloudPostgresUrl;
3696
+ const envEnabled = isTruthyEnv(process.env.EXE_CLOUD_SYNC_TO_POSTGRES);
3697
+ if (!envEnabled && !configEnabled) {
3698
+ return null;
3699
+ }
3700
+ const url = process.env.DATABASE_URL || cloudPostgresUrl;
3641
3701
  if (!url) {
3642
3702
  _pgFailed = true;
3643
3703
  return null;
3644
3704
  }
3645
3705
  if (!_pgPromise) {
3646
3706
  _pgPromise = (async () => {
3707
+ if (!process.env.DATABASE_URL) process.env.DATABASE_URL = url;
3647
3708
  const { createRequire: createRequire3 } = await import("module");
3648
3709
  const { pathToFileURL: pathToFileURL3 } = await import("url");
3649
- const exeDbRoot = process.env.EXE_DB_ROOT ?? path10.join(homedir2(), "exe-db");
3650
- const req = createRequire3(path10.join(exeDbRoot, "package.json"));
3710
+ const explicitPath = process.env.EXE_OS_PRISMA_CLIENT_PATH;
3711
+ if (explicitPath) {
3712
+ const mod2 = await import(pathToFileURL3(explicitPath).href);
3713
+ const Ctor2 = mod2.PrismaClient ?? mod2.default?.PrismaClient;
3714
+ if (!Ctor2) throw new Error(`No PrismaClient at ${explicitPath}`);
3715
+ return new Ctor2();
3716
+ }
3717
+ const exeDbRoot = process.env.EXE_DB_ROOT ?? path11.join(homedir2(), "exe-db");
3718
+ const req = createRequire3(path11.join(exeDbRoot, "package.json"));
3651
3719
  const entry = req.resolve("@prisma/client");
3652
3720
  const mod = await import(pathToFileURL3(entry).href);
3653
3721
  const Ctor = mod.PrismaClient ?? mod.default?.PrismaClient;
@@ -3692,18 +3760,18 @@ async function withRosterLock(fn) {
3692
3760
  try {
3693
3761
  const fd = openSync2(ROSTER_LOCK_PATH, "wx");
3694
3762
  closeSync2(fd);
3695
- writeFileSync5(ROSTER_LOCK_PATH, String(Date.now()));
3763
+ writeFileSync6(ROSTER_LOCK_PATH, String(Date.now()));
3696
3764
  } catch (err) {
3697
3765
  if (err.code === "EEXIST") {
3698
3766
  try {
3699
- const ts = parseInt(readFileSync7(ROSTER_LOCK_PATH, "utf-8"), 10);
3767
+ const ts = parseInt(readFileSync8(ROSTER_LOCK_PATH, "utf-8"), 10);
3700
3768
  if (Date.now() - ts < LOCK_STALE_MS) {
3701
3769
  throw new Error("Roster merge already in progress \u2014 another sync is running");
3702
3770
  }
3703
3771
  unlinkSync5(ROSTER_LOCK_PATH);
3704
3772
  const fd = openSync2(ROSTER_LOCK_PATH, "wx");
3705
3773
  closeSync2(fd);
3706
- writeFileSync5(ROSTER_LOCK_PATH, String(Date.now()));
3774
+ writeFileSync6(ROSTER_LOCK_PATH, String(Date.now()));
3707
3775
  } catch (retryErr) {
3708
3776
  if (retryErr instanceof Error && retryErr.message.includes("already in progress")) throw retryErr;
3709
3777
  throw new Error("Roster merge already in progress \u2014 another sync is running");
@@ -3822,6 +3890,24 @@ async function cloudPull(sinceVersion, config) {
3822
3890
  return { records: [], maxVersion: sinceVersion };
3823
3891
  }
3824
3892
  }
3893
+ async function getCloudRelinkRequired(client = getClient()) {
3894
+ try {
3895
+ await client.execute("CREATE TABLE IF NOT EXISTS sync_meta (key TEXT PRIMARY KEY, value TEXT NOT NULL)");
3896
+ const relink = await client.execute("SELECT value FROM sync_meta WHERE key = 'cloud_relink_required' LIMIT 1");
3897
+ return String(relink.rows[0]?.value ?? "") === "1";
3898
+ } catch {
3899
+ return false;
3900
+ }
3901
+ }
3902
+ async function clearCloudRelinkRequired(client = getClient()) {
3903
+ await client.execute("CREATE TABLE IF NOT EXISTS sync_meta (key TEXT PRIMARY KEY, value TEXT NOT NULL)");
3904
+ await client.execute("INSERT OR REPLACE INTO sync_meta (key, value) VALUES ('cloud_relink_required', '0')");
3905
+ await client.execute({
3906
+ sql: "INSERT OR REPLACE INTO sync_meta (key, value) VALUES ('cloud_relinked_at', ?)",
3907
+ args: [(/* @__PURE__ */ new Date()).toISOString()]
3908
+ });
3909
+ await client.execute("DELETE FROM sync_meta WHERE key IN ('last_cloud_pull_version', 'last_cloud_push_version')");
3910
+ }
3825
3911
  async function cloudSync(config) {
3826
3912
  if (!isSyncCryptoInitialized()) {
3827
3913
  try {
@@ -3842,6 +3928,12 @@ async function cloudSync(config) {
3842
3928
  } catch {
3843
3929
  throw new Error("[cloud-sync] Database not initialized. Call initStore() before cloudSync().");
3844
3930
  }
3931
+ try {
3932
+ if (await getCloudRelinkRequired(client)) throw new Error(CLOUD_RELINK_REQUIRED_MESSAGE);
3933
+ } catch (err) {
3934
+ const msg = err instanceof Error ? err.message : String(err);
3935
+ if (msg.includes("Paused after key rotation")) throw err;
3936
+ }
3845
3937
  try {
3846
3938
  const { getRawClient: getRawClient2 } = await Promise.resolve().then(() => (init_database(), database_exports));
3847
3939
  await getRawClient2().execute("PRAGMA wal_checkpoint(PASSIVE)");
@@ -4087,8 +4179,8 @@ async function cloudSync(config) {
4087
4179
  try {
4088
4180
  const employees = await loadEmployees();
4089
4181
  rosterResult.employees = employees.length;
4090
- const idDir = path10.join(EXE_AI_DIR, "identity");
4091
- if (existsSync10(idDir)) {
4182
+ const idDir = path11.join(EXE_AI_DIR, "identity");
4183
+ if (existsSync11(idDir)) {
4092
4184
  rosterResult.identities = readdirSync2(idDir).filter((f) => f.endsWith(".md")).length;
4093
4185
  }
4094
4186
  } catch {
@@ -4101,7 +4193,7 @@ async function cloudSync(config) {
4101
4193
  const backupSize = statSync3(latestBackup).size;
4102
4194
  const MAX_CLOUD_BACKUP_BYTES = 50 * 1024 * 1024;
4103
4195
  if (backupSize <= MAX_CLOUD_BACKUP_BYTES) {
4104
- const backupData = readFileSync7(latestBackup);
4196
+ const backupData = readFileSync8(latestBackup);
4105
4197
  const deviceId = loadDeviceId() ?? "unknown";
4106
4198
  const encrypted = encryptSyncBlob(backupData);
4107
4199
  const backupRes = await fetchWithRetry(`${config.endpoint}/sync/push-db-backup`, {
@@ -4109,7 +4201,7 @@ async function cloudSync(config) {
4109
4201
  headers: { "Content-Type": "application/json", Authorization: `Bearer ${config.apiKey}` },
4110
4202
  body: JSON.stringify({
4111
4203
  device_id: deviceId,
4112
- filename: path10.basename(latestBackup),
4204
+ filename: path11.basename(latestBackup),
4113
4205
  blob: encrypted,
4114
4206
  size: backupData.length
4115
4207
  })
@@ -4137,56 +4229,56 @@ async function cloudSync(config) {
4137
4229
  function recordRosterDeletion(name) {
4138
4230
  let deletions = [];
4139
4231
  try {
4140
- if (existsSync10(ROSTER_DELETIONS_PATH)) {
4141
- deletions = JSON.parse(readFileSync7(ROSTER_DELETIONS_PATH, "utf-8"));
4232
+ if (existsSync11(ROSTER_DELETIONS_PATH)) {
4233
+ deletions = JSON.parse(readFileSync8(ROSTER_DELETIONS_PATH, "utf-8"));
4142
4234
  }
4143
4235
  } catch {
4144
4236
  }
4145
4237
  if (!deletions.includes(name)) deletions.push(name);
4146
- writeFileSync5(ROSTER_DELETIONS_PATH, JSON.stringify(deletions));
4238
+ writeFileSync6(ROSTER_DELETIONS_PATH, JSON.stringify(deletions));
4147
4239
  }
4148
4240
  function consumeRosterDeletions() {
4149
4241
  try {
4150
- if (!existsSync10(ROSTER_DELETIONS_PATH)) return [];
4151
- const deletions = JSON.parse(readFileSync7(ROSTER_DELETIONS_PATH, "utf-8"));
4152
- writeFileSync5(ROSTER_DELETIONS_PATH, "[]");
4242
+ if (!existsSync11(ROSTER_DELETIONS_PATH)) return [];
4243
+ const deletions = JSON.parse(readFileSync8(ROSTER_DELETIONS_PATH, "utf-8"));
4244
+ writeFileSync6(ROSTER_DELETIONS_PATH, "[]");
4153
4245
  return deletions;
4154
4246
  } catch {
4155
4247
  return [];
4156
4248
  }
4157
4249
  }
4158
4250
  function buildRosterBlob(paths) {
4159
- const rosterPath = paths?.rosterPath ?? path10.join(EXE_AI_DIR, "exe-employees.json");
4160
- const identityDir = paths?.identityDir ?? path10.join(EXE_AI_DIR, "identity");
4161
- const configPath = paths?.configPath ?? path10.join(EXE_AI_DIR, "config.json");
4251
+ const rosterPath = paths?.rosterPath ?? path11.join(EXE_AI_DIR, "exe-employees.json");
4252
+ const identityDir = paths?.identityDir ?? path11.join(EXE_AI_DIR, "identity");
4253
+ const configPath = paths?.configPath ?? path11.join(EXE_AI_DIR, "config.json");
4162
4254
  let roster = [];
4163
- if (existsSync10(rosterPath)) {
4255
+ if (existsSync11(rosterPath)) {
4164
4256
  try {
4165
- roster = JSON.parse(readFileSync7(rosterPath, "utf-8"));
4257
+ roster = JSON.parse(readFileSync8(rosterPath, "utf-8"));
4166
4258
  } catch {
4167
4259
  }
4168
4260
  }
4169
4261
  const identities = {};
4170
- if (existsSync10(identityDir)) {
4262
+ if (existsSync11(identityDir)) {
4171
4263
  for (const file of readdirSync2(identityDir).filter((f) => f.endsWith(".md"))) {
4172
4264
  try {
4173
- identities[file] = readFileSync7(path10.join(identityDir, file), "utf-8");
4265
+ identities[file] = readFileSync8(path11.join(identityDir, file), "utf-8");
4174
4266
  } catch {
4175
4267
  }
4176
4268
  }
4177
4269
  }
4178
4270
  let config;
4179
- if (existsSync10(configPath)) {
4271
+ if (existsSync11(configPath)) {
4180
4272
  try {
4181
- config = JSON.parse(readFileSync7(configPath, "utf-8"));
4273
+ config = JSON.parse(readFileSync8(configPath, "utf-8"));
4182
4274
  } catch {
4183
4275
  }
4184
4276
  }
4185
4277
  let agentConfig;
4186
- const agentConfigPath = path10.join(EXE_AI_DIR, "agent-config.json");
4187
- if (existsSync10(agentConfigPath)) {
4278
+ const agentConfigPath = path11.join(EXE_AI_DIR, "agent-config.json");
4279
+ if (existsSync11(agentConfigPath)) {
4188
4280
  try {
4189
- agentConfig = JSON.parse(readFileSync7(agentConfigPath, "utf-8"));
4281
+ agentConfig = JSON.parse(readFileSync8(agentConfigPath, "utf-8"));
4190
4282
  } catch {
4191
4283
  }
4192
4284
  }
@@ -4262,24 +4354,24 @@ async function cloudPullRoster(config) {
4262
4354
  }
4263
4355
  }
4264
4356
  function mergeConfig(remoteConfig, configPath) {
4265
- const cfgPath = configPath ?? path10.join(EXE_AI_DIR, "config.json");
4357
+ const cfgPath = configPath ?? path11.join(EXE_AI_DIR, "config.json");
4266
4358
  let local = {};
4267
- if (existsSync10(cfgPath)) {
4359
+ if (existsSync11(cfgPath)) {
4268
4360
  try {
4269
- local = JSON.parse(readFileSync7(cfgPath, "utf-8"));
4361
+ local = JSON.parse(readFileSync8(cfgPath, "utf-8"));
4270
4362
  } catch {
4271
4363
  }
4272
4364
  }
4273
4365
  const merged = { ...remoteConfig, ...local };
4274
- const dir = path10.dirname(cfgPath);
4366
+ const dir = path11.dirname(cfgPath);
4275
4367
  ensurePrivateDirSync(dir);
4276
- writeFileSync5(cfgPath, JSON.stringify(merged, null, 2), "utf-8");
4368
+ writeFileSync6(cfgPath, JSON.stringify(merged, null, 2), "utf-8");
4277
4369
  enforcePrivateFileSync(cfgPath);
4278
4370
  }
4279
4371
  async function mergeRosterFromRemote(remote, paths) {
4280
4372
  return withRosterLock(async () => {
4281
4373
  const rosterPath = paths?.rosterPath ?? void 0;
4282
- const identityDir = paths?.identityDir ?? path10.join(EXE_AI_DIR, "identity");
4374
+ const identityDir = paths?.identityDir ?? path11.join(EXE_AI_DIR, "identity");
4283
4375
  const localEmployees = await loadEmployees(rosterPath);
4284
4376
  const localNames = new Set(localEmployees.map((e) => e.name));
4285
4377
  let added = 0;
@@ -4300,15 +4392,15 @@ async function mergeRosterFromRemote(remote, paths) {
4300
4392
  ) ?? lookupKey;
4301
4393
  const remoteIdentity = remote.identities[matchedKey];
4302
4394
  if (remoteIdentity) {
4303
- if (!existsSync10(identityDir)) mkdirSync5(identityDir, { recursive: true });
4304
- const idPath = path10.join(identityDir, `${remoteEmp.name}.md`);
4395
+ if (!existsSync11(identityDir)) mkdirSync6(identityDir, { recursive: true });
4396
+ const idPath = path11.join(identityDir, `${remoteEmp.name}.md`);
4305
4397
  let localIdentity = null;
4306
4398
  try {
4307
- localIdentity = existsSync10(idPath) ? readFileSync7(idPath, "utf-8") : null;
4399
+ localIdentity = existsSync11(idPath) ? readFileSync8(idPath, "utf-8") : null;
4308
4400
  } catch {
4309
4401
  }
4310
4402
  if (localIdentity !== remoteIdentity) {
4311
- writeFileSync5(idPath, remoteIdentity, "utf-8");
4403
+ writeFileSync6(idPath, remoteIdentity, "utf-8");
4312
4404
  identitiesUpdated++;
4313
4405
  }
4314
4406
  }
@@ -4334,17 +4426,17 @@ async function mergeRosterFromRemote(remote, paths) {
4334
4426
  }
4335
4427
  if (remote.agentConfig && Object.keys(remote.agentConfig).length > 0) {
4336
4428
  try {
4337
- const agentConfigPath = path10.join(EXE_AI_DIR, "agent-config.json");
4429
+ const agentConfigPath = path11.join(EXE_AI_DIR, "agent-config.json");
4338
4430
  let local = {};
4339
- if (existsSync10(agentConfigPath)) {
4431
+ if (existsSync11(agentConfigPath)) {
4340
4432
  try {
4341
- local = JSON.parse(readFileSync7(agentConfigPath, "utf-8"));
4433
+ local = JSON.parse(readFileSync8(agentConfigPath, "utf-8"));
4342
4434
  } catch {
4343
4435
  }
4344
4436
  }
4345
4437
  const merged = { ...remote.agentConfig, ...local };
4346
- ensurePrivateDirSync(path10.dirname(agentConfigPath));
4347
- writeFileSync5(agentConfigPath, JSON.stringify(merged, null, 2) + "\n", "utf-8");
4438
+ ensurePrivateDirSync(path11.dirname(agentConfigPath));
4439
+ writeFileSync6(agentConfigPath, JSON.stringify(merged, null, 2) + "\n", "utf-8");
4348
4440
  enforcePrivateFileSync(agentConfigPath);
4349
4441
  } catch {
4350
4442
  }
@@ -4769,7 +4861,7 @@ async function cloudPullDocuments(config) {
4769
4861
  }
4770
4862
  return { pulled };
4771
4863
  }
4772
- var LOCALHOST_PATTERNS, FETCH_TIMEOUT_MS, PUSH_BATCH_SIZE, ROSTER_LOCK_PATH, LOCK_STALE_MS, _pgPromise, _pgFailed, ROSTER_DELETIONS_PATH;
4864
+ var LOCALHOST_PATTERNS, FETCH_TIMEOUT_MS, PUSH_BATCH_SIZE, ROSTER_LOCK_PATH, LOCK_STALE_MS, _pgPromise, _pgFailed, CLOUD_RELINK_REQUIRED_MESSAGE, ROSTER_DELETIONS_PATH;
4773
4865
  var init_cloud_sync = __esm({
4774
4866
  "src/lib/cloud-sync.ts"() {
4775
4867
  "use strict";
@@ -4784,11 +4876,12 @@ var init_cloud_sync = __esm({
4784
4876
  LOCALHOST_PATTERNS = /^(localhost|127\.0\.0\.1|\[::1\])$/i;
4785
4877
  FETCH_TIMEOUT_MS = 3e4;
4786
4878
  PUSH_BATCH_SIZE = 5e3;
4787
- ROSTER_LOCK_PATH = path10.join(EXE_AI_DIR, "roster-merge.lock");
4879
+ ROSTER_LOCK_PATH = path11.join(EXE_AI_DIR, "roster-merge.lock");
4788
4880
  LOCK_STALE_MS = 3e4;
4789
4881
  _pgPromise = null;
4790
4882
  _pgFailed = false;
4791
- ROSTER_DELETIONS_PATH = path10.join(EXE_AI_DIR, "roster-deletions.json");
4883
+ CLOUD_RELINK_REQUIRED_MESSAGE = "[cloud-sync] Paused after key rotation. Run `exe-os cloud relink --dry-run` for the safe relink checklist.";
4884
+ ROSTER_DELETIONS_PATH = path11.join(EXE_AI_DIR, "roster-deletions.json");
4792
4885
  }
4793
4886
  });
4794
4887
 
@@ -4812,74 +4905,113 @@ function isMainModule(importMetaUrl) {
4812
4905
  }
4813
4906
 
4814
4907
  // src/bin/exe-link.ts
4815
- async function main() {
4816
- const mode = process.argv[2];
4817
- if (mode === "export") {
4818
- const key = await getMasterKey();
4819
- if (!key) {
4820
- console.error("No master key found. Run /exe-setup first.");
4821
- process.exit(1);
4822
- }
4823
- const mnemonic = await exportMnemonic(key);
4824
- console.log("Your 24-word recovery phrase:\n");
4825
- const showFull = process.argv.includes("--show-full");
4826
- if (showFull) {
4827
- console.log(` ${mnemonic}
4908
+ function isInteractiveTerminal() {
4909
+ return Boolean(process.stdin.isTTY && process.stdout.isTTY && !process.env.CI);
4910
+ }
4911
+ function assertLocalTerminalExportAllowed() {
4912
+ if (!isInteractiveTerminal()) {
4913
+ throw new Error(
4914
+ "Refusing to reveal the recovery phrase outside a local interactive terminal.\nRun this yourself in Terminal: exe-os link export --local-terminal-only"
4915
+ );
4916
+ }
4917
+ }
4918
+ async function printStatus() {
4919
+ const key = await getMasterKey();
4920
+ if (key) {
4921
+ console.log("Master key: FOUND");
4922
+ console.log(`Key length: ${key.length} bytes`);
4923
+ console.log("Recovery export: available from a local interactive terminal");
4924
+ } else {
4925
+ console.log("Master key: MISSING");
4926
+ console.log("Checked normal storage paths: OS keychain/keytar/file fallback");
4927
+ process.exitCode = 1;
4928
+ }
4929
+ }
4930
+ async function exportKey(argv = process.argv.slice(2)) {
4931
+ const key = await getMasterKey();
4932
+ if (!key) {
4933
+ console.error("No master key found. Run /exe-setup first or import your recovery phrase with exe-os link import.");
4934
+ process.exit(1);
4935
+ }
4936
+ const showFull = argv.includes("--show-full") || argv.includes("--local-terminal-only");
4937
+ if (showFull) {
4938
+ assertLocalTerminalExportAllowed();
4939
+ }
4940
+ const mnemonic = await exportMnemonic(key);
4941
+ console.log("Your 24-word recovery phrase:\n");
4942
+ if (showFull) {
4943
+ console.log(` ${mnemonic}
4828
4944
  `);
4829
- } else {
4830
- const words = mnemonic.split(" ");
4831
- const masked = words.length > 4 ? [...words.slice(0, 2), ...words.slice(2, -2).map(() => "****"), ...words.slice(-2)].join(" ") : mnemonic;
4832
- console.log(` ${masked}
4945
+ const { markKeyBackupConfirmed: markKeyBackupConfirmed2 } = await Promise.resolve().then(() => (init_key_backup_status(), key_backup_status_exports));
4946
+ markKeyBackupConfirmed2("exe-os link export --local-terminal-only");
4947
+ } else {
4948
+ const words = mnemonic.split(" ");
4949
+ const masked = words.length > 4 ? [...words.slice(0, 2), ...words.slice(2, -2).map(() => "****"), ...words.slice(-2)].join(" ") : mnemonic;
4950
+ console.log(` ${masked}
4833
4951
  `);
4834
- console.log("To reveal full phrase: run with --show-full\n");
4835
- }
4836
- console.log("Write this down and enter it on your new device with /exe-link import.");
4837
- console.log("Anyone with this phrase can decrypt your memories.");
4838
- console.log("\u26A0 Clear your terminal history after copying.");
4839
- } else if (mode === "import") {
4840
- const rl = createInterface({ input: process.stdin, output: process.stdout });
4841
- const mnemonic = await new Promise((resolve) => {
4842
- rl.question("Paste your 24-word recovery phrase: ", (answer) => {
4843
- rl.close();
4844
- resolve(answer.trim());
4845
- });
4952
+ console.log("To reveal full phrase locally: exe-os link export --local-terminal-only\n");
4953
+ }
4954
+ console.log("Write this down and enter it on your new device with /exe-link import.");
4955
+ console.log("Anyone with this phrase can decrypt your memories.");
4956
+ console.log("\u26A0 Clear your terminal history after copying.");
4957
+ }
4958
+ async function importKey() {
4959
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
4960
+ const mnemonic = await new Promise((resolve) => {
4961
+ rl.question("Paste your 24-word recovery phrase: ", (answer) => {
4962
+ rl.close();
4963
+ resolve(answer.trim());
4846
4964
  });
4965
+ });
4966
+ try {
4967
+ const key = await importMnemonic(mnemonic);
4968
+ await setMasterKey(key);
4969
+ console.log("Master key imported and stored securely.");
4847
4970
  try {
4848
- const key = await importMnemonic(mnemonic);
4849
- await setMasterKey(key);
4850
- console.log("Master key imported and stored securely.");
4851
- try {
4852
- const { loadConfig: loadConfig2 } = await Promise.resolve().then(() => (init_config(), config_exports));
4853
- const { initSyncCrypto: initSyncCrypto2 } = await Promise.resolve().then(() => (init_crypto(), crypto_exports));
4854
- const { cloudPullRoster: cloudPullRoster2 } = await Promise.resolve().then(() => (init_cloud_sync(), cloud_sync_exports));
4855
- const config = await loadConfig2();
4856
- if (config.cloud?.apiKey && config.cloud?.endpoint) {
4857
- initSyncCrypto2(key);
4858
- const result = await cloudPullRoster2({ apiKey: config.cloud.apiKey, endpoint: config.cloud.endpoint });
4859
- if (result.added > 0) {
4860
- console.log(`Pulled ${result.added} employee(s) from Exe Cloud.`);
4861
- }
4971
+ const { loadConfig: loadConfig2 } = await Promise.resolve().then(() => (init_config(), config_exports));
4972
+ const { initSyncCrypto: initSyncCrypto2 } = await Promise.resolve().then(() => (init_crypto(), crypto_exports));
4973
+ const { cloudPullRoster: cloudPullRoster2 } = await Promise.resolve().then(() => (init_cloud_sync(), cloud_sync_exports));
4974
+ const config = await loadConfig2();
4975
+ if (config.cloud?.apiKey && config.cloud?.endpoint) {
4976
+ initSyncCrypto2(key);
4977
+ const result = await cloudPullRoster2({ apiKey: config.cloud.apiKey, endpoint: config.cloud.endpoint });
4978
+ if (result.added > 0) {
4979
+ console.log(`Pulled ${result.added} employee(s) from Exe Cloud.`);
4862
4980
  }
4863
- } catch {
4864
4981
  }
4865
- console.log("Run /exe-setup to configure sync and download the embedding model.");
4866
- } catch (err) {
4867
- console.error(err instanceof Error ? err.message : String(err));
4868
- process.exit(1);
4982
+ } catch {
4869
4983
  }
4984
+ console.log("Run /exe-setup to configure sync and download the embedding model.");
4985
+ } catch (err) {
4986
+ console.error(err instanceof Error ? err.message : String(err));
4987
+ process.exit(1);
4988
+ }
4989
+ }
4990
+ async function main(argv = process.argv.slice(2)) {
4991
+ const mode = argv[0];
4992
+ if (mode === "status") {
4993
+ await printStatus();
4994
+ } else if (mode === "export") {
4995
+ await exportKey(argv);
4996
+ } else if (mode === "import") {
4997
+ await importKey();
4870
4998
  } else {
4871
4999
  console.error("Usage:");
4872
- console.error(" exe-link export \u2014 show 24-word mnemonic for current key");
4873
- console.error(" exe-link import \u2014 paste mnemonic to import key on new device");
5000
+ console.error(" exe-link status \u2014 verify key availability without printing it");
5001
+ console.error(" exe-link export \u2014 show masked 24-word mnemonic");
5002
+ console.error(" exe-link export --local-terminal-only \u2014 reveal full phrase in local Terminal only");
5003
+ console.error(" exe-link import \u2014 paste mnemonic to import key on new device");
4874
5004
  process.exit(1);
4875
5005
  }
4876
5006
  }
4877
- if (isMainModule(import.meta.url)) {
5007
+ if (isMainModule(import.meta.url) && process.argv[1]?.includes("exe-link")) {
4878
5008
  main().catch((err) => {
4879
5009
  console.error(err instanceof Error ? err.message : String(err));
4880
5010
  process.exit(1);
4881
5011
  });
4882
5012
  }
4883
5013
  export {
5014
+ assertLocalTerminalExportAllowed,
5015
+ isInteractiveTerminal,
4884
5016
  main
4885
5017
  };