unforgit 0.5.5 → 0.6.0

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.
package/dist/index.js CHANGED
@@ -808,11 +808,130 @@ Examples:
808
808
  });
809
809
 
810
810
  // src/commands/delete.ts
811
+ import { Command as Command9 } from "commander";
812
+
813
+ // src/commands/backups.ts
814
+ import fs3 from "fs";
815
+ import path2 from "path";
811
816
  import { Command as Command8 } from "commander";
812
- var deleteCommand = new Command8("delete").description("Soft delete a memory (can be restored)").argument("<id>", "Memory ID to delete").option("--hard", "Permanently delete (cannot be restored)").option("--remote", "Delete on remote").option("--by <author>", "Author of the deletion").option("--force", "Skip confirmation for hard delete").addHelpText("after", `
817
+ function formatBackupTimestamp(date) {
818
+ return date.toISOString().replace(/[-:]/g, "").replace("T", "-").replace(/\.\d{3}Z$/, "");
819
+ }
820
+ function backupRootForDb(dbPath) {
821
+ return path2.join(path2.dirname(dbPath), "backups");
822
+ }
823
+ function describeBackup(dir) {
824
+ const files = fs3.readdirSync(dir).filter((file) => file === "local.db" || file === "local.db-wal" || file === "local.db-shm").sort();
825
+ const sizeBytes = files.reduce((total, file) => total + fs3.statSync(path2.join(dir, file)).size, 0);
826
+ const stat = fs3.statSync(dir);
827
+ return {
828
+ name: path2.basename(dir),
829
+ dir,
830
+ files,
831
+ sizeBytes,
832
+ createdAt: stat.mtime.toISOString()
833
+ };
834
+ }
835
+ function resolveBackupDir(backupRoot, backupName) {
836
+ if (backupName !== path2.basename(backupName)) {
837
+ throw new Error("Invalid backup name");
838
+ }
839
+ const resolvedRoot = path2.resolve(backupRoot);
840
+ const resolvedBackup = path2.resolve(resolvedRoot, backupName);
841
+ if (!resolvedBackup.startsWith(`${resolvedRoot}${path2.sep}`)) {
842
+ throw new Error("Invalid backup name");
843
+ }
844
+ return resolvedBackup;
845
+ }
846
+ function createLocalDatabaseBackup(dbPath, prefix, now = /* @__PURE__ */ new Date()) {
847
+ if (!fs3.existsSync(dbPath)) {
848
+ return null;
849
+ }
850
+ const backupRoot = backupRootForDb(dbPath);
851
+ const baseName = `${prefix}-${formatBackupTimestamp(now)}`;
852
+ let backupDir = path2.join(backupRoot, baseName);
853
+ let suffix = 1;
854
+ while (fs3.existsSync(backupDir)) {
855
+ backupDir = path2.join(backupRoot, `${baseName}-${suffix++}`);
856
+ }
857
+ fs3.mkdirSync(backupDir, { recursive: true, mode: 448 });
858
+ for (const source of [dbPath, `${dbPath}-wal`, `${dbPath}-shm`]) {
859
+ if (!fs3.existsSync(source)) continue;
860
+ fs3.copyFileSync(source, path2.join(backupDir, path2.basename(source)));
861
+ }
862
+ return describeBackup(backupDir);
863
+ }
864
+ function createLocalResetBackup(dbPath, now = /* @__PURE__ */ new Date()) {
865
+ return createLocalDatabaseBackup(dbPath, "reset", now);
866
+ }
867
+ function listLocalResetBackups(dbPath) {
868
+ const backupRoot = backupRootForDb(dbPath);
869
+ if (!fs3.existsSync(backupRoot)) {
870
+ return [];
871
+ }
872
+ return fs3.readdirSync(backupRoot, { withFileTypes: true }).filter((entry) => entry.isDirectory() && entry.name.startsWith("reset-")).map((entry) => describeBackup(path2.join(backupRoot, entry.name))).filter((backup) => backup.files.includes("local.db")).sort((a, b) => b.name.localeCompare(a.name));
873
+ }
874
+ function restoreLocalResetBackup(dbPath, backupName, now = /* @__PURE__ */ new Date()) {
875
+ const backupRoot = backupRootForDb(dbPath);
876
+ const backupDir = resolveBackupDir(backupRoot, backupName);
877
+ if (!fs3.existsSync(backupDir) || !fs3.statSync(backupDir).isDirectory()) {
878
+ throw new Error(`Backup not found: ${backupName}`);
879
+ }
880
+ const restoredFrom = describeBackup(backupDir);
881
+ if (!restoredFrom.files.includes("local.db")) {
882
+ throw new Error(`Backup is missing local.db: ${backupName}`);
883
+ }
884
+ const safetyBackup = createLocalResetBackup(dbPath, now);
885
+ fs3.mkdirSync(path2.dirname(dbPath), { recursive: true });
886
+ for (const target of [dbPath, `${dbPath}-wal`, `${dbPath}-shm`]) {
887
+ if (fs3.existsSync(target)) {
888
+ fs3.rmSync(target, { force: true });
889
+ }
890
+ }
891
+ for (const file of restoredFrom.files) {
892
+ fs3.copyFileSync(path2.join(backupDir, file), path2.join(path2.dirname(dbPath), file));
893
+ }
894
+ return { restoredFrom, safetyBackup };
895
+ }
896
+ var backupsCommand = new Command8("backups").description("List and restore local reset backups");
897
+ backupsCommand.command("list").description("List local backups created before destructive resets/restores").action(() => {
898
+ const backups = listLocalResetBackups(getDbPath());
899
+ if (backups.length === 0) {
900
+ logger.info("No local reset backups found.");
901
+ return;
902
+ }
903
+ for (const backup of backups) {
904
+ logger.info(`${backup.name} ${backup.sizeBytes} bytes ${backup.dir}`);
905
+ }
906
+ });
907
+ backupsCommand.command("restore").argument("<name>", "Backup directory name, for example reset-20260610-123456").description("Restore a local reset backup into the active local database").option("--force", "Skip confirmation prompt").action(async (name, opts) => {
908
+ if (!opts.force) {
909
+ const ok = await confirm(
910
+ "This will replace the active local database after creating a safety backup. Continue?"
911
+ );
912
+ if (!ok) {
913
+ logger.info("Aborted.");
914
+ return;
915
+ }
916
+ }
917
+ try {
918
+ const result = restoreLocalResetBackup(getDbPath(), name);
919
+ logger.info(`Restored local database from ${result.restoredFrom.name}`);
920
+ if (result.safetyBackup) {
921
+ logger.info(`Previous local database safety backup: ${result.safetyBackup.dir}`);
922
+ }
923
+ } catch (err) {
924
+ logger.error(err instanceof Error ? err.message : String(err));
925
+ process.exit(EXIT_ERROR);
926
+ }
927
+ });
928
+
929
+ // src/commands/delete.ts
930
+ var deleteCommand = new Command9("delete").description("Soft delete a memory (can be restored)").argument("<id>", "Memory ID to delete").option("--hard", "Permanently delete (cannot be restored)").option("--remote", "Delete on remote").option("--by <author>", "Author of the deletion").option("--force", "Skip confirmation for hard delete").option("--no-backup", "Skip automatic local database backup before local hard delete").addHelpText("after", `
813
931
  Examples:
814
932
  unforgit delete abc12345 Soft delete (can be restored)
815
- unforgit delete abc12345 --hard Permanent delete
933
+ unforgit delete abc12345 --hard Permanent delete, with local backup by default
934
+ unforgit delete abc12345 --hard --no-backup
816
935
  unforgit delete abc12345 --remote Delete on remote server`).action(async (id, opts) => {
817
936
  if (opts.hard && !opts.force) {
818
937
  const confirmed = await confirm(
@@ -838,7 +957,21 @@ Examples:
838
957
  }
839
958
  return;
840
959
  }
841
- const store = new LocalStore(getDbPath());
960
+ const dbPath = getDbPath();
961
+ if (opts.hard && opts.backup !== false) {
962
+ try {
963
+ const backup = createLocalDatabaseBackup(dbPath, "hard-delete");
964
+ if (backup) {
965
+ logger.info(`Created local hard-delete backup: ${backup.dir}`);
966
+ }
967
+ } catch (err) {
968
+ logger.error(
969
+ `Failed to create local hard-delete backup: ${err instanceof Error ? err.message : String(err)}`
970
+ );
971
+ process.exit(EXIT_ERROR);
972
+ }
973
+ }
974
+ const store = new LocalStore(dbPath);
842
975
  try {
843
976
  let ok;
844
977
  if (opts.hard) {
@@ -859,7 +992,7 @@ Examples:
859
992
  store.close();
860
993
  }
861
994
  });
862
- var restoreCommand = new Command8("restore").description("Restore a soft-deleted memory").argument("<id>", "Memory ID to restore").option("--remote", "Restore on remote").action(async (id, opts) => {
995
+ var restoreCommand = new Command9("restore").description("Restore a soft-deleted memory").argument("<id>", "Memory ID to restore").option("--remote", "Restore on remote").action(async (id, opts) => {
863
996
  if (opts.remote) {
864
997
  const config = loadConfig();
865
998
  const client = new RemoteClient(config.remote.url);
@@ -888,11 +1021,11 @@ var restoreCommand = new Command8("restore").description("Restore a soft-deleted
888
1021
  });
889
1022
 
890
1023
  // src/commands/web.ts
891
- import { Command as Command9 } from "commander";
1024
+ import { Command as Command10 } from "commander";
892
1025
  import { spawn } from "child_process";
893
- import path2 from "path";
894
- import fs3 from "fs";
895
- var webCommand = new Command9("web").description("Start the Unforgit web dashboard").option("-p, --port <port>", "Port to run on", "3838").option("--no-open", "Don't open browser automatically").action(async (opts) => {
1026
+ import path3 from "path";
1027
+ import fs4 from "fs";
1028
+ var webCommand = new Command10("web").description("Start the Unforgit web dashboard").option("-p, --port <port>", "Port to run on", "3838").option("--no-open", "Don't open browser automatically").action(async (opts) => {
896
1029
  const cwd4 = process.cwd();
897
1030
  if (!isInitialized(cwd4)) {
898
1031
  logger.error("Unforgit not initialized in this directory. Run 'unforgit init' first.");
@@ -908,9 +1041,9 @@ var webCommand = new Command9("web").description("Start the Unforgit web dashboa
908
1041
  UNFORGIT_WORKSPACE: cwd4,
909
1042
  PORT: opts.port
910
1043
  };
911
- const dotenvPath = path2.join(cwd4, ".env");
912
- if (fs3.existsSync(dotenvPath)) {
913
- const content = fs3.readFileSync(dotenvPath, "utf-8");
1044
+ const dotenvPath = path3.join(cwd4, ".env");
1045
+ if (fs4.existsSync(dotenvPath)) {
1046
+ const content = fs4.readFileSync(dotenvPath, "utf-8");
914
1047
  for (const line of content.split("\n")) {
915
1048
  const match = line.match(/^\s*([^#=]+?)\s*=\s*(.+?)\s*$/);
916
1049
  if (match) {
@@ -920,11 +1053,11 @@ var webCommand = new Command9("web").description("Start the Unforgit web dashboa
920
1053
  }
921
1054
  logger.info(`Starting Unforgit web dashboard on port ${opts.port}...`);
922
1055
  logger.info(`Workspace: ${cwd4}`);
923
- const hasNextBuild = fs3.existsSync(path2.join(webDir, ".next"));
1056
+ const hasNextBuild = fs4.existsSync(path3.join(webDir, ".next"));
924
1057
  const cmd = hasNextBuild ? "next" : "next";
925
1058
  const args = hasNextBuild ? ["start", "-p", opts.port] : ["dev", "-p", opts.port];
926
- const nextBin = path2.join(webDir, "node_modules", ".bin", "next");
927
- const finalCmd = fs3.existsSync(nextBin) ? nextBin : cmd;
1059
+ const nextBin = path3.join(webDir, "node_modules", ".bin", "next");
1060
+ const finalCmd = fs4.existsSync(nextBin) ? nextBin : cmd;
928
1061
  const child = spawn(finalCmd, args, {
929
1062
  cwd: webDir,
930
1063
  env,
@@ -947,12 +1080,12 @@ var webCommand = new Command9("web").description("Start the Unforgit web dashboa
947
1080
  });
948
1081
  function findWebDir() {
949
1082
  const candidates = [
950
- path2.resolve(import.meta.dirname, "../../../web"),
951
- path2.resolve(import.meta.dirname, "../../web"),
952
- path2.join(process.cwd(), "web")
1083
+ path3.resolve(import.meta.dirname, "../../../web"),
1084
+ path3.resolve(import.meta.dirname, "../../web"),
1085
+ path3.join(process.cwd(), "web")
953
1086
  ];
954
1087
  for (const dir of candidates) {
955
- if (fs3.existsSync(dir) && fs3.existsSync(path2.join(dir, "package.json"))) {
1088
+ if (fs4.existsSync(dir) && fs4.existsSync(path3.join(dir, "package.json"))) {
956
1089
  return dir;
957
1090
  }
958
1091
  }
@@ -960,14 +1093,14 @@ function findWebDir() {
960
1093
  }
961
1094
 
962
1095
  // src/commands/link.ts
963
- import { Command as Command10 } from "commander";
1096
+ import { Command as Command11 } from "commander";
964
1097
  var VALID_LINK_TYPES = [
965
1098
  "related_to",
966
1099
  "derived_from",
967
1100
  "contradicts",
968
1101
  "depends_on"
969
1102
  ];
970
- var linkCommand = new Command10("link").description("Create a link between two memories").argument("<source-id>", "Source memory ID").argument("<target-id>", "Target memory ID").requiredOption(
1103
+ var linkCommand = new Command11("link").description("Create a link between two memories").argument("<source-id>", "Source memory ID").argument("<target-id>", "Target memory ID").requiredOption(
971
1104
  "--type <link-type>",
972
1105
  "Link type (related_to, derived_from, contradicts, depends_on)"
973
1106
  ).option("--remote", "Create link on remote").addHelpText("after", `
@@ -1037,7 +1170,7 @@ Examples:
1037
1170
  store.close();
1038
1171
  }
