@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
@@ -26,6 +26,7 @@ __export(keychain_exports, {
26
26
  });
27
27
  import { readFile, writeFile, unlink, mkdir, chmod } from "fs/promises";
28
28
  import { existsSync } from "fs";
29
+ import { execSync } from "child_process";
29
30
  import path from "path";
30
31
  import os from "os";
31
32
  function getKeyDir() {
@@ -34,6 +35,83 @@ function getKeyDir() {
34
35
  function getKeyPath() {
35
36
  return path.join(getKeyDir(), "master.key");
36
37
  }
38
+ function macKeychainGet() {
39
+ if (process.platform !== "darwin") return null;
40
+ try {
41
+ return execSync(
42
+ `security find-generic-password -s "${SERVICE}" -a "${ACCOUNT}" -w 2>/dev/null`,
43
+ { encoding: "utf-8", timeout: 5e3 }
44
+ ).trim();
45
+ } catch {
46
+ return null;
47
+ }
48
+ }
49
+ function macKeychainSet(value) {
50
+ if (process.platform !== "darwin") return false;
51
+ try {
52
+ try {
53
+ execSync(
54
+ `security delete-generic-password -s "${SERVICE}" -a "${ACCOUNT}" 2>/dev/null`,
55
+ { timeout: 5e3 }
56
+ );
57
+ } catch {
58
+ }
59
+ execSync(
60
+ `security add-generic-password -s "${SERVICE}" -a "${ACCOUNT}" -w "${value}"`,
61
+ { timeout: 5e3 }
62
+ );
63
+ return true;
64
+ } catch {
65
+ return false;
66
+ }
67
+ }
68
+ function macKeychainDelete() {
69
+ if (process.platform !== "darwin") return false;
70
+ try {
71
+ execSync(
72
+ `security delete-generic-password -s "${SERVICE}" -a "${ACCOUNT}" 2>/dev/null`,
73
+ { timeout: 5e3 }
74
+ );
75
+ return true;
76
+ } catch {
77
+ return false;
78
+ }
79
+ }
80
+ function linuxSecretGet() {
81
+ if (process.platform !== "linux") return null;
82
+ try {
83
+ return execSync(
84
+ `secret-tool lookup service "${SERVICE}" account "${ACCOUNT}" 2>/dev/null`,
85
+ { encoding: "utf-8", timeout: 5e3 }
86
+ ).trim();
87
+ } catch {
88
+ return null;
89
+ }
90
+ }
91
+ function linuxSecretSet(value) {
92
+ if (process.platform !== "linux") return false;
93
+ try {
94
+ execSync(
95
+ `echo -n "${value}" | secret-tool store --label="exe-os master key" service "${SERVICE}" account "${ACCOUNT}"`,
96
+ { timeout: 5e3 }
97
+ );
98
+ return true;
99
+ } catch {
100
+ return false;
101
+ }
102
+ }
103
+ function linuxSecretDelete() {
104
+ if (process.platform !== "linux") return false;
105
+ try {
106
+ execSync(
107
+ `secret-tool clear service "${SERVICE}" account "${ACCOUNT}" 2>/dev/null`,
108
+ { timeout: 5e3 }
109
+ );
110
+ return true;
111
+ } catch {
112
+ return false;
113
+ }
114
+ }
37
115
  async function tryKeytar() {
38
116
  try {
39
117
  return await import("keytar");
@@ -41,13 +119,72 @@ async function tryKeytar() {
41
119
  return null;
42
120
  }
43
121
  }
122
+ function deriveMachineKey() {
123
+ try {
124
+ const crypto4 = __require("crypto");
125
+ const material = [
126
+ os.hostname(),
127
+ os.userInfo().username,
128
+ os.arch(),
129
+ os.platform(),
130
+ // Machine ID on Linux (stable across reboots)
131
+ process.platform === "linux" ? readMachineId() : ""
132
+ ].join("|");
133
+ return crypto4.createHash("sha256").update(material).digest();
134
+ } catch {
135
+ return null;
136
+ }
137
+ }
138
+ function readMachineId() {
139
+ try {
140
+ const { readFileSync: readFileSync8 } = __require("fs");
141
+ return readFileSync8("/etc/machine-id", "utf-8").trim();
142
+ } catch {
143
+ return "";
144
+ }
145
+ }
146
+ function encryptWithMachineKey(plaintext, machineKey) {
147
+ const crypto4 = __require("crypto");
148
+ const iv = crypto4.randomBytes(12);
149
+ const cipher = crypto4.createCipheriv("aes-256-gcm", machineKey, iv);
150
+ let encrypted = cipher.update(plaintext, "utf-8", "base64");
151
+ encrypted += cipher.final("base64");
152
+ const authTag = cipher.getAuthTag().toString("base64");
153
+ return `${ENCRYPTED_PREFIX}${iv.toString("base64")}:${authTag}:${encrypted}`;
154
+ }
155
+ function decryptWithMachineKey(encrypted, machineKey) {
156
+ if (!encrypted.startsWith(ENCRYPTED_PREFIX)) return null;
157
+ try {
158
+ const crypto4 = __require("crypto");
159
+ const parts = encrypted.slice(ENCRYPTED_PREFIX.length).split(":");
160
+ if (parts.length !== 3) return null;
161
+ const [ivB64, tagB64, cipherB64] = parts;
162
+ const iv = Buffer.from(ivB64, "base64");
163
+ const authTag = Buffer.from(tagB64, "base64");
164
+ const decipher = crypto4.createDecipheriv("aes-256-gcm", machineKey, iv);
165
+ decipher.setAuthTag(authTag);
166
+ let decrypted = decipher.update(cipherB64, "base64", "utf-8");
167
+ decrypted += decipher.final("utf-8");
168
+ return decrypted;
169
+ } catch {
170
+ return null;
171
+ }
172
+ }
44
173
  async function getMasterKey() {
174
+ const nativeValue = macKeychainGet() ?? linuxSecretGet();
175
+ if (nativeValue) {
176
+ return Buffer.from(nativeValue, "base64");
177
+ }
45
178
  const keytar = await tryKeytar();
46
179
  if (keytar) {
47
180
  try {
48
- const stored = await keytar.getPassword(SERVICE, ACCOUNT);
49
- if (stored) {
50
- return Buffer.from(stored, "base64");
181
+ const keytarValue = await keytar.getPassword(SERVICE, ACCOUNT);
182
+ if (keytarValue) {
183
+ const migrated = macKeychainSet(keytarValue) || linuxSecretSet(keytarValue);
184
+ if (migrated) {
185
+ process.stderr.write("[keychain] Migrated key from keytar to native keychain.\n");
186
+ }
187
+ return Buffer.from(keytarValue, "base64");
51
188
  }
52
189
  } catch {
53
190
  }
@@ -61,8 +198,31 @@ async function getMasterKey() {
61
198
  return null;
62
199
  }
63
200
  try {
64
- const content = await readFile(keyPath, "utf-8");
65
- return Buffer.from(content.trim(), "base64");
201
+ const content = (await readFile(keyPath, "utf-8")).trim();
202
+ let b64Value;
203
+ if (content.startsWith(ENCRYPTED_PREFIX)) {
204
+ const machineKey = deriveMachineKey();
205
+ if (!machineKey) {
206
+ process.stderr.write("[keychain] Cannot derive machine key to decrypt stored key.\n");
207
+ return null;
208
+ }
209
+ const decrypted = decryptWithMachineKey(content, machineKey);
210
+ if (!decrypted) {
211
+ process.stderr.write(
212
+ "[keychain] Key decryption failed \u2014 machine may have changed.\n Use your 24-word recovery phrase: exe-os link import\n"
213
+ );
214
+ return null;
215
+ }
216
+ b64Value = decrypted;
217
+ } else {
218
+ b64Value = content;
219
+ }
220
+ const key = Buffer.from(b64Value, "base64");
221
+ const migrated = macKeychainSet(b64Value) || linuxSecretSet(b64Value);
222
+ if (migrated) {
223
+ process.stderr.write("[keychain] Migrated key from file to native keychain.\n");
224
+ }
225
+ return key;
66
226
  } catch (err) {
67
227
  process.stderr.write(
68
228
  `[keychain] Key read failed at ${keyPath}: ${err instanceof Error ? err.message : String(err)}
@@ -73,6 +233,9 @@ async function getMasterKey() {
73
233
  }
74
234
  async function setMasterKey(key) {
75
235
  const b64 = key.toString("base64");
236
+ if (macKeychainSet(b64) || linuxSecretSet(b64)) {
237
+ return;
238
+ }
76
239
  const keytar = await tryKeytar();
77
240
  if (keytar) {
78
241
  try {
@@ -84,10 +247,23 @@ async function setMasterKey(key) {
84
247
  const dir = getKeyDir();
85
248
  await mkdir(dir, { recursive: true });
86
249
  const keyPath = getKeyPath();
87
- await writeFile(keyPath, b64 + "\n", "utf-8");
88
- await chmod(keyPath, 384);
250
+ const machineKey = deriveMachineKey();
251
+ if (machineKey) {
252
+ const encrypted = encryptWithMachineKey(b64, machineKey);
253
+ await writeFile(keyPath, encrypted + "\n", "utf-8");
254
+ await chmod(keyPath, 384);
255
+ process.stderr.write("[keychain] Key stored encrypted (machine-bound).\n");
256
+ } else {
257
+ await writeFile(keyPath, b64 + "\n", "utf-8");
258
+ await chmod(keyPath, 384);
259
+ process.stderr.write(
260
+ "[keychain] WARNING: Key stored in plaintext file \u2014 no OS keychain available.\n"
261
+ );
262
+ }
89
263
  }
90
264
  async function deleteMasterKey() {
265
+ macKeychainDelete();
266
+ linuxSecretDelete();
91
267
  const keytar = await tryKeytar();
92
268
  if (keytar) {
93
269
  try {
@@ -129,12 +305,13 @@ async function importMnemonic(mnemonic) {
129
305
  const entropy = mnemonicToEntropy(trimmed);
130
306
  return Buffer.from(entropy, "hex");
131
307
  }
132
- var SERVICE, ACCOUNT;
308
+ var SERVICE, ACCOUNT, ENCRYPTED_PREFIX;
133
309
  var init_keychain = __esm({
134
310
  "src/lib/keychain.ts"() {
135
311
  "use strict";
136
312
  SERVICE = "exe-mem";
137
313
  ACCOUNT = "master-key";
314
+ ENCRYPTED_PREFIX = "enc:";
138
315
  }
139
316
  });
140
317
 
@@ -533,7 +710,7 @@ var init_db_retry = __esm({
533
710
  // src/lib/employees.ts
534
711
  import { readFile as readFile3, writeFile as writeFile3, mkdir as mkdir3 } from "fs/promises";
535
712
  import { existsSync as existsSync4, symlinkSync, readlinkSync, readFileSync as readFileSync2, renameSync as renameSync2, unlinkSync, writeFileSync } from "fs";
536
- import { execSync } from "child_process";
713
+ import { execSync as execSync2 } from "child_process";
537
714
  import path3 from "path";
538
715
  import os3 from "os";
539
716
  function normalizeRole(role) {
@@ -573,7 +750,7 @@ function loadEmployeesSync(employeesPath = EMPLOYEES_PATH) {
573
750
  }
574
751
  function findExeBin() {
575
752
  try {
576
- return execSync(process.platform === "win32" ? "where exe-os" : "which exe-os", { encoding: "utf8" }).trim();
753
+ return execSync2(process.platform === "win32" ? "where exe-os" : "which exe-os", { encoding: "utf8" }).trim();
577
754
  } catch {
578
755
  return null;
579
756
  }
@@ -1321,8 +1498,8 @@ function findPackageRoot() {
1321
1498
  function getAvailableMemoryGB() {
1322
1499
  if (process.platform === "darwin") {
1323
1500
  try {
1324
- const { execSync: execSync2 } = __require("child_process");
1325
- const vmstat = execSync2("vm_stat", { encoding: "utf8" });
1501
+ const { execSync: execSync3 } = __require("child_process");
1502
+ const vmstat = execSync3("vm_stat", { encoding: "utf8" });
1326
1503
  const pageSize = 16384;
1327
1504
  const pageSizeMatch = vmstat.match(/page size of (\d+) bytes/);
1328
1505
  const actualPageSize = pageSizeMatch ? parseInt(pageSizeMatch[1], 10) : pageSize;
@@ -3140,6 +3317,109 @@ var init_crdt_sync = __esm({
3140
3317
  }
3141
3318
  });
3142
3319
 
3320
+ // src/lib/db-backup.ts
3321
+ var db_backup_exports = {};
3322
+ __export(db_backup_exports, {
3323
+ createBackup: () => createBackup,
3324
+ findActiveDb: () => findActiveDb,
3325
+ getBackupDir: () => getBackupDir,
3326
+ getLatestBackup: () => getLatestBackup,
3327
+ hasBackupToday: () => hasBackupToday,
3328
+ listBackups: () => listBackups,
3329
+ rotateBackups: () => rotateBackups
3330
+ });
3331
+ import { copyFileSync, existsSync as existsSync9, mkdirSync as mkdirSync4, readdirSync, unlinkSync as unlinkSync4, statSync as statSync2 } from "fs";
3332
+ import path9 from "path";
3333
+ function findActiveDb() {
3334
+ for (const name of DB_NAMES) {
3335
+ const p = path9.join(EXE_AI_DIR, name);
3336
+ if (existsSync9(p)) return p;
3337
+ }
3338
+ return null;
3339
+ }
3340
+ function createBackup(reason = "manual") {
3341
+ const dbPath = findActiveDb();
3342
+ if (!dbPath) return null;
3343
+ mkdirSync4(BACKUP_DIR, { recursive: true });
3344
+ const dbName = path9.basename(dbPath, ".db");
3345
+ const timestamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-").slice(0, 19);
3346
+ const backupName = `${dbName}-${reason}-${timestamp}.db`;
3347
+ const backupPath = path9.join(BACKUP_DIR, backupName);
3348
+ copyFileSync(dbPath, backupPath);
3349
+ const walPath = dbPath + "-wal";
3350
+ if (existsSync9(walPath)) {
3351
+ try {
3352
+ copyFileSync(walPath, backupPath + "-wal");
3353
+ } catch {
3354
+ }
3355
+ }
3356
+ const shmPath = dbPath + "-shm";
3357
+ if (existsSync9(shmPath)) {
3358
+ try {
3359
+ copyFileSync(shmPath, backupPath + "-shm");
3360
+ } catch {
3361
+ }
3362
+ }
3363
+ return backupPath;
3364
+ }
3365
+ function rotateBackups(keepDays = DEFAULT_KEEP_DAYS) {
3366
+ if (!existsSync9(BACKUP_DIR)) return 0;
3367
+ const cutoff = Date.now() - keepDays * 24 * 60 * 60 * 1e3;
3368
+ let deleted = 0;
3369
+ try {
3370
+ const files = readdirSync(BACKUP_DIR);
3371
+ for (const file of files) {
3372
+ if (!file.endsWith(".db") && !file.endsWith(".db-wal") && !file.endsWith(".db-shm")) continue;
3373
+ const filePath = path9.join(BACKUP_DIR, file);
3374
+ try {
3375
+ const stat = statSync2(filePath);
3376
+ if (stat.mtimeMs < cutoff) {
3377
+ unlinkSync4(filePath);
3378
+ deleted++;
3379
+ }
3380
+ } catch {
3381
+ }
3382
+ }
3383
+ } catch {
3384
+ }
3385
+ return deleted;
3386
+ }
3387
+ function listBackups() {
3388
+ if (!existsSync9(BACKUP_DIR)) return [];
3389
+ try {
3390
+ const files = readdirSync(BACKUP_DIR).filter((f) => f.endsWith(".db") && !f.endsWith("-wal") && !f.endsWith("-shm"));
3391
+ return files.map((name) => {
3392
+ const p = path9.join(BACKUP_DIR, name);
3393
+ const stat = statSync2(p);
3394
+ return { path: p, name, size: stat.size, date: stat.mtime };
3395
+ }).sort((a, b) => b.date.getTime() - a.date.getTime());
3396
+ } catch {
3397
+ return [];
3398
+ }
3399
+ }
3400
+ function hasBackupToday(reason) {
3401
+ const today = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
3402
+ const backups = listBackups();
3403
+ return backups.some((b) => b.name.includes(reason) && b.name.includes(today.replace(/-/g, "-")));
3404
+ }
3405
+ function getLatestBackup() {
3406
+ const backups = listBackups();
3407
+ return backups.length > 0 ? backups[0].path : null;
3408
+ }
3409
+ function getBackupDir() {
3410
+ return BACKUP_DIR;
3411
+ }
3412
+ var BACKUP_DIR, DEFAULT_KEEP_DAYS, DB_NAMES;
3413
+ var init_db_backup = __esm({
3414
+ "src/lib/db-backup.ts"() {
3415
+ "use strict";
3416
+ init_config();
3417
+ BACKUP_DIR = path9.join(EXE_AI_DIR, "backups");
3418
+ DEFAULT_KEEP_DAYS = 3;
3419
+ DB_NAMES = ["memories.db", "exe-mem.db", "exe-os.db", "exe.db"];
3420
+ }
3421
+ });
3422
+
3143
3423
  // src/lib/cloud-sync.ts
3144
3424
  var cloud_sync_exports = {};
3145
3425
  __export(cloud_sync_exports, {
@@ -3169,16 +3449,16 @@ __export(cloud_sync_exports, {
3169
3449
  pushToPostgres: () => pushToPostgres,
3170
3450
  recordRosterDeletion: () => recordRosterDeletion
3171
3451
  });
3172
- 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";
3452
+ 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";
3173
3453
  import crypto3 from "crypto";
3174
- import path9 from "path";
3454
+ import path10 from "path";
3175
3455
  import { homedir as homedir2 } from "os";
3176
3456
  function sqlSafe(v) {
3177
3457
  return v === void 0 ? null : v;
3178
3458
  }
3179
3459
  function logError(msg) {
3180
3460
  try {
3181
- const logPath = path9.join(homedir2(), ".exe-os", "workers.log");
3461
+ const logPath = path10.join(homedir2(), ".exe-os", "workers.log");
3182
3462
  appendFileSync(logPath, `${(/* @__PURE__ */ new Date()).toISOString()} ${msg}
3183
3463
  `);
3184
3464
  } catch {
@@ -3187,10 +3467,10 @@ function logError(msg) {
3187
3467
  function loadPgClient() {
3188
3468
  if (_pgFailed) return null;
3189
3469
  const postgresUrl = process.env.DATABASE_URL;
3190
- const configPath = path9.join(EXE_AI_DIR, "config.json");
3470
+ const configPath = path10.join(EXE_AI_DIR, "config.json");
3191
3471
  let cloudPostgresUrl;
3192
3472
  try {
3193
- if (existsSync9(configPath)) {
3473
+ if (existsSync10(configPath)) {
3194
3474
  const cfg = JSON.parse(readFileSync7(configPath, "utf8"));
3195
3475
  cloudPostgresUrl = cfg.cloud?.postgresUrl;
3196
3476
  if (cfg.cloud?.syncToPostgres === false) {
@@ -3209,8 +3489,8 @@ function loadPgClient() {
3209
3489
  _pgPromise = (async () => {
3210
3490
  const { createRequire: createRequire3 } = await import("module");
3211
3491
  const { pathToFileURL: pathToFileURL3 } = await import("url");
3212
- const exeDbRoot = process.env.EXE_DB_ROOT ?? path9.join(homedir2(), "exe-db");
3213
- const req = createRequire3(path9.join(exeDbRoot, "package.json"));
3492
+ const exeDbRoot = process.env.EXE_DB_ROOT ?? path10.join(homedir2(), "exe-db");
3493
+ const req = createRequire3(path10.join(exeDbRoot, "package.json"));
3214
3494
  const entry = req.resolve("@prisma/client");
3215
3495
  const mod = await import(pathToFileURL3(entry).href);
3216
3496
  const Ctor = mod.PrismaClient ?? mod.default?.PrismaClient;
@@ -3263,7 +3543,7 @@ async function withRosterLock(fn) {
3263
3543
  if (Date.now() - ts < LOCK_STALE_MS) {
3264
3544
  throw new Error("Roster merge already in progress \u2014 another sync is running");
3265
3545
  }
3266
- unlinkSync4(ROSTER_LOCK_PATH);
3546
+ unlinkSync5(ROSTER_LOCK_PATH);
3267
3547
  const fd = openSync2(ROSTER_LOCK_PATH, "wx");
3268
3548
  closeSync2(fd);
3269
3549
  writeFileSync5(ROSTER_LOCK_PATH, String(Date.now()));
@@ -3279,7 +3559,7 @@ async function withRosterLock(fn) {
3279
3559
  return await fn();
3280
3560
  } finally {
3281
3561
  try {
3282
- unlinkSync4(ROSTER_LOCK_PATH);
3562
+ unlinkSync5(ROSTER_LOCK_PATH);
3283
3563
  } catch {
3284
3564
  }
3285
3565
  }
@@ -3650,13 +3930,42 @@ async function cloudSync(config) {
3650
3930
  try {
3651
3931
  const employees = await loadEmployees();
3652
3932
  rosterResult.employees = employees.length;
3653
- const idDir = path9.join(EXE_AI_DIR, "identity");
3654
- if (existsSync9(idDir)) {
3655
- rosterResult.identities = readdirSync(idDir).filter((f) => f.endsWith(".md")).length;
3933
+ const idDir = path10.join(EXE_AI_DIR, "identity");
3934
+ if (existsSync10(idDir)) {
3935
+ rosterResult.identities = readdirSync2(idDir).filter((f) => f.endsWith(".md")).length;
3656
3936
  }
3657
3937
  } catch {
3658
3938
  }
3659
3939
  const totalMemories = await countRows("SELECT COUNT(*) as cnt FROM memories WHERE status = 'active' OR status IS NULL");
3940
+ try {
3941
+ const { getLatestBackup: getLatestBackup2 } = await Promise.resolve().then(() => (init_db_backup(), db_backup_exports));
3942
+ const { statSync: statFile } = await import("fs");
3943
+ const latestBackup = getLatestBackup2();
3944
+ if (latestBackup) {
3945
+ const backupSize = statFile(latestBackup).size;
3946
+ const MAX_CLOUD_BACKUP_BYTES = 50 * 1024 * 1024;
3947
+ if (backupSize <= MAX_CLOUD_BACKUP_BYTES) {
3948
+ const backupData = readFileSync7(latestBackup);
3949
+ const deviceId = loadDeviceId() ?? "unknown";
3950
+ const encrypted = encryptSyncBlob(backupData);
3951
+ const backupRes = await fetchWithRetry(`${config.endpoint}/sync/push-db-backup`, {
3952
+ method: "POST",
3953
+ headers: { "Content-Type": "application/json", Authorization: `Bearer ${config.apiKey}` },
3954
+ body: JSON.stringify({
3955
+ device_id: deviceId,
3956
+ filename: path10.basename(latestBackup),
3957
+ blob: encrypted,
3958
+ size: backupData.length
3959
+ })
3960
+ });
3961
+ if (backupRes && !backupRes.ok) {
3962
+ logError(`[cloud-sync] DB backup upload failed: ${backupRes.status}`);
3963
+ }
3964
+ }
3965
+ }
3966
+ } catch (err) {
3967
+ logError(`[cloud-sync] DB backup upload error: ${err instanceof Error ? err.message : String(err)}`);
3968
+ }
3660
3969
  return {
3661
3970
  pushed,
3662
3971
  pulled,
@@ -3672,7 +3981,7 @@ async function cloudSync(config) {
3672
3981
  function recordRosterDeletion(name) {
3673
3982
  let deletions = [];
3674
3983
  try {
3675
- if (existsSync9(ROSTER_DELETIONS_PATH)) {
3984
+ if (existsSync10(ROSTER_DELETIONS_PATH)) {
3676
3985
  deletions = JSON.parse(readFileSync7(ROSTER_DELETIONS_PATH, "utf-8"));
3677
3986
  }
3678
3987
  } catch {
@@ -3682,7 +3991,7 @@ function recordRosterDeletion(name) {
3682
3991
  }
3683
3992
  function consumeRosterDeletions() {
3684
3993
  try {
3685
- if (!existsSync9(ROSTER_DELETIONS_PATH)) return [];
3994
+ if (!existsSync10(ROSTER_DELETIONS_PATH)) return [];
3686
3995
  const deletions = JSON.parse(readFileSync7(ROSTER_DELETIONS_PATH, "utf-8"));
3687
3996
  writeFileSync5(ROSTER_DELETIONS_PATH, "[]");
3688
3997
  return deletions;
@@ -3691,35 +4000,35 @@ function consumeRosterDeletions() {
3691
4000
  }
3692
4001
  }
3693
4002
  function buildRosterBlob(paths) {
3694
- const rosterPath = paths?.rosterPath ?? path9.join(EXE_AI_DIR, "exe-employees.json");
3695
- const identityDir = paths?.identityDir ?? path9.join(EXE_AI_DIR, "identity");
3696
- const configPath = paths?.configPath ?? path9.join(EXE_AI_DIR, "config.json");
4003
+ const rosterPath = paths?.rosterPath ?? path10.join(EXE_AI_DIR, "exe-employees.json");
4004
+ const identityDir = paths?.identityDir ?? path10.join(EXE_AI_DIR, "identity");
4005
+ const configPath = paths?.configPath ?? path10.join(EXE_AI_DIR, "config.json");
3697
4006
  let roster = [];
3698
- if (existsSync9(rosterPath)) {
4007
+ if (existsSync10(rosterPath)) {
3699
4008
  try {
3700
4009
  roster = JSON.parse(readFileSync7(rosterPath, "utf-8"));
3701
4010
  } catch {
3702
4011
  }
3703
4012
  }
3704
4013
  const identities = {};
3705
- if (existsSync9(identityDir)) {
3706
- for (const file of readdirSync(identityDir).filter((f) => f.endsWith(".md"))) {
4014
+ if (existsSync10(identityDir)) {
4015
+ for (const file of readdirSync2(identityDir).filter((f) => f.endsWith(".md"))) {
3707
4016
  try {
3708
- identities[file] = readFileSync7(path9.join(identityDir, file), "utf-8");
4017
+ identities[file] = readFileSync7(path10.join(identityDir, file), "utf-8");
3709
4018
  } catch {
3710
4019
  }
3711
4020
  }
3712
4021
  }
3713
4022
  let config;
3714
- if (existsSync9(configPath)) {
4023
+ if (existsSync10(configPath)) {
3715
4024
  try {
3716
4025
  config = JSON.parse(readFileSync7(configPath, "utf-8"));
3717
4026
  } catch {
3718
4027
  }
3719
4028
  }
3720
4029
  let agentConfig;
3721
- const agentConfigPath = path9.join(EXE_AI_DIR, "agent-config.json");
3722
- if (existsSync9(agentConfigPath)) {
4030
+ const agentConfigPath = path10.join(EXE_AI_DIR, "agent-config.json");
4031
+ if (existsSync10(agentConfigPath)) {
3723
4032
  try {
3724
4033
  agentConfig = JSON.parse(readFileSync7(agentConfigPath, "utf-8"));
3725
4034
  } catch {
@@ -3797,16 +4106,16 @@ async function cloudPullRoster(config) {
3797
4106
  }
3798
4107
  }
3799
4108
  function mergeConfig(remoteConfig, configPath) {
3800
- const cfgPath = configPath ?? path9.join(EXE_AI_DIR, "config.json");
4109
+ const cfgPath = configPath ?? path10.join(EXE_AI_DIR, "config.json");
3801
4110
  let local = {};
3802
- if (existsSync9(cfgPath)) {
4111
+ if (existsSync10(cfgPath)) {
3803
4112
  try {
3804
4113
  local = JSON.parse(readFileSync7(cfgPath, "utf-8"));
3805
4114
  } catch {
3806
4115
  }
3807
4116
  }
3808
4117
  const merged = { ...remoteConfig, ...local };
3809
- const dir = path9.dirname(cfgPath);
4118
+ const dir = path10.dirname(cfgPath);
3810
4119
  ensurePrivateDirSync(dir);
3811
4120
  writeFileSync5(cfgPath, JSON.stringify(merged, null, 2), "utf-8");
3812
4121
  enforcePrivateFileSync(cfgPath);
@@ -3814,7 +4123,7 @@ function mergeConfig(remoteConfig, configPath) {
3814
4123
  async function mergeRosterFromRemote(remote, paths) {
3815
4124
  return withRosterLock(async () => {
3816
4125
  const rosterPath = paths?.rosterPath ?? void 0;
3817
- const identityDir = paths?.identityDir ?? path9.join(EXE_AI_DIR, "identity");
4126
+ const identityDir = paths?.identityDir ?? path10.join(EXE_AI_DIR, "identity");
3818
4127
  const localEmployees = await loadEmployees(rosterPath);
3819
4128
  const localNames = new Set(localEmployees.map((e) => e.name));
3820
4129
  let added = 0;
@@ -3835,11 +4144,11 @@ async function mergeRosterFromRemote(remote, paths) {
3835
4144
  ) ?? lookupKey;
3836
4145
  const remoteIdentity = remote.identities[matchedKey];
3837
4146
  if (remoteIdentity) {
3838
- if (!existsSync9(identityDir)) mkdirSync4(identityDir, { recursive: true });
3839
- const idPath = path9.join(identityDir, `${remoteEmp.name}.md`);
4147
+ if (!existsSync10(identityDir)) mkdirSync5(identityDir, { recursive: true });
4148
+ const idPath = path10.join(identityDir, `${remoteEmp.name}.md`);
3840
4149
  let localIdentity = null;
3841
4150
  try {
3842
- localIdentity = existsSync9(idPath) ? readFileSync7(idPath, "utf-8") : null;
4151
+ localIdentity = existsSync10(idPath) ? readFileSync7(idPath, "utf-8") : null;
3843
4152
  } catch {
3844
4153
  }
3845
4154
  if (localIdentity !== remoteIdentity) {
@@ -3869,16 +4178,16 @@ async function mergeRosterFromRemote(remote, paths) {
3869
4178
  }
3870
4179
  if (remote.agentConfig && Object.keys(remote.agentConfig).length > 0) {
3871
4180
  try {
3872
- const agentConfigPath = path9.join(EXE_AI_DIR, "agent-config.json");
4181
+ const agentConfigPath = path10.join(EXE_AI_DIR, "agent-config.json");
3873
4182
  let local = {};
3874
- if (existsSync9(agentConfigPath)) {
4183
+ if (existsSync10(agentConfigPath)) {
3875
4184
  try {
3876
4185
  local = JSON.parse(readFileSync7(agentConfigPath, "utf-8"));
3877
4186
  } catch {
3878
4187
  }
3879
4188
  }
3880
4189
  const merged = { ...remote.agentConfig, ...local };
3881
- ensurePrivateDirSync(path9.dirname(agentConfigPath));
4190
+ ensurePrivateDirSync(path10.dirname(agentConfigPath));
3882
4191
  writeFileSync5(agentConfigPath, JSON.stringify(merged, null, 2) + "\n", "utf-8");
3883
4192
  enforcePrivateFileSync(agentConfigPath);
3884
4193
  } catch {
@@ -4319,11 +4628,11 @@ var init_cloud_sync = __esm({
4319
4628
  LOCALHOST_PATTERNS = /^(localhost|127\.0\.0\.1|\[::1\])$/i;
4320
4629
  FETCH_TIMEOUT_MS = 3e4;
4321
4630
  PUSH_BATCH_SIZE = 5e3;
4322
- ROSTER_LOCK_PATH = path9.join(EXE_AI_DIR, "roster-merge.lock");
4631
+ ROSTER_LOCK_PATH = path10.join(EXE_AI_DIR, "roster-merge.lock");
4323
4632
  LOCK_STALE_MS = 3e4;
4324
4633
  _pgPromise = null;
4325
4634
  _pgFailed = false;
4326
- ROSTER_DELETIONS_PATH = path9.join(EXE_AI_DIR, "roster-deletions.json");
4635
+ ROSTER_DELETIONS_PATH = path10.join(EXE_AI_DIR, "roster-deletions.json");
4327
4636
  }
4328
4637
  });
4329
4638