@askexenow/exe-os 0.8.38 → 0.8.39

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 (91) hide show
  1. package/README.md +17 -8
  2. package/dist/bin/backfill-conversations.js +46 -10
  3. package/dist/bin/backfill-responses.js +46 -10
  4. package/dist/bin/backfill-vectors.js +42 -8
  5. package/dist/bin/cleanup-stale-review-tasks.js +37 -8
  6. package/dist/bin/cli.js +281 -154
  7. package/dist/bin/exe-agent.js +19 -4
  8. package/dist/bin/exe-assign.js +39 -5
  9. package/dist/bin/exe-boot.js +237 -111
  10. package/dist/bin/exe-call.js +11 -6
  11. package/dist/bin/exe-cloud.js +99 -28
  12. package/dist/bin/exe-dispatch.js +1 -1
  13. package/dist/bin/exe-doctor.js +37 -8
  14. package/dist/bin/exe-export-behaviors.js +39 -10
  15. package/dist/bin/exe-forget.js +38 -9
  16. package/dist/bin/exe-gateway.js +109 -42
  17. package/dist/bin/exe-heartbeat.js +49 -20
  18. package/dist/bin/exe-kill.js +39 -10
  19. package/dist/bin/exe-launch-agent.js +58 -22
  20. package/dist/bin/exe-link.js +184 -85
  21. package/dist/bin/exe-new-employee.js +21 -7
  22. package/dist/bin/exe-pending-messages.js +46 -17
  23. package/dist/bin/exe-pending-notifications.js +37 -8
  24. package/dist/bin/exe-pending-reviews.js +47 -18
  25. package/dist/bin/exe-rename.js +21 -7
  26. package/dist/bin/exe-review.js +34 -5
  27. package/dist/bin/exe-search.js +47 -10
  28. package/dist/bin/exe-session-cleanup.js +56 -19
  29. package/dist/bin/exe-settings.js +63 -2
  30. package/dist/bin/exe-status.js +34 -5
  31. package/dist/bin/exe-team.js +34 -5
  32. package/dist/bin/git-sweep.js +38 -9
  33. package/dist/bin/graph-backfill.js +37 -8
  34. package/dist/bin/graph-export.js +37 -8
  35. package/dist/bin/install.js +1 -1
  36. package/dist/bin/scan-tasks.js +40 -11
  37. package/dist/bin/setup.js +58 -24
  38. package/dist/bin/shard-migrate.js +37 -8
  39. package/dist/bin/wiki-sync.js +39 -9
  40. package/dist/gateway/index.js +102 -37
  41. package/dist/hooks/bug-report-worker.js +62 -28
  42. package/dist/hooks/commit-complete.js +38 -9
  43. package/dist/hooks/error-recall.js +49 -8
  44. package/dist/hooks/exe-heartbeat-hook.js +3 -2
  45. package/dist/hooks/ingest-worker.js +151 -37
  46. package/dist/hooks/ingest.js +74 -28
  47. package/dist/hooks/instructions-loaded.js +39 -9
  48. package/dist/hooks/notification.js +37 -7
  49. package/dist/hooks/post-compact.js +37 -7
  50. package/dist/hooks/pre-compact.js +35 -6
  51. package/dist/hooks/pre-tool-use.js +52 -14
  52. package/dist/hooks/prompt-ingest-worker.js +56 -10
  53. package/dist/hooks/prompt-submit.js +61 -23
  54. package/dist/hooks/response-ingest-worker.js +57 -11
  55. package/dist/hooks/session-end.js +43 -10
  56. package/dist/hooks/session-start.js +46 -8
  57. package/dist/hooks/stop.js +37 -7
  58. package/dist/hooks/subagent-stop.js +37 -7
  59. package/dist/hooks/summary-worker.js +317 -99
  60. package/dist/index.js +87 -22
  61. package/dist/lib/cloud-sync.js +172 -78
  62. package/dist/lib/config.js +4 -1
  63. package/dist/lib/consolidation.js +5 -4
  64. package/dist/lib/database.js +1 -0
  65. package/dist/lib/device-registry.js +2 -1
  66. package/dist/lib/embedder.js +9 -1
  67. package/dist/lib/employees.js +11 -6
  68. package/dist/lib/exe-daemon-client.js +6 -1
  69. package/dist/lib/exe-daemon.js +71 -28
  70. package/dist/lib/hybrid-search.js +47 -10
  71. package/dist/lib/identity.js +1 -1
  72. package/dist/lib/keychain.js +2 -1
  73. package/dist/lib/license.js +13 -4
  74. package/dist/lib/messaging.js +1 -1
  75. package/dist/lib/reminders.js +2 -2
  76. package/dist/lib/schedules.js +37 -8
  77. package/dist/lib/skill-learning.js +1 -1
  78. package/dist/lib/store.js +37 -8
  79. package/dist/lib/tasks.js +1 -1
  80. package/dist/lib/tmux-routing.js +1 -1
  81. package/dist/mcp/server.js +97 -43
  82. package/dist/mcp/tools/complete-reminder.js +1 -1
  83. package/dist/mcp/tools/create-task.js +14 -6
  84. package/dist/mcp/tools/deactivate-behavior.js +2 -2
  85. package/dist/mcp/tools/list-reminders.js +1 -1
  86. package/dist/mcp/tools/list-tasks.js +1 -1
  87. package/dist/mcp/tools/send-message.js +1 -1
  88. package/dist/mcp/tools/update-task.js +1 -1
  89. package/dist/runtime/index.js +35 -6
  90. package/dist/tui/App.js +177 -95
  91. package/package.json +3 -3
