@askexenow/exe-os 0.9.30 → 0.9.32

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 (64) hide show
  1. package/dist/bin/backfill-conversations.js +135 -7
  2. package/dist/bin/backfill-responses.js +135 -7
  3. package/dist/bin/backfill-vectors.js +135 -7
  4. package/dist/bin/cleanup-stale-review-tasks.js +139 -11
  5. package/dist/bin/cli.js +812 -486
  6. package/dist/bin/exe-assign.js +135 -7
  7. package/dist/bin/exe-boot.js +422 -113
  8. package/dist/bin/exe-cloud.js +160 -9
  9. package/dist/bin/exe-dispatch.js +136 -8
  10. package/dist/bin/exe-doctor.js +255 -13
  11. package/dist/bin/exe-export-behaviors.js +136 -8
  12. package/dist/bin/exe-forget.js +136 -8
  13. package/dist/bin/exe-gateway.js +171 -24
  14. package/dist/bin/exe-heartbeat.js +141 -13
  15. package/dist/bin/exe-kill.js +140 -12
  16. package/dist/bin/exe-launch-agent.js +143 -15
  17. package/dist/bin/exe-link.js +357 -48
  18. package/dist/bin/exe-pending-messages.js +136 -8
  19. package/dist/bin/exe-pending-notifications.js +136 -8
  20. package/dist/bin/exe-pending-reviews.js +138 -10
  21. package/dist/bin/exe-review.js +136 -8
  22. package/dist/bin/exe-search.js +155 -20
  23. package/dist/bin/exe-session-cleanup.js +166 -38
  24. package/dist/bin/exe-start-codex.js +142 -14
  25. package/dist/bin/exe-start-opencode.js +140 -12
  26. package/dist/bin/exe-status.js +148 -20
  27. package/dist/bin/exe-team.js +136 -8
  28. package/dist/bin/git-sweep.js +138 -10
  29. package/dist/bin/graph-backfill.js +135 -7
  30. package/dist/bin/graph-export.js +136 -8
  31. package/dist/bin/intercom-check.js +153 -25
  32. package/dist/bin/scan-tasks.js +138 -10
  33. package/dist/bin/setup.js +447 -121
  34. package/dist/bin/shard-migrate.js +135 -7
  35. package/dist/gateway/index.js +151 -23
  36. package/dist/hooks/bug-report-worker.js +151 -23
  37. package/dist/hooks/codex-stop-task-finalizer.js +145 -17
  38. package/dist/hooks/commit-complete.js +138 -10
  39. package/dist/hooks/error-recall.js +159 -24
  40. package/dist/hooks/ingest.js +142 -14
  41. package/dist/hooks/instructions-loaded.js +136 -8
  42. package/dist/hooks/notification.js +136 -8
  43. package/dist/hooks/post-compact.js +136 -8
  44. package/dist/hooks/post-tool-combined.js +159 -24
  45. package/dist/hooks/pre-compact.js +136 -8
  46. package/dist/hooks/pre-tool-use.js +144 -16
  47. package/dist/hooks/prompt-submit.js +195 -55
  48. package/dist/hooks/session-end.js +141 -13
  49. package/dist/hooks/session-start.js +165 -30
  50. package/dist/hooks/stop.js +136 -8
  51. package/dist/hooks/subagent-stop.js +136 -8
  52. package/dist/hooks/summary-worker.js +374 -65
  53. package/dist/index.js +136 -8
  54. package/dist/lib/cloud-sync.js +355 -46
  55. package/dist/lib/consolidation.js +1 -0
  56. package/dist/lib/exe-daemon.js +469 -127
  57. package/dist/lib/hybrid-search.js +155 -20
  58. package/dist/lib/keychain.js +191 -7
  59. package/dist/lib/schedules.js +138 -10
  60. package/dist/lib/store.js +135 -7
  61. package/dist/mcp/server.js +706 -213
  62. package/dist/runtime/index.js +136 -8
  63. package/dist/tui/App.js +208 -31
  64. package/package.json +1 -1