1039
1172
  });
1040
- var unlinkCommand = new Command10("unlink").description("Remove a link between two memories").argument("<source-id>", "Source memory ID").argument("<target-id>", "Target memory ID").requiredOption(
1173
+ var unlinkCommand = new Command11("unlink").description("Remove a link between two memories").argument("<source-id>", "Source memory ID").argument("<target-id>", "Target memory ID").requiredOption(
1041
1174
  "--type <link-type>",
1042
1175
  "Link type (related_to, derived_from, contradicts, depends_on)"
1043
1176
  ).option("--remote", "Remove link on remote").action(async (sourceId, targetId, opts) => {
@@ -1077,7 +1210,7 @@ var unlinkCommand = new Command10("unlink").description("Remove a link between t
1077
1210
  store.close();
1078
1211
  }
1079
1212
  });
1080
- var linksCommand = new Command10("links").description("List all links for a memory").argument("<memory-id>", "Memory ID to get links for").option(
1213
+ var linksCommand = new Command11("links").description("List all links for a memory").argument("<memory-id>", "Memory ID to get links for").option(
1081
1214
  "--type <link-type>",
1082
1215
  "Filter by link type (related_to, derived_from, contradicts, depends_on)"
1083
1216
  ).option("--remote", "List links on remote").action(async (memoryId, opts) => {
@@ -1147,9 +1280,9 @@ var linksCommand = new Command10("links").description("List all links for a memo
1147
1280
  });
1148
1281
 
1149
1282
  // src/commands/merge.ts
1150
- import { Command as Command11 } from "commander";
1283
+ import { Command as Command12 } from "commander";
1151
1284
  var cwd = process.cwd();
1152
- var mergeCommand = new Command11("merge").description(
1285
+ var mergeCommand = new Command12("merge").description(
1153
1286
  "Consolidate multiple local memories into one unified memory while preserving history"
1154
1287
  ).argument("<ids...>", "Memory IDs to consolidate (minimum 2)").requiredOption(
1155
1288
  "-t, --text <text>",
@@ -1221,7 +1354,7 @@ var mergeCommand = new Command11("merge").description(
1221
1354
  store.close();
1222
1355
  }
1223
1356
  });
1224
- var remergeCommand = new Command11("remerge").description(
1357
+ var remergeCommand = new Command12("remerge").description(
1225
1358
  "Update an existing consolidation with new information or additional sources"
1226
1359
  ).argument("<consolidation-id>", "ID of existing consolidated memory to update").requiredOption("-t, --text <text>", "Updated consolidated text").option(
1227
1360
  "--add <ids>",
@@ -1260,7 +1393,7 @@ var remergeCommand = new Command11("remerge").description(
1260
1393
  store.close();
1261
1394
  }
1262
1395
  });
1263
- var similarCommand = new Command11("similar").description("Find memories similar to a given memory (candidates for merging)").argument("<memory-id>", "Memory ID to find similar ones for").option(
1396
+ var similarCommand = new Command12("similar").description("Find memories similar to a given memory (candidates for merging)").argument("<memory-id>", "Memory ID to find similar ones for").option(
1264
1397
  "-k, --limit <n>",
1265
1398
  "Max number of similar memories to return",
1266
1399
  "10"
@@ -1324,7 +1457,7 @@ Examples:
1324
1457
  store.close();
1325
1458
  }
1326
1459
  });
1327
- var historyCommand = new Command11("history").description("Show consolidation history for a memory").argument("<memory-id>", "Memory ID to show history for").action(async (memoryId) => {
1460
+ var historyCommand = new Command12("history").description("Show consolidation history for a memory").argument("<memory-id>", "Memory ID to show history for").action(async (memoryId) => {
1328
1461
  if (!isInitialized(cwd)) {
1329
1462
  logger.error("Unforgit not initialized. Run 'unforgit init' first.");
1330
1463
  process.exit(EXIT_CONFIG_ERROR);
@@ -1389,7 +1522,7 @@ var historyCommand = new Command11("history").description("Show consolidation hi
1389
1522
  });
1390
1523
 
1391
1524
  // src/commands/auto-consolidate.ts
1392
- import { Command as Command12 } from "commander";
1525
+ import { Command as Command13 } from "commander";
1393
1526
  import * as readline2 from "readline";
1394
1527
  var cwd2 = process.cwd();
1395
1528
  function createReadlineInterface() {
@@ -1405,7 +1538,7 @@ async function askConfirmation(rl, question) {
1405
1538
  });
1406
1539
  });
1407
1540
  }
1408
- var autoConsolidateCommand = new Command12("auto-consolidate").description(
1541
+ var autoConsolidateCommand = new Command13("auto-consolidate").description(
1409
1542
  "Automatically find and consolidate similar memories using AI"
1410
1543
  ).option(
1411
1544
  "--threshold <score>",
@@ -1570,8 +1703,8 @@ Original memories are preserved with status 'superseded'.`
1570
1703
  });
1571
1704
 
1572
1705
  // src/commands/unconsolidate.ts
1573
- import { Command as Command13 } from "commander";
1574
- var unconsolidateCommand = new Command13("unconsolidate").description("Revert a consolidation, restoring original memories to active status").argument("<consolidation-id>", "ID of the consolidated memory to revert").option("--dry-run", "Show what would be restored without making changes").option("--force", "Skip confirmation").addHelpText("after", `
1706
+ import { Command as Command14 } from "commander";
1707
+ var unconsolidateCommand = new Command14("unconsolidate").description("Revert a consolidation, restoring original memories to active status").argument("<consolidation-id>", "ID of the consolidated memory to revert").option("--dry-run", "Show what would be restored without making changes").option("--force", "Skip confirmation").addHelpText("after", `
1575
1708
  Examples:
1576
1709
  unforgit unconsolidate abc123 --dry-run Preview what would be restored
1577
1710
  unforgit unconsolidate abc123 Revert a consolidation`).action(async (consolidationId, opts) => {
@@ -1645,8 +1778,8 @@ Examples:
1645
1778
  });
1646
1779
 
1647
1780
  // src/commands/status.ts
1648
- import { Command as Command14 } from "commander";
1649
- var statusCommand = new Command14("status").description("Show the working tree status (pending sync state)").option("-s, --short", "Give the output in short format").addHelpText("after", `
1781
+ import { Command as Command15 } from "commander";
1782
+ var statusCommand = new Command15("status").description("Show the working tree status (pending sync state)").option("-s, --short", "Give the output in short format").addHelpText("after", `
1650
1783
  Examples:
1651
1784
  unforgit status Show full sync status
1652
1785
  unforgit status -s Short format
@@ -1664,32 +1797,50 @@ Examples:
1664
1797
  const remoteUrl = config.remote.url;
1665
1798
  const remoteName = "origin";
1666
1799
  const pendingPush = store.getPendingPush();
1800
+ const pendingPull = store.getSyncStatesByStatus("pending_pull").map((syncState) => {
1801
+ const memory = store.getById(syncState.memoryId);
1802
+ return memory ? { memory, syncState } : void 0;
1803
+ }).filter((item) => Boolean(item));
1667
1804
  const conflicts = store.getConflicts();
1668
1805
  const untracked = store.getUntrackedMemories(orgId, repoId);
1669
1806
  const summary = store.getSyncSummary(orgId, repoId);
1670
1807
  if (isJsonMode()) {
1808
+ const clean = pendingPush.length === 0 && pendingPull.length === 0 && conflicts.length === 0 && untracked.length === 0;
1671
1809
  outputJson({
1672
1810
  remote: remoteUrl || null,
1811
+ remoteConfigured: Boolean(remoteUrl),
1812
+ synced: summary.synced,
1673
1813
  pendingPush: pendingPush.length,
1814
+ pendingPull: pendingPull.length,
1674
1815
  conflicts: conflicts.length,
1675
1816
  untracked: untracked.length,
1676
- synced: summary.synced
1817
+ clean,
1818
+ recommendations: buildRecommendations(Boolean(remoteUrl), pendingPush.length, pendingPull.length, conflicts.length, untracked.length),
1819
+ details: {
1820
+ pendingPush: pendingPush.map(toDetailsItem),
1821
+ pendingPull: pendingPull.map(toDetailsItem),
1822
+ conflicts: conflicts.map(toDetailsItem),
1823
+ untracked: untracked.map((memory) => ({ id: memory.id, preview: truncate(memory.text, 80), status: memory.status }))
1824
+ }
1677
1825
  });
1678
1826
  return;
1679
1827
  }
1680
1828
  if (opts.short) {
1681
- printShortStatus(pendingPush, conflicts, untracked);
1829
+ printShortStatus(pendingPush, pendingPull, conflicts, untracked);
1682
1830
  } else {
1683
- printLongStatus(remoteName, remoteUrl, pendingPush, conflicts, untracked, summary);
1831
+ printLongStatus(remoteName, remoteUrl, pendingPush, pendingPull, conflicts, untracked, summary);
1684
1832
  }
1685
1833
  } finally {
1686
1834
  store.close();
1687
1835
  }
1688
1836
  });
1689
- function printShortStatus(pendingPush, conflicts, untracked) {
1837
+ function printShortStatus(pendingPush, pendingPull, conflicts, untracked) {
1690
1838
  for (const { memory } of pendingPush) {
1691
1839
  logger.info(`M ${memory.id.slice(0, 8)} ${truncate(memory.text, 50)}`);
1692
1840
  }
1841
+ for (const { memory } of pendingPull) {
1842
+ logger.info(`P ${memory.id.slice(0, 8)} ${truncate(memory.text, 50)}`);
1843
+ }
1693
1844
  for (const { memory } of conflicts) {
1694
1845
  logger.info(`C ${memory.id.slice(0, 8)} ${truncate(memory.text, 50)}`);
1695
1846
  }
@@ -1697,15 +1848,15 @@ function printShortStatus(pendingPush, conflicts, untracked) {
1697
1848
  logger.info(`?? ${memory.id.slice(0, 8)} ${truncate(memory.text, 50)}`);
1698
1849
  }
1699
1850
  }
1700
- function printLongStatus(remoteName, remoteUrl, pendingPush, conflicts, untracked, summary) {
1851
+ function printLongStatus(remoteName, remoteUrl, pendingPush, pendingPull, conflicts, untracked, summary) {
1701
1852
  if (remoteUrl) {
1702
1853
  logger.info(`Remote '${remoteName}' at ${remoteUrl}`);
1703
1854
  } else {
1704
1855
  logger.info("No remote configured. Use 'unforgit remote add origin <url>' to add one.");
1705
1856
  }
1706
1857
  logger.info("");
1707
- if (pendingPush.length === 0 && conflicts.length === 0 && untracked.length === 0) {
1708
- logger.info("Nothing to push, working tree clean");
1858
+ if (pendingPush.length === 0 && pendingPull.length === 0 && conflicts.length === 0 && untracked.length === 0) {
1859
+ logger.info("Nothing to push or pull, working tree clean");
1709
1860
  if (summary.synced > 0) {
1710
1861
  logger.info(` ${summary.synced} memories synced with remote`);
1711
1862
  }
@@ -1721,6 +1872,15 @@ function printLongStatus(remoteName, remoteUrl, pendingPush, conflicts, untracke
1721
1872
  }
1722
1873
  logger.info("");
1723
1874
  }
1875
+ if (pendingPull.length > 0) {
1876
+ logger.info("Changes to be pulled:");
1877
+ logger.info(' (use "unforgit pull" to sync from remote)');
1878
+ logger.info("");
1879
+ for (const { memory } of pendingPull) {
1880
+ logger.info(` remote update: ${memory.id.slice(0, 8)}... "${truncate(memory.text, 40)}"`);
1881
+ }
1882
+ logger.info("");
1883
+ }
1724
1884
  if (conflicts.length > 0) {
1725
1885
  logger.info("Conflicts:");
1726
1886
  logger.info(' (use "unforgit push --force" to overwrite remote or "unforgit pull --force" to accept remote)');
@@ -1740,13 +1900,43 @@ function printLongStatus(remoteName, remoteUrl, pendingPush, conflicts, untracke
1740
1900
  }
1741
1901
  logger.info("");
1742
1902
  }
1743
- const total = pendingPush.length + conflicts.length + untracked.length;
1903
+ const total = pendingPush.length + pendingPull.length + conflicts.length + untracked.length;
1744
1904
  logger.info(`${total} change(s) pending`);
1745
1905
  }
1906
+ function buildRecommendations(remoteConfigured, pendingPush, pendingPull, conflicts, untracked) {
1907
+ const recommendations = [];
1908
+ if (!remoteConfigured && (pendingPush > 0 || pendingPull > 0 || untracked > 0)) {
1909
+ recommendations.push("Configure a remote with 'unforgit remote add origin <url>' or keep this repository intentionally local-only.");
1910
+ return recommendations;
1911
+ }
1912
+ if (conflicts > 0) {
1913
+ recommendations.push("Review conflicts, then run 'unforgit pull --force' to accept remote or 'unforgit push --force' to keep local.");
1914
+ }
1915
+ if (pendingPush > 0 || untracked > 0) {
1916
+ recommendations.push("Run 'unforgit push' to publish local memory changes.");
1917
+ }
1918
+ if (pendingPull > 0) {
1919
+ recommendations.push("Run 'unforgit pull' to fetch remote memory changes.");
1920
+ }
1921
+ return recommendations;
1922
+ }
1923
+ function toDetailsItem(item) {
1924
+ const { memory, syncState } = item;
1925
+ return {
1926
+ id: memory.id,
1927
+ preview: truncate(memory.text, 80),
1928
+ status: memory.status,
1929
+ syncStatus: syncState.syncStatus,
1930
+ localVersion: syncState.localVersion,
1931
+ remoteVersion: syncState.remoteVersion,
1932
+ lastPushedAt: syncState.lastPushedAt?.toISOString(),
1933
+ lastPulledAt: syncState.lastPulledAt?.toISOString()
1934
+ };
1935
+ }
1746
1936
 
1747
1937
  // src/commands/push.ts
1748
- import { Command as Command15 } from "commander";
1749
- var pushCommand = new Command15("push").description("Push local memories to remote").argument("[remote]", "Remote name to push to", "origin").option("-f, --force", "Force push, overwriting remote conflicts").option("--dry-run", "Show what would be pushed without actually pushing").option("-a, --all", "Push all memories including untracked ones").action(async (remote, opts) => {
1938
+ import { Command as Command16 } from "commander";
1939
+ var pushCommand = new Command16("push").description("Push local memories to remote").argument("[remote]", "Remote name to push to", "origin").option("-f, --force", "Force push, overwriting remote conflicts").option("--dry-run", "Show what would be pushed without actually pushing").option("-a, --all", "Push all memories including untracked ones").action(async (remote, opts) => {
1750
1940
  if (!isInitialized()) {
1751
1941
  logger.fatal("not an unforgit repository");
1752
1942
  process.exit(EXIT_CONFIG_ERROR);
@@ -1890,8 +2080,8 @@ Total: ${allToPush.length} memories, ${supersededToSync.length} status updates,
1890
2080
  });
1891
2081
 
1892
2082
  // src/commands/pull.ts
1893
- import { Command as Command16 } from "commander";
1894
- var pullCommand = new Command16("pull").description("Pull remote memories to local").argument("[remote]", "Remote name to pull from", "origin").option("-f, --force", "Force pull, overwriting local conflicts").option("--dry-run", "Show what would be pulled without actually pulling").action(async (remote, opts) => {
2083
+ import { Command as Command17 } from "commander";
2084
+ var pullCommand = new Command17("pull").description("Pull remote memories to local").argument("[remote]", "Remote name to pull from", "origin").option("-f, --force", "Force pull, overwriting local conflicts").option("--dry-run", "Show what would be pulled without actually pulling").action(async (remote, opts) => {
1895
2085
  if (!isInitialized()) {
1896
2086
  logger.fatal("not an unforgit repository");
1897
2087
  process.exit(EXIT_CONFIG_ERROR);
@@ -2036,8 +2226,8 @@ Would pull:`);
2036
2226
  });
2037
2227
 
2038
2228
  // src/commands/remote.ts
2039
- import { Command as Command17 } from "commander";
2040
- var remoteCommand = new Command17("remote").description("Manage set of tracked remote repositories").addHelpText("after", `
2229
+ import { Command as Command18 } from "commander";
2230
+ var remoteCommand = new Command18("remote").description("Manage set of tracked remote repositories").addHelpText("after", `
2041
2231
  Examples:
2042
2232
  unforgit remote List remotes
2043
2233
  unforgit remote add origin <url> Add a remote
@@ -2057,7 +2247,7 @@ Examples:
2057
2247
  logger.info(`${name} ${remote.url}`);
2058
2248
  }
2059
2249
  });
2060
- var remoteAddCommand = new Command17("add").description("Add a new remote").argument("<name>", "Name for the remote (e.g., origin)").argument("<url>", "URL of the remote unforgit server").option("--org <orgId>", "Organization ID").option("--repo <repoId>", "Repository ID").action((name, url, opts) => {
2250
+ var remoteAddCommand = new Command18("add").description("Add a new remote").argument("<name>", "Name for the remote (e.g., origin)").argument("<url>", "URL of the remote unforgit server").option("--org <orgId>", "Organization ID").option("--repo <repoId>", "Repository ID").action((name, url, opts) => {
2061
2251
  if (!isInitialized()) {
2062
2252
  logger.fatal("not an unforgit repository");
2063
2253
  process.exit(EXIT_CONFIG_ERROR);
@@ -2077,7 +2267,7 @@ var remoteAddCommand = new Command17("add").description("Add a new remote").argu
2077
2267
  saveRemotes(config, remotes);
2078
2268
  logger.info(`Remote '${name}' added: ${url}`);
2079
2269
  });
2080
- var remoteRemoveCommand = new Command17("remove").alias("rm").description("Remove a remote").argument("<name>", "Name of the remote to remove").action((name) => {
2270
+ var remoteRemoveCommand = new Command18("remove").alias("rm").description("Remove a remote").argument("<name>", "Name of the remote to remove").action((name) => {
2081
2271
  if (!isInitialized()) {
2082
2272
  logger.fatal("not an unforgit repository");
2083
2273
  process.exit(EXIT_CONFIG_ERROR);
@@ -2092,7 +2282,7 @@ var remoteRemoveCommand = new Command17("remove").alias("rm").description("Remov
2092
2282
  saveRemotes(config, remotes);
2093
2283
  logger.info(`Remote '${name}' removed.`);
2094
2284
  });
2095
- var remoteSetUrlCommand = new Command17("set-url").description("Change the URL for a remote").argument("<name>", "Name of the remote").argument("<newurl>", "New URL for the remote").action((name, newurl) => {
2285
+ var remoteSetUrlCommand = new Command18("set-url").description("Change the URL for a remote").argument("<name>", "Name of the remote").argument("<newurl>", "New URL for the remote").action((name, newurl) => {
2096
2286
  if (!isInitialized()) {
2097
2287
  logger.fatal("not an unforgit repository");
2098
2288
  process.exit(EXIT_CONFIG_ERROR);
@@ -2108,7 +2298,7 @@ var remoteSetUrlCommand = new Command17("set-url").description("Change the URL f
2108
2298
  saveRemotes(config, remotes);
2109
2299
  logger.info(`Remote '${name}' URL changed to: ${newurl}`);
2110
2300
  });
2111
- var remoteShowCommand = new Command17("show").description("Show information about a remote").argument("<name>", "Name of the remote").action((name) => {
2301
+ var remoteShowCommand = new Command18("show").description("Show information about a remote").argument("<name>", "Name of the remote").action((name) => {
2112
2302
  if (!isInitialized()) {
2113
2303
  logger.fatal("not an unforgit repository");
2114
2304
  process.exit(EXIT_CONFIG_ERROR);
@@ -2158,8 +2348,8 @@ function saveRemotes(config, remotes) {
2158
2348
  }
2159
2349
 
2160
2350
  // src/commands/log.ts
2161
- import { Command as Command18 } from "commander";
2162
- var logCommand = new Command18("log").description("Show memory history log").option("-n, --max-count <n>", "Limit the number of memories shown", "20").option("--oneline", "Show each memory on a single line").option("--all", "Show all memories including deprecated/superseded").option("--type <type>", "Filter by memory type (episodic|semantic|procedural)").option("--tags <tags>", "Filter by tags (comma-separated)").option("--page <n>", "Page number for pagination", "1").option("--per-page <n>", "Items per page", "20").addHelpText("after", `
2351
+ import { Command as Command19 } from "commander";
2352
+ var logCommand = new Command19("log").description("Show memory history log").option("-n, --max-count <n>", "Limit the number of memories shown", "20").option("--oneline", "Show each memory on a single line").option("--all", "Show all memories including deprecated/superseded").option("--type <type>", "Filter by memory type (episodic|semantic|procedural)").option("--tags <tags>", "Filter by tags (comma-separated)").option("--page <n>", "Page number for pagination", "1").option("--per-page <n>", "Items per page", "20").addHelpText("after", `
2163
2353
  Examples:
2164
2354
  unforgit log Show recent memories
2165
2355
  unforgit log --all Include deprecated/superseded
@@ -2251,8 +2441,8 @@ Examples:
2251
2441
  });
2252
2442
 
2253
2443
  // src/commands/diff.ts
2254
- import { Command as Command19 } from "commander";
2255
- var diffCommand = new Command19("diff").description("Show differences between local and remote memories").argument("[memoryId]", "Specific memory ID to diff").option("--stat", "Show only statistics").addHelpText("after", `
2444
+ import { Command as Command20 } from "commander";
2445
+ var diffCommand = new Command20("diff").description("Show differences between local and remote memories").argument("[memoryId]", "Specific memory ID to diff").option("--stat", "Show only statistics").addHelpText("after", `
2256
2446
  Examples:
2257
2447
  unforgit diff Show all differences
2258
2448
  unforgit diff --stat Show difference statistics only
@@ -2418,7 +2608,7 @@ function findFullId(store, partialId, orgId, repoId) {
2418
2608
  }
2419
2609
 
2420
2610
  // src/commands/keys.ts
2421
- import { Command as Command20 } from "commander";
2611
+ import { Command as Command21 } from "commander";
2422
2612
  function requireRemote() {
2423
2613
  if (!isInitialized()) {
2424
2614
  logger.fatal("not an unforgit repository");
@@ -2438,7 +2628,7 @@ function requireRemote() {
2438
2628
  }
2439
2629
  return { url: config.remote.url, apiKey };
2440
2630
  }
2441
- var keysCommand = new Command20("keys").description("Manage API keys for remote authentication");
2631
+ var keysCommand = new Command21("keys").description("Manage API keys for remote authentication");
2442
2632
  keysCommand.command("create").description("Create a new API key").requiredOption("--name <name>", "Name for the API key").requiredOption("--org <orgId>", "Organization ID for the key").addHelpText("after", `
2443
2633
  Examples:
2444
2634
  unforgit keys create --name "CI pipeline" --org my-org
@@ -2507,8 +2697,8 @@ keysCommand.command("revoke").description("Revoke an API key").argument("<id>",
2507
2697
  });
2508
2698
 
2509
2699
  // src/commands/auth.ts
2510
- import { Command as Command21 } from "commander";
2511
- var authCommand = new Command21("auth").description("Check authentication status for remote server and APIs");
2700
+ import { Command as Command22 } from "commander";
2701
+ var authCommand = new Command22("auth").description("Check authentication status for remote server and APIs");
2512
2702
  authCommand.command("status").description("Check authentication status").action(async () => {
2513
2703
  if (!isInitialized()) {
2514
2704
  logger.fatal("not an unforgit repository");
@@ -2559,8 +2749,8 @@ authCommand.command("status").description("Check authentication status").action(
2559
2749
  });
2560
2750
 
2561
2751
  // src/commands/config.ts
2562
- import { Command as Command22 } from "commander";
2563
- var configCommand = new Command22("config").description("Manage unforgit configuration");
2752
+ import { Command as Command23 } from "commander";
2753
+ var configCommand = new Command23("config").description("Manage unforgit configuration");
2564
2754
  configCommand.command("list").alias("ls").description("List all configuration values").action(() => {
2565
2755
  if (!isInitialized()) {
2566
2756
  logger.fatal("not an unforgit repository");
@@ -2721,9 +2911,33 @@ function coerceConfigValue(value) {
2721
2911
  }
2722
2912
 
2723
2913
  // src/commands/embeddings.ts
2724
- import { Command as Command23 } from "commander";
2914
+ import { Command as Command24 } from "commander";
2725
2915
  var cwd3 = process.cwd();
2726
- var embeddingsCommand = new Command23("embeddings").description("Manage memory embeddings for semantic search");
2916
+ function embeddingCoverage(stats) {
2917
+ return stats.total > 0 ? Number((stats.withEmbedding / stats.total * 100).toFixed(1)) : 0;
2918
+ }
2919
+ function buildBackfillJsonPayload(options) {
2920
+ const statsAfter = options.statsAfter ?? options.statsBefore;
2921
+ const failures = options.failures ?? [];
2922
+ return {
2923
+ dryRun: options.dryRun,
2924
+ model: options.model,
2925
+ planned: options.planned,
2926
+ processed: options.processed,
2927
+ errors: failures.length,
2928
+ failures,
2929
+ statsBefore: {
2930
+ ...options.statsBefore,
2931
+ coverage: embeddingCoverage(options.statsBefore)
2932
+ },
2933
+ statsAfter: {
2934
+ ...statsAfter,
2935
+ coverage: embeddingCoverage(statsAfter)
2936
+ },
2937
+ ...options.previews ? { memories: options.previews, truncated: Boolean(options.truncated) } : {}
2938
+ };
2939
+ }
2940
+ var embeddingsCommand = new Command24("embeddings").description("Manage memory embeddings for semantic search");
2727
2941
  embeddingsCommand.command("backfill").description("Generate embeddings for memories that don't have them").option("--batch-size <n>", "Number of memories to process in parallel", "5").option("--delay <ms>", "Delay between batches (ms)", "500").option("--dry-run", "Show what would be done without making changes").option("--model <model>", "OpenAI embedding model", "text-embedding-3-small").action(async (opts) => {
2728
2942
  if (!isInitialized(cwd3)) {
2729
2943
  logger.error("Unforgit not initialized. Run 'unforgit init' first.");
@@ -2743,12 +2957,37 @@ embeddingsCommand.command("backfill").description("Generate embeddings for memor
2743
2957
  try {
2744
2958
  const memories = store.getMemoriesWithoutEmbeddings(orgId, repoId);
2745
2959
  const stats = store.getEmbeddingStats(orgId, repoId);
2960
+ if (isJsonMode() && opts.dryRun) {
2961
+ outputJson(buildBackfillJsonPayload({
2962
+ dryRun: true,
2963
+ model: opts.model,
2964
+ statsBefore: stats,
2965
+ planned: memories.length,
2966
+ processed: 0,
2967
+ previews: memories.slice(0, 10).map((memory) => ({
2968
+ id: memory.id,
2969
+ textPreview: memory.text.slice(0, 80)
2970
+ })),
2971
+ truncated: memories.length > 10
2972
+ }));
2973
+ return;
2974
+ }
2746
2975
  logger.info(`Embedding stats:`);
2747
2976
  logger.info(` Total memories: ${stats.total}`);
2748
2977
  logger.info(` With embedding: ${stats.withEmbedding}`);
2749
2978
  logger.info(` Without embedding: ${stats.withoutEmbedding}`);
2750
2979
  logger.info("");
2751
2980
  if (memories.length === 0) {
2981
+ if (isJsonMode()) {
2982
+ outputJson(buildBackfillJsonPayload({
2983
+ dryRun: Boolean(opts.dryRun),
2984
+ model: opts.model,
2985
+ statsBefore: stats,
2986
+ planned: 0,
2987
+ processed: 0
2988
+ }));
2989
+ return;
2990
+ }
2752
2991
  logger.info("All memories already have embeddings.");
2753
2992
  return;
2754
2993
  }
@@ -2767,7 +3006,7 @@ embeddingsCommand.command("backfill").description("Generate embeddings for memor
2767
3006
  const batchSize = parsePositiveInt(opts.batchSize, "batch-size");
2768
3007
  const delay = parsePositiveInt(opts.delay, "delay");
2769
3008
  let processed = 0;
2770
- let errors = 0;
3009
+ const failures = [];
2771
3010
  logger.info(`
2772
3011
  Generating embeddings (batch size: ${batchSize}, delay: ${delay}ms)...`);
2773
3012
  for (let i = 0; i < memories.length; i += batchSize) {
@@ -2783,8 +3022,13 @@ Generating embeddings (batch size: ${batchSize}, delay: ${delay}ms)...`);
2783
3022
  processed++;
2784
3023
  logger.progress(processed, memories.length, "embeddings");
2785
3024
  } catch (err) {
2786
- errors++;
2787
- logger.error(`${memory.id.slice(0, 8)}: ${err instanceof Error ? err.message : err}`);
3025
+ const message = err instanceof Error ? err.message : String(err);
3026
+ failures.push({
3027
+ id: memory.id,
3028
+ textPreview: memory.text.slice(0, 80),
3029
+ error: message
3030
+ });
3031
+ logger.error(`${memory.id.slice(0, 8)}: ${message}`);
2788
3032
  }
2789
3033
  })
2790
3034
  );
@@ -2795,7 +3039,22 @@ Generating embeddings (batch size: ${batchSize}, delay: ${delay}ms)...`);
2795
3039
  logger.info(`
2796
3040
  Backfill complete:`);
2797
3041
  logger.info(` Processed: ${processed}`);
2798
- logger.info(` Errors: ${errors}`);
3042
+ logger.info(` Errors: ${failures.length}`);
3043
+ const statsAfter = store.getEmbeddingStats(orgId, repoId);
3044
+ if (isJsonMode()) {
3045
+ outputJson(buildBackfillJsonPayload({
3046
+ dryRun: false,
3047
+ model: opts.model,
3048
+ statsBefore: stats,
3049
+ statsAfter,
3050
+ planned: memories.length,
3051
+ processed,
3052
+ failures
3053
+ }));
3054
+ }
3055
+ if (failures.length > 0) {
3056
+ process.exitCode = EXIT_ERROR;
3057
+ }
2799
3058
  } finally {
2800
3059
  store.close();
2801
3060
  }
@@ -2831,7 +3090,7 @@ Run 'unforgit embeddings backfill' to generate missing embeddings.`);
2831
3090
  store.close();
2832
3091
  }
2833
3092
  });
2834
- embeddingsCommand.command("clear").description("Remove all embeddings (requires regeneration)").option("--yes", "Skip confirmation").action(async (opts) => {
3093
+ embeddingsCommand.command("clear").description("Remove all embeddings (requires regeneration)").option("--yes", "Skip confirmation").option("--no-backup", "Skip automatic local database backup before clearing embeddings").action(async (opts) => {
2835
3094
  if (!isInitialized(cwd3)) {
2836
3095
  logger.error("Unforgit not initialized. Run 'unforgit init' first.");
2837
3096
  process.exit(EXIT_CONFIG_ERROR);
@@ -2842,6 +3101,19 @@ embeddingsCommand.command("clear").description("Remove all embeddings (requires
2842
3101
  return;
2843
3102
  }
2844
3103
  const dbPath = getDbPath(cwd3);
3104
+ if (opts.backup !== false) {
3105
+ try {
3106
+ const backup = createLocalDatabaseBackup(dbPath, "embeddings-clear");
3107
+ if (backup) {
3108
+ logger.info(`Created local embeddings backup: ${backup.dir}`);
3109
+ }
3110
+ } catch (err) {
3111
+ logger.error(
3112
+ `Failed to create local embeddings backup: ${err instanceof Error ? err.message : String(err)}`
3113
+ );
3114
+ process.exit(EXIT_ERROR);
3115
+ }
3116
+ }
2845
3117
  const store = new LocalStore(dbPath);
2846
3118
  try {
2847
3119
  const deleted = store.clearEmbeddings();
@@ -2853,121 +3125,6 @@ embeddingsCommand.command("clear").description("Remove all embeddings (requires
2853
3125
 
2854
3126
  // src/commands/reset.ts
2855
3127
  import { Command as Command25 } from "commander";
2856
-
2857
- // src/commands/backups.ts
2858
- import fs4 from "fs";
2859
- import path3 from "path";
2860
- import { Command as Command24 } from "commander";
2861
- function formatBackupTimestamp(date) {
2862
- return date.toISOString().replace(/[-:]/g, "").replace("T", "-").replace(/\.\d{3}Z$/, "");
2863
- }
2864
- function backupRootForDb(dbPath) {
2865
- return path3.join(path3.dirname(dbPath), "backups");
2866
- }
2867
- function describeBackup(dir) {
2868
- const files = fs4.readdirSync(dir).filter((file) => file === "local.db" || file === "local.db-wal" || file === "local.db-shm").sort();
2869
- const sizeBytes = files.reduce((total, file) => total + fs4.statSync(path3.join(dir, file)).size, 0);
2870
- const stat = fs4.statSync(dir);
2871
- return {
2872
- name: path3.basename(dir),
2873
- dir,
2874
- files,
2875
- sizeBytes,
2876
- createdAt: stat.mtime.toISOString()
2877
- };
2878
- }
2879
- function resolveBackupDir(backupRoot, backupName) {
2880
- if (backupName !== path3.basename(backupName)) {
2881
- throw new Error("Invalid backup name");
2882
- }
2883
- const resolvedRoot = path3.resolve(backupRoot);
2884
- const resolvedBackup = path3.resolve(resolvedRoot, backupName);
2885
- if (!resolvedBackup.startsWith(`${resolvedRoot}${path3.sep}`)) {
2886
- throw new Error("Invalid backup name");
2887
- }
2888
- return resolvedBackup;
2889
- }
2890
- function createLocalResetBackup(dbPath, now = /* @__PURE__ */ new Date()) {
2891
- if (!fs4.existsSync(dbPath)) {
2892
- return null;
2893
- }
2894
- const backupRoot = backupRootForDb(dbPath);
2895
- const baseName = `reset-${formatBackupTimestamp(now)}`;
2896
- let backupDir = path3.join(backupRoot, baseName);
2897
- let suffix = 1;
2898
- while (fs4.existsSync(backupDir)) {
2899
- backupDir = path3.join(backupRoot, `${baseName}-${suffix++}`);
2900
- }
2901
- fs4.mkdirSync(backupDir, { recursive: true, mode: 448 });
2902
- for (const source of [dbPath, `${dbPath}-wal`, `${dbPath}-shm`]) {
2903
- if (!fs4.existsSync(source)) continue;
2904
- fs4.copyFileSync(source, path3.join(backupDir, path3.basename(source)));
2905
- }
2906
- return describeBackup(backupDir);
2907
- }
2908
- function listLocalResetBackups(dbPath) {
2909
- const backupRoot = backupRootForDb(dbPath);
2910
- if (!fs4.existsSync(backupRoot)) {
2911
- return [];
2912
- }
2913
- return fs4.readdirSync(backupRoot, { withFileTypes: true }).filter((entry) => entry.isDirectory() && entry.name.startsWith("reset-")).map((entry) => describeBackup(path3.join(backupRoot, entry.name))).filter((backup) => backup.files.includes("local.db")).sort((a, b) => b.name.localeCompare(a.name));
2914
- }
2915
- function restoreLocalResetBackup(dbPath, backupName, now = /* @__PURE__ */ new Date()) {
2916
- const backupRoot = backupRootForDb(dbPath);
2917
- const backupDir = resolveBackupDir(backupRoot, backupName);
2918
- if (!fs4.existsSync(backupDir) || !fs4.statSync(backupDir).isDirectory()) {
2919
- throw new Error(`Backup not found: ${backupName}`);
2920
- }
2921
- const restoredFrom = describeBackup(backupDir);
2922
- if (!restoredFrom.files.includes("local.db")) {
2923
- throw new Error(`Backup is missing local.db: ${backupName}`);
2924
- }
2925
- const safetyBackup = createLocalResetBackup(dbPath, now);
2926
- fs4.mkdirSync(path3.dirname(dbPath), { recursive: true });
2927
- for (const target of [dbPath, `${dbPath}-wal`, `${dbPath}-shm`]) {
2928
- if (fs4.existsSync(target)) {
2929
- fs4.rmSync(target, { force: true });
2930
- }
2931
- }
2932
- for (const file of restoredFrom.files) {
2933
- fs4.copyFileSync(path3.join(backupDir, file), path3.join(path3.dirname(dbPath), file));
2934
- }
2935
- return { restoredFrom, safetyBackup };
2936
- }
2937
- var backupsCommand = new Command24("backups").description("List and restore local reset backups");
2938
- backupsCommand.command("list").description("List local backups created before destructive resets/restores").action(() => {
2939
- const backups = listLocalResetBackups(getDbPath());
2940
- if (backups.length === 0) {
2941
- logger.info("No local reset backups found.");
2942
- return;
2943
- }
2944
- for (const backup of backups) {
2945
- logger.info(`${backup.name} ${backup.sizeBytes} bytes ${backup.dir}`);
2946
- }
2947
- });
2948
- backupsCommand.command("restore").argument("<name>", "Backup directory name, for example reset-20260610-123456").description("Restore a local reset backup into the active local database").option("--force", "Skip confirmation prompt").action(async (name, opts) => {
2949
- if (!opts.force) {
2950
- const ok = await confirm(
2951
- "This will replace the active local database after creating a safety backup. Continue?"
2952
- );
2953
- if (!ok) {
2954
- logger.info("Aborted.");
2955
- return;
2956
- }
2957
- }
2958
- try {
2959
- const result = restoreLocalResetBackup(getDbPath(), name);
2960
- logger.info(`Restored local database from ${result.restoredFrom.name}`);
2961
- if (result.safetyBackup) {
2962
- logger.info(`Previous local database safety backup: ${result.safetyBackup.dir}`);
2963
- }
2964
- } catch (err) {
2965
- logger.error(err instanceof Error ? err.message : String(err));
2966
- process.exit(EXIT_ERROR);
2967
- }
2968
- });
2969
-
2970
- // src/commands/reset.ts
2971
3128
  var resetCommand = new Command25("reset").description("Permanently delete all memories and related data").option("--local", "Reset local store only").option("--remote", "Reset remote store only").option("--force", "Skip confirmation prompt").option("--no-backup", "Skip automatic local database backup before local reset").addHelpText("after", `
2972
3129
  Examples:
2973
3130
  unforgit reset Reset both local and remote
@@ -3227,6 +3384,7 @@ var doctorCommand = new Command27("doctor").description("Check system health and
3227
3384
  }
3228
3385
  const pendingPush = store.getPendingPush();
3229
3386
  const conflicts = store.getConflicts();
3387
+ const syncSummary = store.getSyncSummary(orgId, repoId);
3230
3388
  const unsyncedTombstones = store.getUnsyncedTombstones(orgId, repoId);
3231
3389
  if (unsyncedTombstones.length > 0) {
3232
3390
  results.push({
@@ -3243,16 +3401,22 @@ var doctorCommand = new Command27("doctor").description("Check system health and
3243
3401
  check: "sync",
3244
3402
  status: "warn",
3245
3403
  message: `${conflicts.length} sync conflict(s) need resolution`,
3246
- fix: "Resolve conflicts with 'unforgit pull --force' or 'unforgit push --force' after reviewing the desired source of truth."
3404
+ fix: "Resolve conflicts with 'unforgit pull --force' or 'unforgit push --force' after reviewing the desired source of truth.",
3405
+ details: syncSummary
3247
3406
  });
3248
- } else if (pendingPush.length > 0) {
3407
+ } else if (pendingPush.length > 0 || syncSummary.pendingPull > 0) {
3408
+ const parts = [];
3409
+ if (pendingPush.length > 0) parts.push(`${pendingPush.length} memory(s) pending push`);
3410
+ if (syncSummary.pendingPull > 0) parts.push(`${syncSummary.pendingPull} memory(s) pending pull`);
3249
3411
  results.push({
3250
3412
  check: "sync",
3251
- status: "ok",
3252
- message: `${pendingPush.length} memory(s) pending push`
3413
+ status: "warn",
3414
+ message: parts.join(", "),
3415
+ fix: "Run 'unforgit push' to publish local memory changes, or configure/disable sync if this repository is intentionally local-only.",
3416
+ details: syncSummary
3253
3417
  });
3254
3418
  } else {
3255
- results.push({ check: "sync", status: "ok", message: "Sync state clean" });
3419
+ results.push({ check: "sync", status: "ok", message: "Sync state clean", details: syncSummary });
3256
3420
  }
3257
3421
  } finally {
3258
3422
  store.close();