@@ -31,15 +31,15 @@ __export(config_exports, {
31
31
  migrateConfig: () => migrateConfig,
32
32
  saveConfig: () => saveConfig
33
33
  });
34
- import { readFile as readFile2, writeFile as writeFile2, mkdir as mkdir2 } from "fs/promises";
34
+ import { readFile as readFile2, writeFile as writeFile2, mkdir as mkdir2, chmod as chmod2 } from "fs/promises";
35
35
  import { readFileSync, existsSync as existsSync2, renameSync } from "fs";
36
36
  import path2 from "path";
37
- import os from "os";
37
+ import os2 from "os";
38
38
  function resolveDataDir() {
39
39
  if (process.env.EXE_OS_DIR) return process.env.EXE_OS_DIR;
40
40
  if (process.env.EXE_MEM_DIR) return process.env.EXE_MEM_DIR;
41
- const newDir = path2.join(os.homedir(), ".exe-os");
42
- const legacyDir = path2.join(os.homedir(), ".exe-mem");
41
+ const newDir = path2.join(os2.homedir(), ".exe-os");
42
+ const legacyDir = path2.join(os2.homedir(), ".exe-mem");
43
43
  if (!existsSync2(newDir) && existsSync2(legacyDir)) {
44
44
  try {
45
45
  renameSync(legacyDir, newDir);
@@ -126,7 +126,7 @@ async function loadConfig() {
126
126
  normalizeAutoUpdate(migratedCfg);
127
127
  const config = { ...DEFAULT_CONFIG, dbPath: path2.join(dir, "memories.db"), ...migratedCfg };
128
128
  if (config.dbPath.startsWith("~")) {
129
- config.dbPath = config.dbPath.replace(/^~/, os.homedir());
129
+ config.dbPath = config.dbPath.replace(/^~/, os2.homedir());
130
130
  }
131
131
  return config;
132
132
  } catch {
@@ -157,6 +157,9 @@ async function saveConfig(config) {
157
157
  await mkdir2(dir, { recursive: true });
158
158
  const configPath = path2.join(dir, "config.json");
159
159
  await writeFile2(configPath, JSON.stringify(config, null, 2) + "\n");
160
+ if (config.cloud?.apiKey) {
161
+ await chmod2(configPath, 384);
162
+ }
160
163
  }
161
164
  async function loadConfigFrom(configPath) {
162
165
  const raw = await readFile2(configPath, "utf-8");
@@ -381,15 +384,20 @@ async function saveEmployees(employees, employeesPath = EMPLOYEES_PATH) {
381
384
  await mkdir3(path3.dirname(employeesPath), { recursive: true });
382
385
  await writeFile3(employeesPath, JSON.stringify(employees, null, 2) + "\n", "utf-8");
383
386
  }
387
+ function findExeBin() {
388
+ try {
389
+ return execSync(process.platform === "win32" ? "where exe-os" : "which exe-os", { encoding: "utf8" }).trim();
390
+ } catch {
391
+ return null;
392
+ }
393
+ }
384
394
  function registerBinSymlinks(name) {
385
395
  const created = [];
386
396
  const skipped = [];
387
397
  const errors = [];
388
- let exeBinPath;
389
- try {
390
- exeBinPath = execSync("which exe", { encoding: "utf-8" }).trim();
391
- } catch {
392
- errors.push("Could not find 'exe' in PATH");
398
+ const exeBinPath = findExeBin();
399
+ if (!exeBinPath) {
400
+ errors.push("Could not find 'exe-os' in PATH");
393
401
  return { created, skipped, errors };
394
402
  }
395
403
  const binDir = path3.dirname(exeBinPath);
@@ -430,6 +438,14 @@ import { readFileSync as readFileSync3, writeFileSync, existsSync as existsSync4
430
438
  import { randomUUID } from "crypto";
431
439
  import path4 from "path";
432
440
  import { jwtVerify, importSPKI } from "jose";
441
+ async function fetchRetry(url, init) {
442
+ try {
443
+ return await fetch(url, init);
444
+ } catch {
445
+ await new Promise((r) => setTimeout(r, RETRY_DELAY_MS));
446
+ return fetch(url, { ...init, signal: AbortSignal.timeout(1e4) });
447
+ }
448
+ }
433
449
  function loadDeviceId() {
434
450
  const deviceJsonPath = path4.join(EXE_AI_DIR, "device.json");
435
451
  try {
@@ -500,7 +516,7 @@ function cacheResponse(token) {
500
516
  async function validateLicense(apiKey, deviceId) {
501
517
  const did = deviceId ?? loadDeviceId();
502
518
  try {
503
- const res = await fetch(`${API_BASE}/auth/activate`, {
519
+ const res = await fetchRetry(`${API_BASE}/auth/activate`, {
504
520
  method: "POST",
505
521
  headers: { "Content-Type": "application/json" },
506
522
  body: JSON.stringify({ apiKey, deviceId: did }),
@@ -565,7 +581,7 @@ function isFeatureAllowed(license, feature) {
565
581
  return license.plan === "team" || license.plan === "agency" || license.plan === "enterprise";
566
582
  }
567
583
  }
568
- var LICENSE_PATH, CACHE_PATH, DEVICE_ID_PATH, API_BASE, LICENSE_PUBLIC_KEY_PEM, LICENSE_JWT_ALG, PLAN_LIMITS, FREE_LICENSE, CACHE_MAX_AGE_MS;
584
+ var LICENSE_PATH, CACHE_PATH, DEVICE_ID_PATH, API_BASE, RETRY_DELAY_MS, LICENSE_PUBLIC_KEY_PEM, LICENSE_JWT_ALG, PLAN_LIMITS, FREE_LICENSE, CACHE_MAX_AGE_MS;
569
585
  var init_license = __esm({
570
586
  "src/lib/license.ts"() {
571
587
  "use strict";
@@ -574,6 +590,7 @@ var init_license = __esm({
574
590
  CACHE_PATH = path4.join(EXE_AI_DIR, "license-cache.json");
575
591
  DEVICE_ID_PATH = path4.join(EXE_AI_DIR, "device-id");
576
592
  API_BASE = "https://askexe.com/cloud";
593
+ RETRY_DELAY_MS = 500;
577
594
  LICENSE_PUBLIC_KEY_PEM = `-----BEGIN PUBLIC KEY-----
578
595
  MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEeHztAMOpR/ZMh+rWuOASjEZ54CGY
579
596
  4uj+UqeKCcvtgNHKmOK278HJaJcANe9xAeji8AFYu27q3WtzCi04pHudow==
@@ -631,6 +648,7 @@ var init_plan_limits = __esm({
631
648
  // src/lib/cloud-sync.ts
632
649
  var cloud_sync_exports = {};
633
650
  __export(cloud_sync_exports, {
651
+ assertSecureEndpoint: () => assertSecureEndpoint,
634
652
  buildRosterBlob: () => buildRosterBlob,
635
653
  cloudPull: () => cloudPull,
636
654
  cloudPullBehaviors: () => cloudPullBehaviors,
@@ -650,9 +668,10 @@ __export(cloud_sync_exports, {
650
668
  cloudPushTasks: () => cloudPushTasks,
651
669
  cloudSync: () => cloudSync,
652
670
  mergeConfig: () => mergeConfig,
653
- mergeRosterFromRemote: () => mergeRosterFromRemote
671
+ mergeRosterFromRemote: () => mergeRosterFromRemote,
672
+ recordRosterDeletion: () => recordRosterDeletion
654
673
  });
655
- import { readFileSync as readFileSync5, writeFileSync as writeFileSync2, existsSync as existsSync6, readdirSync, mkdirSync as mkdirSync2, appendFileSync } from "fs";
674
+ import { readFileSync as readFileSync5, writeFileSync as writeFileSync2, existsSync as existsSync6, readdirSync, mkdirSync as mkdirSync2, appendFileSync, unlinkSync } from "fs";
656
675
  import path6 from "path";
657
676
  import { homedir } from "os";
658
677
  function logError(msg) {
@@ -663,16 +682,47 @@ function logError(msg) {
663
682
  } catch {
664
683
  }
665
684
  }
685
+ async function withRosterLock(fn) {
686
+ if (existsSync6(ROSTER_LOCK_PATH)) {
687
+ try {
688
+ const ts = parseInt(readFileSync5(ROSTER_LOCK_PATH, "utf-8"), 10);
689
+ if (Date.now() - ts < LOCK_STALE_MS) {
690
+ throw new Error("Roster merge already in progress \u2014 another sync is running");
691
+ }
692
+ } catch (err) {
693
+ if (err instanceof Error && err.message.includes("already in progress")) throw err;
694
+ }
695
+ }
696
+ writeFileSync2(ROSTER_LOCK_PATH, String(Date.now()));
697
+ try {
698
+ return await fn();
699
+ } finally {
700
+ try {
701
+ unlinkSync(ROSTER_LOCK_PATH);
702
+ } catch {
703
+ }
704
+ }
705
+ }
666
706
  async function fetchWithRetry(url, init) {
667
- const attempt = async () => {
668
- const signal = AbortSignal.timeout(FETCH_TIMEOUT_MS);
669
- return fetch(url, { ...init, signal });
670
- };
671
- const resp = await attempt();
672
- if (resp.status >= 500) {
673
- return attempt();
707
+ const MAX_RETRIES = 3;
708
+ const BASE_DELAY_MS = 200;
709
+ let lastError;
710
+ for (let attempt = 0; attempt <= MAX_RETRIES; attempt++) {
711
+ try {
712
+ const signal = AbortSignal.timeout(FETCH_TIMEOUT_MS);
713
+ const resp = await fetch(url, { ...init, signal });
714
+ if (resp.status >= 500 && attempt < MAX_RETRIES) {
715
+ await new Promise((r) => setTimeout(r, BASE_DELAY_MS * Math.pow(2, attempt)));
716
+ continue;
717
+ }
718
+ return resp;
719
+ } catch (err) {
720
+ lastError = err;
721
+ if (attempt === MAX_RETRIES) throw err;
722
+ await new Promise((r) => setTimeout(r, BASE_DELAY_MS * Math.pow(2, attempt)));
723
+ }
674
724
  }
675
- return resp;
725
+ throw lastError;
676
726
  }
677
727
  function assertSecureEndpoint(endpoint) {
678
728
  if (endpoint.startsWith("https://")) return;
@@ -700,10 +750,15 @@ async function cloudPush(records, maxVersion, config) {
700
750
  headers: {
701
751
  Authorization: `Bearer ${config.apiKey}`,
702
752
  "Content-Type": "application/json",
703
- "X-Device-Id": loadDeviceId()
753
+ "X-Device-Id": loadDeviceId(),
754
+ "X-Expected-Version": String(maxVersion)
704
755
  },
705
756
  body: JSON.stringify({ version: maxVersion, blob })
706
757
  });
758
+ if (resp.status === 409) {
759
+ logError("[cloud-sync] PUSH VERSION CONFLICT \u2014 re-pull required before next push");
760
+ return false;
761
+ }
707
762
  return resp.ok;
708
763
  } catch (err) {
709
764
  logError(`[cloud-sync] PUSH FAILED: ${err instanceof Error ? err.message : String(err)}`);
@@ -790,18 +845,21 @@ async function cloudSync(config) {
790
845
  "SELECT value FROM sync_meta WHERE key = 'last_cloud_push_version'"
791
846
  );
792
847
  const lastPushVersion = pushMeta.rows.length > 0 ? Number(pushMeta.rows[0].value) : 0;
793
- const recordsResult = await client.execute({
794
- sql: `SELECT id, agent_id, agent_role, session_id, timestamp,
795
- tool_name, project_name, has_error, raw_text, version,
796
- author_device_id, scope
797
- FROM memories
798
- WHERE version > ?
799
- AND (scope IS NULL OR scope != 'personal')
800
- ORDER BY version ASC`,
801
- args: [lastPushVersion]
802
- });
803
848
  let pushed = 0;
804
- if (recordsResult.rows.length > 0) {
849
+ let batchCursor = lastPushVersion;
850
+ while (true) {
851
+ const recordsResult = await client.execute({
852
+ sql: `SELECT id, agent_id, agent_role, session_id, timestamp,
853
+ tool_name, project_name, has_error, raw_text, version,
854
+ author_device_id, scope
855
+ FROM memories
856
+ WHERE version > ?
857
+ AND (scope IS NULL OR scope != 'personal')
858
+ ORDER BY version ASC
859
+ LIMIT ?`,
860
+ args: [batchCursor, PUSH_BATCH_SIZE]
861
+ });
862
+ if (recordsResult.rows.length === 0) break;
805
863
  const records = recordsResult.rows.map((row) => ({
806
864
  id: row.id,
807
865
  agent_id: row.agent_id,
@@ -818,13 +876,14 @@ async function cloudSync(config) {
818
876
  }));
819
877
  const maxVersion = Number(records[records.length - 1].version);
820
878
  const pushOk = await cloudPush(records, maxVersion, config);
821
- if (pushOk) {
822
- await client.execute({
823
- sql: "INSERT OR REPLACE INTO sync_meta (key, value) VALUES ('last_cloud_push_version', ?)",
824
- args: [String(maxVersion)]
825
- });
826
- pushed = records.length;
827
- }
879
+ if (!pushOk) break;
880
+ await client.execute({
881
+ sql: "INSERT OR REPLACE INTO sync_meta (key, value) VALUES ('last_cloud_push_version', ?)",
882
+ args: [String(maxVersion)]
883
+ });
884
+ pushed += records.length;
885
+ batchCursor = maxVersion;
886
+ if (recordsResult.rows.length < PUSH_BATCH_SIZE) break;
828
887
  }
829
888
  try {
830
889
  await cloudPushRoster(config);
@@ -906,6 +965,27 @@ async function cloudSync(config) {
906
965
  documents: documentsResult
907
966
  };
908
967
  }
968
+ function recordRosterDeletion(name) {
969
+ let deletions = [];
970
+ try {
971
+ if (existsSync6(ROSTER_DELETIONS_PATH)) {
972
+ deletions = JSON.parse(readFileSync5(ROSTER_DELETIONS_PATH, "utf-8"));
973
+ }
974
+ } catch {
975
+ }
976
+ if (!deletions.includes(name)) deletions.push(name);
977
+ writeFileSync2(ROSTER_DELETIONS_PATH, JSON.stringify(deletions));
978
+ }
979
+ function consumeRosterDeletions() {
980
+ try {
981
+ if (!existsSync6(ROSTER_DELETIONS_PATH)) return [];
982
+ const deletions = JSON.parse(readFileSync5(ROSTER_DELETIONS_PATH, "utf-8"));
983
+ writeFileSync2(ROSTER_DELETIONS_PATH, "[]");
984
+ return deletions;
985
+ } catch {
986
+ return [];
987
+ }
988
+ }
909
989
  function buildRosterBlob(paths) {
910
990
  const rosterPath = paths?.rosterPath ?? path6.join(EXE_AI_DIR, "exe-employees.json");
911
991
  const identityDir = paths?.identityDir ?? path6.join(EXE_AI_DIR, "identity");
@@ -933,9 +1013,10 @@ function buildRosterBlob(paths) {
933
1013
  } catch {
934
1014
  }
935
1015
  }
936
- const content = JSON.stringify({ roster, identities, config });
1016
+ const deletedNames = consumeRosterDeletions();
1017
+ const content = JSON.stringify({ roster, identities, config, deletedNames });
937
1018
  const hash = Buffer.from(content).length;
938
- return { roster, identities, config, version: hash };
1019
+ return { roster, identities, config, deletedNames, version: hash };
939
1020
  }
940
1021
  async function cloudPushRoster(config) {
941
1022
  assertSecureEndpoint(config.endpoint);
@@ -1018,38 +1099,50 @@ function mergeConfig(remoteConfig, configPath) {
1018
1099
  writeFileSync2(cfgPath, JSON.stringify(merged, null, 2), "utf-8");
1019
1100
  }
1020
1101
  async function mergeRosterFromRemote(remote, paths) {
1021
- const rosterPath = paths?.rosterPath ?? void 0;
1022
- const identityDir = paths?.identityDir ?? path6.join(EXE_AI_DIR, "identity");
1023
- const localEmployees = await loadEmployees(rosterPath);
1024
- const localNames = new Set(localEmployees.map((e) => e.name));
1025
- let added = 0;
1026
- for (const remoteEmp of remote.roster) {
1027
- if (localNames.has(remoteEmp.name)) continue;
1028
- localEmployees.push(remoteEmp);
1029
- localNames.add(remoteEmp.name);
1030
- added++;
1031
- if (remote.identities[`${remoteEmp.name}.md`]) {
1032
- if (!existsSync6(identityDir)) mkdirSync2(identityDir, { recursive: true });
1033
- const idPath = path6.join(identityDir, `${remoteEmp.name}.md`);
1034
- if (!existsSync6(idPath)) {
1035
- writeFileSync2(idPath, remote.identities[`${remoteEmp.name}.md`], "utf-8");
1102
+ return withRosterLock(async () => {
1103
+ const rosterPath = paths?.rosterPath ?? void 0;
1104
+ const identityDir = paths?.identityDir ?? path6.join(EXE_AI_DIR, "identity");
1105
+ const localEmployees = await loadEmployees(rosterPath);
1106
+ const localNames = new Set(localEmployees.map((e) => e.name));
1107
+ let added = 0;
1108
+ for (const remoteEmp of remote.roster) {
1109
+ if (localNames.has(remoteEmp.name)) continue;
1110
+ localEmployees.push(remoteEmp);
1111
+ localNames.add(remoteEmp.name);
1112
+ added++;
1113
+ if (remote.identities[`${remoteEmp.name}.md`]) {
1114
+ if (!existsSync6(identityDir)) mkdirSync2(identityDir, { recursive: true });
1115
+ const idPath = path6.join(identityDir, `${remoteEmp.name}.md`);
1116
+ if (!existsSync6(idPath)) {
1117
+ writeFileSync2(idPath, remote.identities[`${remoteEmp.name}.md`], "utf-8");
1118
+ }
1119
+ }
1120
+ try {
1121
+ registerBinSymlinks(remoteEmp.name);
1122
+ } catch {
1036
1123
  }
1037
1124
  }
1038
- try {
1039
- registerBinSymlinks(remoteEmp.name);
1040
- } catch {
1125
+ let removed = 0;
1126
+ if (remote.deletedNames && remote.deletedNames.length > 0) {
1127
+ const toRemove = new Set(remote.deletedNames);
1128
+ const filtered = localEmployees.filter((e) => !toRemove.has(e.name));
1129
+ removed = localEmployees.length - filtered.length;
1130
+ if (removed > 0) {
1131
+ localEmployees.length = 0;
1132
+ localEmployees.push(...filtered);
1133
+ }
1041
1134
  }
1042
- }
1043
- if (added > 0) {
1044
- await saveEmployees(localEmployees, rosterPath);
1045
- }
1046
- if (remote.config && Object.keys(remote.config).length > 0) {
1047
- try {
1048
- mergeConfig(remote.config, paths?.configPath);
1049
- } catch {
1135
+ if (added > 0 || removed > 0) {
1136
+ await saveEmployees(localEmployees, rosterPath);
1050
1137
  }
1051
- }
1052
- return { added };
1138
+ if (remote.config && Object.keys(remote.config).length > 0) {
1139
+ try {
1140
+ mergeConfig(remote.config, paths?.configPath);
1141
+ } catch {
1142
+ }
1143
+ }
1144
+ return { added };
1145
+ });
1053
1146
  }
1054
1147
  async function cloudPushBlob(route, data, metaKey, config) {
1055
1148
  if (data.length === 0) return { ok: true };
@@ -1117,7 +1210,7 @@ async function cloudPullBlob(route, config) {
1117
1210
  }
1118
1211
  async function cloudPushBehaviors(config) {
1119
1212
  const client = getClient();
1120
- const result = await client.execute("SELECT * FROM behaviors");
1213
+ const result = await client.execute("SELECT * FROM behaviors LIMIT 10000");
1121
1214
  const rows = result.rows;
1122
1215
  const { ok } = await cloudPushBlob(
1123
1216
  "/sync/push-behaviors",
@@ -1165,13 +1258,13 @@ async function cloudPullBehaviors(config) {
1165
1258
  async function cloudPushGraphRAG(config) {
1166
1259
  const client = getClient();
1167
1260
  const [entities, relationships, aliases, entityMems, relMems, hyperedges, hyperedgeNodes] = await Promise.all([
1168
- client.execute("SELECT * FROM entities"),
1169
- client.execute("SELECT * FROM relationships"),
1170
- client.execute("SELECT * FROM entity_aliases"),
1171
- client.execute("SELECT * FROM entity_memories"),
1172
- client.execute("SELECT * FROM relationship_memories"),
1173
- client.execute("SELECT * FROM hyperedges"),
1174
- client.execute("SELECT * FROM hyperedge_nodes")
1261
+ client.execute("SELECT * FROM entities LIMIT 50000"),
1262
+ client.execute("SELECT * FROM relationships LIMIT 50000"),
1263
+ client.execute("SELECT * FROM entity_aliases LIMIT 50000"),
1264
+ client.execute("SELECT * FROM entity_memories LIMIT 50000"),
1265
+ client.execute("SELECT * FROM relationship_memories LIMIT 50000"),
1266
+ client.execute("SELECT * FROM hyperedges LIMIT 50000"),
1267
+ client.execute("SELECT * FROM hyperedge_nodes LIMIT 50000")
1175
1268
  ]);
1176
1269
  const blob = {
1177
1270
  entities: entities.rows,
@@ -1273,7 +1366,7 @@ async function cloudPullGraphRAG(config) {
1273
1366
  }
1274
1367
  async function cloudPushTasks(config) {
1275
1368
  const client = getClient();
1276
- const result = await client.execute("SELECT * FROM tasks");
1369
+ const result = await client.execute("SELECT * FROM tasks LIMIT 10000");
1277
1370
  const rows = result.rows;
1278
1371
  const { ok } = await cloudPushBlob(
1279
1372
  "/sync/push-tasks",
@@ -1319,7 +1412,7 @@ async function cloudPullTasks(config) {
1319
1412
  }
1320
1413
  async function cloudPushConversations(config) {
1321
1414
  const client = getClient();
1322
- const result = await client.execute("SELECT * FROM conversations");
1415
+ const result = await client.execute("SELECT * FROM conversations LIMIT 50000");
1323
1416
  const rows = result.rows;
1324
1417
  const { ok } = await cloudPushBlob(
1325
1418
  "/sync/push-conversations",
@@ -1369,8 +1462,8 @@ async function cloudPullConversations(config) {
1369
1462
  async function cloudPushDocuments(config) {
1370
1463
  const client = getClient();
1371
1464
  const [workspaces, documents] = await Promise.all([
1372
- client.execute("SELECT * FROM workspaces"),
1373
- client.execute("SELECT * FROM documents")
1465
+ client.execute("SELECT * FROM workspaces LIMIT 1000"),
1466
+ client.execute("SELECT * FROM documents LIMIT 10000")
1374
1467
  ]);
1375
1468
  const blob = {
1376
1469
  workspaces: workspaces.rows,
@@ -1423,7 +1516,7 @@ async function cloudPullDocuments(config) {
1423
1516
  }
1424
1517
  return { pulled };
1425
1518
  }
1426
- var LOCALHOST_PATTERNS, FETCH_TIMEOUT_MS;
1519
+ var LOCALHOST_PATTERNS, FETCH_TIMEOUT_MS, PUSH_BATCH_SIZE, ROSTER_LOCK_PATH, LOCK_STALE_MS, ROSTER_DELETIONS_PATH;
1427
1520
  var init_cloud_sync = __esm({
1428
1521
  "src/lib/cloud-sync.ts"() {
1429
1522
  "use strict";
@@ -1436,6 +1529,10 @@ var init_cloud_sync = __esm({
1436
1529
  init_employees();
1437
1530
  LOCALHOST_PATTERNS = /^(localhost|127\.0\.0\.1|\[::1\])$/i;
1438
1531
  FETCH_TIMEOUT_MS = 3e4;
1532
+ PUSH_BATCH_SIZE = 5e3;
1533
+ ROSTER_LOCK_PATH = path6.join(EXE_AI_DIR, "roster-merge.lock");
1534
+ LOCK_STALE_MS = 3e4;
1535
+ ROSTER_DELETIONS_PATH = path6.join(EXE_AI_DIR, "roster-deletions.json");
1439
1536
  }
1440
1537
  });
1441
1538
 
@@ -1446,11 +1543,12 @@ import { createInterface } from "readline";
1446
1543
  import { readFile, writeFile, unlink, mkdir, chmod } from "fs/promises";
1447
1544
  import { existsSync } from "fs";
1448
1545
  import path from "path";
1546
+ import os from "os";
1449
1547
  import crypto from "crypto";
1450
1548
  var SERVICE = "exe-mem";
1451
1549
  var ACCOUNT = "master-key";
1452
1550
  function getKeyDir() {
1453
- return process.env.EXE_OS_DIR ?? process.env.EXE_MEM_DIR ?? path.join(process.env.HOME ?? "/tmp", ".exe-os");
1551
+ return process.env.EXE_OS_DIR ?? process.env.EXE_MEM_DIR ?? path.join(os.homedir(), ".exe-os");
1454
1552
  }
1455
1553
  function getKeyPath() {
1456
1554
  return path.join(getKeyDir(), "master.key");
@@ -1590,6 +1688,7 @@ async function main() {
1590
1688
  `);
1591
1689
  console.log("Write this down and enter it on your new device with /exe-link import.");
1592
1690
  console.log("Anyone with this phrase can decrypt your memories.");
1691
+ console.log("\u26A0 Clear your terminal history after copying.");
1593
1692
  } else if (mode === "import") {
1594
1693
  const rl = createInterface({ input: process.stdin, output: process.stdout });
1595
1694
  const mnemonic = await new Promise((resolve) => {
@@ -16,7 +16,7 @@ var __export = (target, all) => {
16
16
  };
17
17
 
18
18
  // src/lib/config.ts
19
- import { readFile, writeFile, mkdir } from "fs/promises";
19
+ import { readFile, writeFile, mkdir, chmod } from "fs/promises";
20
20
  import { readFileSync, existsSync, renameSync } from "fs";
21
21
  import path from "path";
22
22
  import os from "os";
@@ -849,15 +849,20 @@ function addEmployee(employees, employee) {
849
849
  }
850
850
  return [...employees, normalized];
851
851
  }
852
+ function findExeBin() {
853
+ try {
854
+ return execSync(process.platform === "win32" ? "where exe-os" : "which exe-os", { encoding: "utf8" }).trim();
855
+ } catch {
856
+ return null;
857
+ }
858
+ }
852
859
  function registerBinSymlinks(name) {
853
860
  const created = [];
854
861
  const skipped = [];
855
862
  const errors = [];
856
- let exeBinPath;
857
- try {
858
- exeBinPath = execSync("which exe", { encoding: "utf-8" }).trim();
859
- } catch {
860
- errors.push("Could not find 'exe' in PATH");
863
+ const exeBinPath = findExeBin();
864
+ if (!exeBinPath) {
865
+ errors.push("Could not find 'exe-os' in PATH");
861
866
  return { created, skipped, errors };
862
867
  }
863
868
  const binDir = path2.dirname(exeBinPath);
@@ -1238,6 +1243,15 @@ var LICENSE_PATH = path3.join(EXE_AI_DIR, "license.key");
1238
1243
  var CACHE_PATH = path3.join(EXE_AI_DIR, "license-cache.json");
1239
1244
  var DEVICE_ID_PATH = path3.join(EXE_AI_DIR, "device-id");
1240
1245
  var API_BASE = "https://askexe.com/cloud";
1246
+ var RETRY_DELAY_MS = 500;
1247
+ async function fetchRetry(url, init) {
1248
+ try {
1249
+ return await fetch(url, init);
1250
+ } catch {
1251
+ await new Promise((r) => setTimeout(r, RETRY_DELAY_MS));
1252
+ return fetch(url, { ...init, signal: AbortSignal.timeout(1e4) });
1253
+ }
1254
+ }
1241
1255
  var LICENSE_PUBLIC_KEY_PEM = `-----BEGIN PUBLIC KEY-----
1242
1256
  MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEeHztAMOpR/ZMh+rWuOASjEZ54CGY
1243
1257
  4uj+UqeKCcvtgNHKmOK278HJaJcANe9xAeji8AFYu27q3WtzCi04pHudow==
@@ -1329,7 +1343,7 @@ function cacheResponse(token) {
1329
1343
  async function validateLicense(apiKey, deviceId) {
1330
1344
  const did = deviceId ?? loadDeviceId();
1331
1345
  try {
1332
- const res = await fetch(`${API_BASE}/auth/activate`, {
1346
+ const res = await fetchRetry(`${API_BASE}/auth/activate`, {
1333
1347
  method: "POST",
1334
1348
  headers: { "Content-Type": "application/json" },
1335
1349
  body: JSON.stringify({ apiKey, deviceId: did }),