@@ -977,8 +977,8 @@ function findPackageRoot() {
977
977
  function getAvailableMemoryGB() {
978
978
  if (process.platform === "darwin") {
979
979
  try {
980
- const { execSync: execSync2 } = __require("child_process");
981
- const vmstat = execSync2("vm_stat", { encoding: "utf8" });
980
+ const { execSync: execSync3 } = __require("child_process");
981
+ const vmstat = execSync3("vm_stat", { encoding: "utf8" });
982
982
  const pageSize = 16384;
983
983
  const pageSizeMatch = vmstat.match(/page size of (\d+) bytes/);
984
984
  const actualPageSize = pageSizeMatch ? parseInt(pageSizeMatch[1], 10) : pageSize;
@@ -2747,6 +2747,7 @@ __export(keychain_exports, {
2747
2747
  });
2748
2748
  import { readFile as readFile3, writeFile as writeFile3, unlink, mkdir as mkdir3, chmod as chmod2 } from "fs/promises";
2749
2749
  import { existsSync as existsSync8 } from "fs";
2750
+ import { execSync as execSync2 } from "child_process";
2750
2751
  import path8 from "path";
2751
2752
  import os6 from "os";
2752
2753
  function getKeyDir() {
@@ -2755,6 +2756,83 @@ function getKeyDir() {
2755
2756
  function getKeyPath() {
2756
2757
  return path8.join(getKeyDir(), "master.key");
2757
2758
  }
2759
+ function macKeychainGet() {
2760
+ if (process.platform !== "darwin") return null;
2761
+ try {
2762
+ return execSync2(
2763
+ `security find-generic-password -s "${SERVICE}" -a "${ACCOUNT}" -w 2>/dev/null`,
2764
+ { encoding: "utf-8", timeout: 5e3 }
2765
+ ).trim();
2766
+ } catch {
2767
+ return null;
2768
+ }
2769
+ }
2770
+ function macKeychainSet(value) {
2771
+ if (process.platform !== "darwin") return false;
2772
+ try {
2773
+ try {
2774
+ execSync2(
2775
+ `security delete-generic-password -s "${SERVICE}" -a "${ACCOUNT}" 2>/dev/null`,
2776
+ { timeout: 5e3 }
2777
+ );
2778
+ } catch {
2779
+ }
2780
+ execSync2(
2781
+ `security add-generic-password -s "${SERVICE}" -a "${ACCOUNT}" -w "${value}"`,
2782
+ { timeout: 5e3 }
2783
+ );
2784
+ return true;
2785
+ } catch {
2786
+ return false;
2787
+ }
2788
+ }
2789
+ function macKeychainDelete() {
2790
+ if (process.platform !== "darwin") return false;
2791
+ try {
2792
+ execSync2(
2793
+ `security delete-generic-password -s "${SERVICE}" -a "${ACCOUNT}" 2>/dev/null`,
2794
+ { timeout: 5e3 }
2795
+ );
2796
+ return true;
2797
+ } catch {
2798
+ return false;
2799
+ }
2800
+ }
2801
+ function linuxSecretGet() {
2802
+ if (process.platform !== "linux") return null;
2803
+ try {
2804
+ return execSync2(
2805
+ `secret-tool lookup service "${SERVICE}" account "${ACCOUNT}" 2>/dev/null`,
2806
+ { encoding: "utf-8", timeout: 5e3 }
2807
+ ).trim();
2808
+ } catch {
2809
+ return null;
2810
+ }
2811
+ }
2812
+ function linuxSecretSet(value) {
2813
+ if (process.platform !== "linux") return false;
2814
+ try {
2815
+ execSync2(
2816
+ `echo -n "${value}" | secret-tool store --label="exe-os master key" service "${SERVICE}" account "${ACCOUNT}"`,
2817
+ { timeout: 5e3 }
2818
+ );
2819
+ return true;
2820
+ } catch {
2821
+ return false;
2822
+ }
2823
+ }
2824
+ function linuxSecretDelete() {
2825
+ if (process.platform !== "linux") return false;
2826
+ try {
2827
+ execSync2(
2828
+ `secret-tool clear service "${SERVICE}" account "${ACCOUNT}" 2>/dev/null`,
2829
+ { timeout: 5e3 }
2830
+ );
2831
+ return true;
2832
+ } catch {
2833
+ return false;
2834
+ }
2835
+ }
2758
2836
  async function tryKeytar() {
2759
2837
  try {
2760
2838
  return await import("keytar");
@@ -2762,13 +2840,72 @@ async function tryKeytar() {
2762
2840
  return null;
2763
2841
  }
2764
2842
  }
2843
+ function deriveMachineKey() {
2844
+ try {
2845
+ const crypto4 = __require("crypto");
2846
+ const material = [
2847
+ os6.hostname(),
2848
+ os6.userInfo().username,
2849
+ os6.arch(),
2850
+ os6.platform(),
2851
+ // Machine ID on Linux (stable across reboots)
2852
+ process.platform === "linux" ? readMachineId() : ""
2853
+ ].join("|");
2854
+ return crypto4.createHash("sha256").update(material).digest();
2855
+ } catch {
2856
+ return null;
2857
+ }
2858
+ }
2859
+ function readMachineId() {
2860
+ try {
2861
+ const { readFileSync: readFileSync8 } = __require("fs");
2862
+ return readFileSync8("/etc/machine-id", "utf-8").trim();
2863
+ } catch {
2864
+ return "";
2865
+ }
2866
+ }
2867
+ function encryptWithMachineKey(plaintext, machineKey) {
2868
+ const crypto4 = __require("crypto");
2869
+ const iv = crypto4.randomBytes(12);
2870
+ const cipher = crypto4.createCipheriv("aes-256-gcm", machineKey, iv);
2871
+ let encrypted = cipher.update(plaintext, "utf-8", "base64");
2872
+ encrypted += cipher.final("base64");
2873
+ const authTag = cipher.getAuthTag().toString("base64");
2874
+ return `${ENCRYPTED_PREFIX}${iv.toString("base64")}:${authTag}:${encrypted}`;
2875
+ }
2876
+ function decryptWithMachineKey(encrypted, machineKey) {
2877
+ if (!encrypted.startsWith(ENCRYPTED_PREFIX)) return null;
2878
+ try {
2879
+ const crypto4 = __require("crypto");
2880
+ const parts = encrypted.slice(ENCRYPTED_PREFIX.length).split(":");
2881
+ if (parts.length !== 3) return null;
2882
+ const [ivB64, tagB64, cipherB64] = parts;
2883
+ const iv = Buffer.from(ivB64, "base64");
2884
+ const authTag = Buffer.from(tagB64, "base64");
2885
+ const decipher = crypto4.createDecipheriv("aes-256-gcm", machineKey, iv);
2886
+ decipher.setAuthTag(authTag);
2887
+ let decrypted = decipher.update(cipherB64, "base64", "utf-8");
2888
+ decrypted += decipher.final("utf-8");
2889
+ return decrypted;
2890
+ } catch {
2891
+ return null;
2892
+ }
2893
+ }
2765
2894
  async function getMasterKey() {
2895
+ const nativeValue = macKeychainGet() ?? linuxSecretGet();
2896
+ if (nativeValue) {
2897
+ return Buffer.from(nativeValue, "base64");
2898
+ }
2766
2899
  const keytar = await tryKeytar();
2767
2900
  if (keytar) {
2768
2901
  try {
2769
- const stored = await keytar.getPassword(SERVICE, ACCOUNT);
2770
- if (stored) {
2771
- return Buffer.from(stored, "base64");
2902
+ const keytarValue = await keytar.getPassword(SERVICE, ACCOUNT);
2903
+ if (keytarValue) {
2904
+ const migrated = macKeychainSet(keytarValue) || linuxSecretSet(keytarValue);
2905
+ if (migrated) {
2906
+ process.stderr.write("[keychain] Migrated key from keytar to native keychain.\n");
2907
+ }
2908
+ return Buffer.from(keytarValue, "base64");
2772
2909
  }
2773
2910
  } catch {
2774
2911
  }
@@ -2782,8 +2919,31 @@ async function getMasterKey() {
2782
2919
  return null;
2783
2920
  }
2784
2921
  try {
2785
- const content = await readFile3(keyPath, "utf-8");
2786
- return Buffer.from(content.trim(), "base64");
2922
+ const content = (await readFile3(keyPath, "utf-8")).trim();
2923
+ let b64Value;
2924
+ if (content.startsWith(ENCRYPTED_PREFIX)) {
2925
+ const machineKey = deriveMachineKey();
2926
+ if (!machineKey) {
2927
+ process.stderr.write("[keychain] Cannot derive machine key to decrypt stored key.\n");
2928
+ return null;
2929
+ }
2930
+ const decrypted = decryptWithMachineKey(content, machineKey);
2931
+ if (!decrypted) {
2932
+ process.stderr.write(
2933
+ "[keychain] Key decryption failed \u2014 machine may have changed.\n Use your 24-word recovery phrase: exe-os link import\n"
2934
+ );
2935
+ return null;
2936
+ }
2937
+ b64Value = decrypted;
2938
+ } else {
2939
+ b64Value = content;
2940
+ }
2941
+ const key = Buffer.from(b64Value, "base64");
2942
+ const migrated = macKeychainSet(b64Value) || linuxSecretSet(b64Value);
2943
+ if (migrated) {
2944
+ process.stderr.write("[keychain] Migrated key from file to native keychain.\n");
2945
+ }
2946
+ return key;
2787
2947
  } catch (err) {
2788
2948
  process.stderr.write(
2789
2949
  `[keychain] Key read failed at ${keyPath}: ${err instanceof Error ? err.message : String(err)}
@@ -2794,6 +2954,9 @@ async function getMasterKey() {
2794
2954
  }
2795
2955
  async function setMasterKey(key) {
2796
2956
  const b64 = key.toString("base64");
2957
+ if (macKeychainSet(b64) || linuxSecretSet(b64)) {
2958
+ return;
2959
+ }
2797
2960
  const keytar = await tryKeytar();
2798
2961
  if (keytar) {
2799
2962
  try {
@@ -2805,10 +2968,23 @@ async function setMasterKey(key) {
2805
2968
  const dir = getKeyDir();
2806
2969
  await mkdir3(dir, { recursive: true });
2807
2970
  const keyPath = getKeyPath();
2808
- await writeFile3(keyPath, b64 + "\n", "utf-8");
2809
- await chmod2(keyPath, 384);
2971
+ const machineKey = deriveMachineKey();
2972
+ if (machineKey) {
2973
+ const encrypted = encryptWithMachineKey(b64, machineKey);
2974
+ await writeFile3(keyPath, encrypted + "\n", "utf-8");
2975
+ await chmod2(keyPath, 384);
2976
+ process.stderr.write("[keychain] Key stored encrypted (machine-bound).\n");
2977
+ } else {
2978
+ await writeFile3(keyPath, b64 + "\n", "utf-8");
2979
+ await chmod2(keyPath, 384);
2980
+ process.stderr.write(
2981
+ "[keychain] WARNING: Key stored in plaintext file \u2014 no OS keychain available.\n"
2982
+ );
2983
+ }
2810
2984
  }
2811
2985
  async function deleteMasterKey() {
2986
+ macKeychainDelete();
2987
+ linuxSecretDelete();
2812
2988
  const keytar = await tryKeytar();
2813
2989
  if (keytar) {
2814
2990
  try {
@@ -2850,20 +3026,124 @@ async function importMnemonic(mnemonic) {
2850
3026
  const entropy = mnemonicToEntropy(trimmed);
2851
3027
  return Buffer.from(entropy, "hex");
2852
3028
  }
2853
- var SERVICE, ACCOUNT;
3029
+ var SERVICE, ACCOUNT, ENCRYPTED_PREFIX;
2854
3030
  var init_keychain = __esm({
2855
3031
  "src/lib/keychain.ts"() {
2856
3032
  "use strict";
2857
3033
  SERVICE = "exe-mem";
2858
3034
  ACCOUNT = "master-key";
3035
+ ENCRYPTED_PREFIX = "enc:";
3036
+ }
3037
+ });
3038
+
3039
+ // src/lib/db-backup.ts
3040
+ var db_backup_exports = {};
3041
+ __export(db_backup_exports, {
3042
+ createBackup: () => createBackup,
3043
+ findActiveDb: () => findActiveDb,
3044
+ getBackupDir: () => getBackupDir,
3045
+ getLatestBackup: () => getLatestBackup,
3046
+ hasBackupToday: () => hasBackupToday,
3047
+ listBackups: () => listBackups,
3048
+ rotateBackups: () => rotateBackups
3049
+ });
3050
+ import { copyFileSync, existsSync as existsSync9, mkdirSync as mkdirSync4, readdirSync, unlinkSync as unlinkSync4, statSync as statSync2 } from "fs";
3051
+ import path9 from "path";
3052
+ function findActiveDb() {
3053
+ for (const name of DB_NAMES) {
3054
+ const p = path9.join(EXE_AI_DIR, name);
3055
+ if (existsSync9(p)) return p;
3056
+ }
3057
+ return null;
3058
+ }
3059
+ function createBackup(reason = "manual") {
3060
+ const dbPath = findActiveDb();
3061
+ if (!dbPath) return null;
3062
+ mkdirSync4(BACKUP_DIR, { recursive: true });
3063
+ const dbName = path9.basename(dbPath, ".db");
3064
+ const timestamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-").slice(0, 19);
3065
+ const backupName = `${dbName}-${reason}-${timestamp}.db`;
3066
+ const backupPath = path9.join(BACKUP_DIR, backupName);
3067
+ copyFileSync(dbPath, backupPath);
3068
+ const walPath = dbPath + "-wal";
3069
+ if (existsSync9(walPath)) {
3070
+ try {
3071
+ copyFileSync(walPath, backupPath + "-wal");
3072
+ } catch {
3073
+ }
3074
+ }
3075
+ const shmPath = dbPath + "-shm";
3076
+ if (existsSync9(shmPath)) {
3077
+ try {
3078
+ copyFileSync(shmPath, backupPath + "-shm");
3079
+ } catch {
3080
+ }
3081
+ }
3082
+ return backupPath;
3083
+ }
3084
+ function rotateBackups(keepDays = DEFAULT_KEEP_DAYS) {
3085
+ if (!existsSync9(BACKUP_DIR)) return 0;
3086
+ const cutoff = Date.now() - keepDays * 24 * 60 * 60 * 1e3;
3087
+ let deleted = 0;
3088
+ try {
3089
+ const files = readdirSync(BACKUP_DIR);
3090
+ for (const file of files) {
3091
+ if (!file.endsWith(".db") && !file.endsWith(".db-wal") && !file.endsWith(".db-shm")) continue;
3092
+ const filePath = path9.join(BACKUP_DIR, file);
3093
+ try {
3094
+ const stat = statSync2(filePath);
3095
+ if (stat.mtimeMs < cutoff) {
3096
+ unlinkSync4(filePath);
3097
+ deleted++;
3098
+ }
3099
+ } catch {
3100
+ }
3101
+ }
3102
+ } catch {
3103
+ }
3104
+ return deleted;
3105
+ }
3106
+ function listBackups() {
3107
+ if (!existsSync9(BACKUP_DIR)) return [];
3108
+ try {
3109
+ const files = readdirSync(BACKUP_DIR).filter((f) => f.endsWith(".db") && !f.endsWith("-wal") && !f.endsWith("-shm"));
3110
+ return files.map((name) => {
3111
+ const p = path9.join(BACKUP_DIR, name);
3112
+ const stat = statSync2(p);
3113
+ return { path: p, name, size: stat.size, date: stat.mtime };
3114
+ }).sort((a, b) => b.date.getTime() - a.date.getTime());
3115
+ } catch {
3116
+ return [];
3117
+ }
3118
+ }
3119
+ function hasBackupToday(reason) {
3120
+ const today = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
3121
+ const backups = listBackups();
3122
+ return backups.some((b) => b.name.includes(reason) && b.name.includes(today.replace(/-/g, "-")));
3123
+ }
3124
+ function getLatestBackup() {
3125
+ const backups = listBackups();
3126
+ return backups.length > 0 ? backups[0].path : null;
3127
+ }
3128
+ function getBackupDir() {
3129
+ return BACKUP_DIR;
3130
+ }
3131
+ var BACKUP_DIR, DEFAULT_KEEP_DAYS, DB_NAMES;
3132
+ var init_db_backup = __esm({
3133
+ "src/lib/db-backup.ts"() {
3134
+ "use strict";
3135
+ init_config();
3136
+ BACKUP_DIR = path9.join(EXE_AI_DIR, "backups");
3137
+ DEFAULT_KEEP_DAYS = 3;
3138
+ DB_NAMES = ["memories.db", "exe-mem.db", "exe-os.db", "exe.db"];
2859
3139
  }
2860
3140
  });
2861
3141
 
2862
3142
  // src/lib/cloud-sync.ts
2863
3143
  init_database();
2864
- import { readFileSync as readFileSync7, writeFileSync as writeFileSync5, existsSync as existsSync9, readdirSync, mkdirSync as mkdirSync4, appendFileSync, unlinkSync as unlinkSync4, openSync as openSync2, closeSync as closeSync2 } from "fs";
3144
+ 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 } from "fs";
2865
3145
  import crypto3 from "crypto";
2866
- import path9 from "path";
3146
+ import path10 from "path";
2867
3147
  import { homedir as homedir2 } from "os";
2868
3148
 
2869
3149
  // src/lib/crypto.ts
@@ -2971,7 +3251,7 @@ function sqlSafe(v) {
2971
3251
  }
2972
3252
  function logError(msg) {
2973
3253
  try {
2974
- const logPath = path9.join(homedir2(), ".exe-os", "workers.log");
3254
+ const logPath = path10.join(homedir2(), ".exe-os", "workers.log");
2975
3255
  appendFileSync(logPath, `${(/* @__PURE__ */ new Date()).toISOString()} ${msg}
2976
3256
  `);
2977
3257
  } catch {
@@ -2980,17 +3260,17 @@ function logError(msg) {
2980
3260
  var LOCALHOST_PATTERNS = /^(localhost|127\.0\.0\.1|\[::1\])$/i;
2981
3261
  var FETCH_TIMEOUT_MS = 3e4;
2982
3262
  var PUSH_BATCH_SIZE = 5e3;
2983
- var ROSTER_LOCK_PATH = path9.join(EXE_AI_DIR, "roster-merge.lock");
3263
+ var ROSTER_LOCK_PATH = path10.join(EXE_AI_DIR, "roster-merge.lock");
2984
3264
  var LOCK_STALE_MS = 3e4;
2985
3265
  var _pgPromise = null;
2986
3266
  var _pgFailed = false;
2987
3267
  function loadPgClient() {
2988
3268
  if (_pgFailed) return null;
2989
3269
  const postgresUrl = process.env.DATABASE_URL;
2990
- const configPath = path9.join(EXE_AI_DIR, "config.json");
3270
+ const configPath = path10.join(EXE_AI_DIR, "config.json");
2991
3271
  let cloudPostgresUrl;
2992
3272
  try {
2993
- if (existsSync9(configPath)) {
3273
+ if (existsSync10(configPath)) {
2994
3274
  const cfg = JSON.parse(readFileSync7(configPath, "utf8"));
2995
3275
  cloudPostgresUrl = cfg.cloud?.postgresUrl;
2996
3276
  if (cfg.cloud?.syncToPostgres === false) {
@@ -3009,8 +3289,8 @@ function loadPgClient() {
3009
3289
  _pgPromise = (async () => {
3010
3290
  const { createRequire: createRequire3 } = await import("module");
3011
3291
  const { pathToFileURL: pathToFileURL3 } = await import("url");
3012
- const exeDbRoot = process.env.EXE_DB_ROOT ?? path9.join(homedir2(), "exe-db");
3013
- const req = createRequire3(path9.join(exeDbRoot, "package.json"));
3292
+ const exeDbRoot = process.env.EXE_DB_ROOT ?? path10.join(homedir2(), "exe-db");
3293
+ const req = createRequire3(path10.join(exeDbRoot, "package.json"));
3014
3294
  const entry = req.resolve("@prisma/client");
3015
3295
  const mod = await import(pathToFileURL3(entry).href);
3016
3296
  const Ctor = mod.PrismaClient ?? mod.default?.PrismaClient;
@@ -3063,7 +3343,7 @@ async function withRosterLock(fn) {
3063
3343
  if (Date.now() - ts < LOCK_STALE_MS) {
3064
3344
  throw new Error("Roster merge already in progress \u2014 another sync is running");
3065
3345
  }
3066
- unlinkSync4(ROSTER_LOCK_PATH);
3346
+ unlinkSync5(ROSTER_LOCK_PATH);
3067
3347
  const fd = openSync2(ROSTER_LOCK_PATH, "wx");
3068
3348
  closeSync2(fd);
3069
3349
  writeFileSync5(ROSTER_LOCK_PATH, String(Date.now()));
@@ -3079,7 +3359,7 @@ async function withRosterLock(fn) {
3079
3359
  return await fn();
3080
3360
  } finally {
3081
3361
  try {
3082
- unlinkSync4(ROSTER_LOCK_PATH);
3362
+ unlinkSync5(ROSTER_LOCK_PATH);
3083
3363
  } catch {
3084
3364
  }
3085
3365
  }
@@ -3450,13 +3730,42 @@ async function cloudSync(config) {
3450
3730
  try {
3451
3731
  const employees = await loadEmployees();
3452
3732
  rosterResult.employees = employees.length;
3453
- const idDir = path9.join(EXE_AI_DIR, "identity");
3454
- if (existsSync9(idDir)) {
3455
- rosterResult.identities = readdirSync(idDir).filter((f) => f.endsWith(".md")).length;
3733
+ const idDir = path10.join(EXE_AI_DIR, "identity");
3734
+ if (existsSync10(idDir)) {
3735
+ rosterResult.identities = readdirSync2(idDir).filter((f) => f.endsWith(".md")).length;
3456
3736
  }
3457
3737
  } catch {
3458
3738
  }
3459
3739
  const totalMemories = await countRows("SELECT COUNT(*) as cnt FROM memories WHERE status = 'active' OR status IS NULL");
3740
+ try {
3741
+ const { getLatestBackup: getLatestBackup2 } = await Promise.resolve().then(() => (init_db_backup(), db_backup_exports));
3742
+ const { statSync: statFile } = await import("fs");
3743
+ const latestBackup = getLatestBackup2();
3744
+ if (latestBackup) {
3745
+ const backupSize = statFile(latestBackup).size;
3746
+ const MAX_CLOUD_BACKUP_BYTES = 50 * 1024 * 1024;
3747
+ if (backupSize <= MAX_CLOUD_BACKUP_BYTES) {
3748
+ const backupData = readFileSync7(latestBackup);
3749
+ const deviceId = loadDeviceId() ?? "unknown";
3750
+ const encrypted = encryptSyncBlob(backupData);
3751
+ const backupRes = await fetchWithRetry(`${config.endpoint}/sync/push-db-backup`, {
3752
+ method: "POST",
3753
+ headers: { "Content-Type": "application/json", Authorization: `Bearer ${config.apiKey}` },
3754
+ body: JSON.stringify({
3755
+ device_id: deviceId,
3756
+ filename: path10.basename(latestBackup),
3757
+ blob: encrypted,
3758
+ size: backupData.length
3759
+ })
3760
+ });
3761
+ if (backupRes && !backupRes.ok) {
3762
+ logError(`[cloud-sync] DB backup upload failed: ${backupRes.status}`);
3763
+ }
3764
+ }
3765
+ }
3766
+ } catch (err) {
3767
+ logError(`[cloud-sync] DB backup upload error: ${err instanceof Error ? err.message : String(err)}`);
3768
+ }
3460
3769
  return {
3461
3770
  pushed,
3462
3771
  pulled,
@@ -3469,11 +3778,11 @@ async function cloudSync(config) {
3469
3778
  roster: rosterResult
3470
3779
  };
3471
3780
  }
3472
- var ROSTER_DELETIONS_PATH = path9.join(EXE_AI_DIR, "roster-deletions.json");
3781
+ var ROSTER_DELETIONS_PATH = path10.join(EXE_AI_DIR, "roster-deletions.json");
3473
3782
  function recordRosterDeletion(name) {
3474
3783
  let deletions = [];
3475
3784
  try {
3476
- if (existsSync9(ROSTER_DELETIONS_PATH)) {
3785
+ if (existsSync10(ROSTER_DELETIONS_PATH)) {
3477
3786
  deletions = JSON.parse(readFileSync7(ROSTER_DELETIONS_PATH, "utf-8"));
3478
3787
  }
3479
3788
  } catch {
@@ -3483,7 +3792,7 @@ function recordRosterDeletion(name) {
3483
3792
  }
3484
3793
  function consumeRosterDeletions() {
3485
3794
  try {
3486
- if (!existsSync9(ROSTER_DELETIONS_PATH)) return [];
3795
+ if (!existsSync10(ROSTER_DELETIONS_PATH)) return [];
3487
3796
  const deletions = JSON.parse(readFileSync7(ROSTER_DELETIONS_PATH, "utf-8"));
3488
3797
  writeFileSync5(ROSTER_DELETIONS_PATH, "[]");
3489
3798
  return deletions;
@@ -3492,35 +3801,35 @@ function consumeRosterDeletions() {
3492
3801
  }
3493
3802
  }
3494
3803
  function buildRosterBlob(paths) {
3495
- const rosterPath = paths?.rosterPath ?? path9.join(EXE_AI_DIR, "exe-employees.json");
3496
- const identityDir = paths?.identityDir ?? path9.join(EXE_AI_DIR, "identity");
3497
- const configPath = paths?.configPath ?? path9.join(EXE_AI_DIR, "config.json");
3804
+ const rosterPath = paths?.rosterPath ?? path10.join(EXE_AI_DIR, "exe-employees.json");
3805
+ const identityDir = paths?.identityDir ?? path10.join(EXE_AI_DIR, "identity");
3806
+ const configPath = paths?.configPath ?? path10.join(EXE_AI_DIR, "config.json");
3498
3807
  let roster = [];
3499
- if (existsSync9(rosterPath)) {
3808
+ if (existsSync10(rosterPath)) {
3500
3809
  try {
3501
3810
  roster = JSON.parse(readFileSync7(rosterPath, "utf-8"));
3502
3811
  } catch {
3503
3812
  }
3504
3813
  }
3505
3814
  const identities = {};
3506
- if (existsSync9(identityDir)) {
3507
- for (const file of readdirSync(identityDir).filter((f) => f.endsWith(".md"))) {
3815
+ if (existsSync10(identityDir)) {
3816
+ for (const file of readdirSync2(identityDir).filter((f) => f.endsWith(".md"))) {
3508
3817
  try {
3509
- identities[file] = readFileSync7(path9.join(identityDir, file), "utf-8");
3818
+ identities[file] = readFileSync7(path10.join(identityDir, file), "utf-8");
3510
3819
  } catch {
3511
3820
  }
3512
3821
  }
3513
3822
  }
3514
3823
  let config;
3515
- if (existsSync9(configPath)) {
3824
+ if (existsSync10(configPath)) {
3516
3825
  try {
3517
3826
  config = JSON.parse(readFileSync7(configPath, "utf-8"));
3518
3827
  } catch {
3519
3828
  }
3520
3829
  }
3521
3830
  let agentConfig;
3522
- const agentConfigPath = path9.join(EXE_AI_DIR, "agent-config.json");
3523
- if (existsSync9(agentConfigPath)) {
3831
+ const agentConfigPath = path10.join(EXE_AI_DIR, "agent-config.json");
3832
+ if (existsSync10(agentConfigPath)) {
3524
3833
  try {
3525
3834
  agentConfig = JSON.parse(readFileSync7(agentConfigPath, "utf-8"));
3526
3835
  } catch {
@@ -3598,16 +3907,16 @@ async function cloudPullRoster(config) {
3598
3907
  }
3599
3908
  }
3600
3909
  function mergeConfig(remoteConfig, configPath) {
3601
- const cfgPath = configPath ?? path9.join(EXE_AI_DIR, "config.json");
3910
+ const cfgPath = configPath ?? path10.join(EXE_AI_DIR, "config.json");
3602
3911
  let local = {};
3603
- if (existsSync9(cfgPath)) {
3912
+ if (existsSync10(cfgPath)) {
3604
3913
  try {
3605
3914
  local = JSON.parse(readFileSync7(cfgPath, "utf-8"));
3606
3915
  } catch {
3607
3916
  }
3608
3917
  }
3609
3918
  const merged = { ...remoteConfig, ...local };
3610
- const dir = path9.dirname(cfgPath);
3919
+ const dir = path10.dirname(cfgPath);
3611
3920
  ensurePrivateDirSync(dir);
3612
3921
  writeFileSync5(cfgPath, JSON.stringify(merged, null, 2), "utf-8");
3613
3922
  enforcePrivateFileSync(cfgPath);
@@ -3615,7 +3924,7 @@ function mergeConfig(remoteConfig, configPath) {
3615
3924
  async function mergeRosterFromRemote(remote, paths) {
3616
3925
  return withRosterLock(async () => {
3617
3926
  const rosterPath = paths?.rosterPath ?? void 0;
3618
- const identityDir = paths?.identityDir ?? path9.join(EXE_AI_DIR, "identity");
3927
+ const identityDir = paths?.identityDir ?? path10.join(EXE_AI_DIR, "identity");
3619
3928
  const localEmployees = await loadEmployees(rosterPath);
3620
3929
  const localNames = new Set(localEmployees.map((e) => e.name));
3621
3930
  let added = 0;
@@ -3636,11 +3945,11 @@ async function mergeRosterFromRemote(remote, paths) {
3636
3945
  ) ?? lookupKey;
3637
3946
  const remoteIdentity = remote.identities[matchedKey];
3638
3947
  if (remoteIdentity) {
3639
- if (!existsSync9(identityDir)) mkdirSync4(identityDir, { recursive: true });
3640
- const idPath = path9.join(identityDir, `${remoteEmp.name}.md`);
3948
+ if (!existsSync10(identityDir)) mkdirSync5(identityDir, { recursive: true });
3949
+ const idPath = path10.join(identityDir, `${remoteEmp.name}.md`);
3641
3950
  let localIdentity = null;
3642
3951
  try {
3643
- localIdentity = existsSync9(idPath) ? readFileSync7(idPath, "utf-8") : null;
3952
+ localIdentity = existsSync10(idPath) ? readFileSync7(idPath, "utf-8") : null;
3644
3953
  } catch {
3645
3954
  }
3646
3955
  if (localIdentity !== remoteIdentity) {
@@ -3670,16 +3979,16 @@ async function mergeRosterFromRemote(remote, paths) {
3670
3979
  }
3671
3980
  if (remote.agentConfig && Object.keys(remote.agentConfig).length > 0) {
3672
3981
  try {
3673
- const agentConfigPath = path9.join(EXE_AI_DIR, "agent-config.json");
3982
+ const agentConfigPath = path10.join(EXE_AI_DIR, "agent-config.json");
3674
3983
  let local = {};
3675
- if (existsSync9(agentConfigPath)) {
3984
+ if (existsSync10(agentConfigPath)) {
3676
3985
  try {
3677
3986
  local = JSON.parse(readFileSync7(agentConfigPath, "utf-8"));
3678
3987
  } catch {
3679
3988
  }
3680
3989
  }
3681
3990
  const merged = { ...remote.agentConfig, ...local };
3682
- ensurePrivateDirSync(path9.dirname(agentConfigPath));
3991
+ ensurePrivateDirSync(path10.dirname(agentConfigPath));
3683
3992
  writeFileSync5(agentConfigPath, JSON.stringify(merged, null, 2) + "\n", "utf-8");
3684
3993
  enforcePrivateFileSync(agentConfigPath);
3685
3994
  } catch {
@@ -555,6 +555,7 @@ import { createHash } from "crypto";
555
555
  // src/lib/keychain.ts
556
556
  import { readFile as readFile3, writeFile as writeFile3, unlink, mkdir as mkdir3, chmod as chmod2 } from "fs/promises";
557
557
  import { existsSync as existsSync5 } from "fs";
558
+ import { execSync as execSync2 } from "child_process";
558
559
  import path5 from "path";
559
560
  import os4 from "os";
560
561