unforgit 0.5.6 → 0.7.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/{chunk-CKUDYQYP.js → chunk-TGDR7Y7T.js} +161 -18
- package/dist/chunk-TGDR7Y7T.js.map +1 -0
- package/dist/index.js +425 -210
- package/dist/index.js.map +1 -1
- package/dist/mcp.js +1 -1
- package/package.json +3 -3
- package/dist/chunk-CKUDYQYP.js.map +0 -1
package/dist/index.js
CHANGED
|
@@ -17,6 +17,7 @@ import {
|
|
|
17
17
|
getDbPath,
|
|
18
18
|
getTemplate,
|
|
19
19
|
isInitialized,
|
|
20
|
+
isOpenAIConfigured,
|
|
20
21
|
loadConfig,
|
|
21
22
|
mergeAndRank,
|
|
22
23
|
parseConfidence,
|
|
@@ -24,12 +25,13 @@ import {
|
|
|
24
25
|
parseThreshold,
|
|
25
26
|
parseTtl,
|
|
26
27
|
persistReviewableSuggestions,
|
|
28
|
+
resolveEmbeddingProvider,
|
|
27
29
|
resolveLifecycleConfig,
|
|
28
30
|
resolveVisibility,
|
|
29
31
|
runLocalLifecycleMaintenance,
|
|
30
32
|
saveConfig,
|
|
31
33
|
validateMemoryType
|
|
32
|
-
} from "./chunk-
|
|
34
|
+
} from "./chunk-TGDR7Y7T.js";
|
|
33
35
|
|
|
34
36
|
// src/index.ts
|
|
35
37
|
import { Command as Command31 } from "commander";
|
|
@@ -808,11 +810,130 @@ Examples:
|
|
|
808
810
|
});
|
|
809
811
|
|
|
810
812
|
// src/commands/delete.ts
|
|
813
|
+
import { Command as Command9 } from "commander";
|
|
814
|
+
|
|
815
|
+
// src/commands/backups.ts
|
|
816
|
+
import fs3 from "fs";
|
|
817
|
+
import path2 from "path";
|
|
811
818
|
import { Command as Command8 } from "commander";
|
|
812
|
-
|
|
819
|
+
function formatBackupTimestamp(date) {
|
|
820
|
+
return date.toISOString().replace(/[-:]/g, "").replace("T", "-").replace(/\.\d{3}Z$/, "");
|
|
821
|
+
}
|
|
822
|
+
function backupRootForDb(dbPath) {
|
|
823
|
+
return path2.join(path2.dirname(dbPath), "backups");
|
|
824
|
+
}
|
|
825
|
+
function describeBackup(dir) {
|
|
826
|
+
const files = fs3.readdirSync(dir).filter((file) => file === "local.db" || file === "local.db-wal" || file === "local.db-shm").sort();
|
|
827
|
+
const sizeBytes = files.reduce((total, file) => total + fs3.statSync(path2.join(dir, file)).size, 0);
|
|
828
|
+
const stat = fs3.statSync(dir);
|
|
829
|
+
return {
|
|
830
|
+
name: path2.basename(dir),
|
|
831
|
+
dir,
|
|
832
|
+
files,
|
|
833
|
+
sizeBytes,
|
|
834
|
+
createdAt: stat.mtime.toISOString()
|
|
835
|
+
};
|
|
836
|
+
}
|
|
837
|
+
function resolveBackupDir(backupRoot, backupName) {
|
|
838
|
+
if (backupName !== path2.basename(backupName)) {
|
|
839
|
+
throw new Error("Invalid backup name");
|
|
840
|
+
}
|
|
841
|
+
const resolvedRoot = path2.resolve(backupRoot);
|
|
842
|
+
const resolvedBackup = path2.resolve(resolvedRoot, backupName);
|
|
843
|
+
if (!resolvedBackup.startsWith(`${resolvedRoot}${path2.sep}`)) {
|
|
844
|
+
throw new Error("Invalid backup name");
|
|
845
|
+
}
|
|
846
|
+
return resolvedBackup;
|
|
847
|
+
}
|
|
848
|
+
function createLocalDatabaseBackup(dbPath, prefix, now = /* @__PURE__ */ new Date()) {
|
|
849
|
+
if (!fs3.existsSync(dbPath)) {
|
|
850
|
+
return null;
|
|
851
|
+
}
|
|
852
|
+
const backupRoot = backupRootForDb(dbPath);
|
|
853
|
+
const baseName = `${prefix}-${formatBackupTimestamp(now)}`;
|
|
854
|
+
let backupDir = path2.join(backupRoot, baseName);
|
|
855
|
+
let suffix = 1;
|
|
856
|
+
while (fs3.existsSync(backupDir)) {
|
|
857
|
+
backupDir = path2.join(backupRoot, `${baseName}-${suffix++}`);
|
|
858
|
+
}
|
|
859
|
+
fs3.mkdirSync(backupDir, { recursive: true, mode: 448 });
|
|
860
|
+
for (const source of [dbPath, `${dbPath}-wal`, `${dbPath}-shm`]) {
|
|
861
|
+
if (!fs3.existsSync(source)) continue;
|
|
862
|
+
fs3.copyFileSync(source, path2.join(backupDir, path2.basename(source)));
|
|
863
|
+
}
|
|
864
|
+
return describeBackup(backupDir);
|
|
865
|
+
}
|
|
866
|
+
function createLocalResetBackup(dbPath, now = /* @__PURE__ */ new Date()) {
|
|
867
|
+
return createLocalDatabaseBackup(dbPath, "reset", now);
|
|
868
|
+
}
|
|
869
|
+
function listLocalResetBackups(dbPath) {
|
|
870
|
+
const backupRoot = backupRootForDb(dbPath);
|
|
871
|
+
if (!fs3.existsSync(backupRoot)) {
|
|
872
|
+
return [];
|
|
873
|
+
}
|
|
874
|
+
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));
|
|
875
|
+
}
|
|
876
|
+
function restoreLocalResetBackup(dbPath, backupName, now = /* @__PURE__ */ new Date()) {
|
|
877
|
+
const backupRoot = backupRootForDb(dbPath);
|
|
878
|
+
const backupDir = resolveBackupDir(backupRoot, backupName);
|
|
879
|
+
if (!fs3.existsSync(backupDir) || !fs3.statSync(backupDir).isDirectory()) {
|
|
880
|
+
throw new Error(`Backup not found: ${backupName}`);
|
|
881
|
+
}
|
|
882
|
+
const restoredFrom = describeBackup(backupDir);
|
|
883
|
+
if (!restoredFrom.files.includes("local.db")) {
|
|
884
|
+
throw new Error(`Backup is missing local.db: ${backupName}`);
|
|
885
|
+
}
|
|
886
|
+
const safetyBackup = createLocalResetBackup(dbPath, now);
|
|
887
|
+
fs3.mkdirSync(path2.dirname(dbPath), { recursive: true });
|
|
888
|
+
for (const target of [dbPath, `${dbPath}-wal`, `${dbPath}-shm`]) {
|
|
889
|
+
if (fs3.existsSync(target)) {
|
|
890
|
+
fs3.rmSync(target, { force: true });
|
|
891
|
+
}
|
|
892
|
+
}
|
|
893
|
+
for (const file of restoredFrom.files) {
|
|
894
|
+
fs3.copyFileSync(path2.join(backupDir, file), path2.join(path2.dirname(dbPath), file));
|
|
895
|
+
}
|
|
896
|
+
return { restoredFrom, safetyBackup };
|
|
897
|
+
}
|
|
898
|
+
var backupsCommand = new Command8("backups").description("List and restore local reset backups");
|
|
899
|
+
backupsCommand.command("list").description("List local backups created before destructive resets/restores").action(() => {
|
|
900
|
+
const backups = listLocalResetBackups(getDbPath());
|
|
901
|
+
if (backups.length === 0) {
|
|
902
|
+
logger.info("No local reset backups found.");
|
|
903
|
+
return;
|
|
904
|
+
}
|
|
905
|
+
for (const backup of backups) {
|
|
906
|
+
logger.info(`${backup.name} ${backup.sizeBytes} bytes ${backup.dir}`);
|
|
907
|
+
}
|
|
908
|
+
});
|
|
909
|
+
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) => {
|
|
910
|
+
if (!opts.force) {
|
|
911
|
+
const ok = await confirm(
|
|
912
|
+
"This will replace the active local database after creating a safety backup. Continue?"
|
|
913
|
+
);
|
|
914
|
+
if (!ok) {
|
|
915
|
+
logger.info("Aborted.");
|
|
916
|
+
return;
|
|
917
|
+
}
|
|
918
|
+
}
|
|
919
|
+
try {
|
|
920
|
+
const result = restoreLocalResetBackup(getDbPath(), name);
|
|
921
|
+
logger.info(`Restored local database from ${result.restoredFrom.name}`);
|
|
922
|
+
if (result.safetyBackup) {
|
|
923
|
+
logger.info(`Previous local database safety backup: ${result.safetyBackup.dir}`);
|
|
924
|
+
}
|
|
925
|
+
} catch (err) {
|
|
926
|
+
logger.error(err instanceof Error ? err.message : String(err));
|
|
927
|
+
process.exit(EXIT_ERROR);
|
|
928
|
+
}
|
|
929
|
+
});
|
|
930
|
+
|
|
931
|
+
// src/commands/delete.ts
|
|
932
|
+
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
933
|
Examples:
|
|
814
934
|
unforgit delete abc12345 Soft delete (can be restored)
|
|
815
|
-
unforgit delete abc12345 --hard Permanent delete
|
|
935
|
+
unforgit delete abc12345 --hard Permanent delete, with local backup by default
|
|
936
|
+
unforgit delete abc12345 --hard --no-backup
|
|
816
937
|
unforgit delete abc12345 --remote Delete on remote server`).action(async (id, opts) => {
|
|
817
938
|
if (opts.hard && !opts.force) {
|
|
818
939
|
const confirmed = await confirm(
|
|
@@ -838,7 +959,21 @@ Examples:
|
|
|
838
959
|
}
|
|
839
960
|
return;
|
|
840
961
|
}
|
|
841
|
-
const
|
|
962
|
+
const dbPath = getDbPath();
|
|
963
|
+
if (opts.hard && opts.backup !== false) {
|
|
964
|
+
try {
|
|
965
|
+
const backup = createLocalDatabaseBackup(dbPath, "hard-delete");
|
|
966
|
+
if (backup) {
|
|
967
|
+
logger.info(`Created local hard-delete backup: ${backup.dir}`);
|
|
968
|
+
}
|
|
969
|
+
} catch (err) {
|
|
970
|
+
logger.error(
|
|
971
|
+
`Failed to create local hard-delete backup: ${err instanceof Error ? err.message : String(err)}`
|
|
972
|
+
);
|
|
973
|
+
process.exit(EXIT_ERROR);
|
|
974
|
+
}
|
|
975
|
+
}
|
|
976
|
+
const store = new LocalStore(dbPath);
|
|
842
977
|
try {
|
|
843
978
|
let ok;
|
|
844
979
|
if (opts.hard) {
|
|
@@ -859,7 +994,7 @@ Examples:
|
|
|
859
994
|
store.close();
|
|
860
995
|
}
|
|
861
996
|
});
|
|
862
|
-
var restoreCommand = new
|
|
997
|
+
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
998
|
if (opts.remote) {
|
|
864
999
|
const config = loadConfig();
|
|
865
1000
|
const client = new RemoteClient(config.remote.url);
|
|
@@ -888,11 +1023,11 @@ var restoreCommand = new Command8("restore").description("Restore a soft-deleted
|
|
|
888
1023
|
});
|
|
889
1024
|
|
|
890
1025
|
// src/commands/web.ts
|
|
891
|
-
import { Command as
|
|
1026
|
+
import { Command as Command10 } from "commander";
|
|
892
1027
|
import { spawn } from "child_process";
|
|
893
|
-
import
|
|
894
|
-
import
|
|
895
|
-
var webCommand = new
|
|
1028
|
+
import path3 from "path";
|
|
1029
|
+
import fs4 from "fs";
|
|
1030
|
+
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
1031
|
const cwd4 = process.cwd();
|
|
897
1032
|
if (!isInitialized(cwd4)) {
|
|
898
1033
|
logger.error("Unforgit not initialized in this directory. Run 'unforgit init' first.");
|
|
@@ -908,9 +1043,9 @@ var webCommand = new Command9("web").description("Start the Unforgit web dashboa
|
|
|
908
1043
|
UNFORGIT_WORKSPACE: cwd4,
|
|
909
1044
|
PORT: opts.port
|
|
910
1045
|
};
|
|
911
|
-
const dotenvPath =
|
|
912
|
-
if (
|
|
913
|
-
const content =
|
|
1046
|
+
const dotenvPath = path3.join(cwd4, ".env");
|
|
1047
|
+
if (fs4.existsSync(dotenvPath)) {
|
|
1048
|
+
const content = fs4.readFileSync(dotenvPath, "utf-8");
|
|
914
1049
|
for (const line of content.split("\n")) {
|
|
915
1050
|
const match = line.match(/^\s*([^#=]+?)\s*=\s*(.+?)\s*$/);
|
|
916
1051
|
if (match) {
|
|
@@ -920,11 +1055,11 @@ var webCommand = new Command9("web").description("Start the Unforgit web dashboa
|
|
|
920
1055
|
}
|
|
921
1056
|
logger.info(`Starting Unforgit web dashboard on port ${opts.port}...`);
|
|
922
1057
|
logger.info(`Workspace: ${cwd4}`);
|
|
923
|
-
const hasNextBuild =
|
|
1058
|
+
const hasNextBuild = fs4.existsSync(path3.join(webDir, ".next"));
|
|
924
1059
|
const cmd = hasNextBuild ? "next" : "next";
|
|
925
1060
|
const args = hasNextBuild ? ["start", "-p", opts.port] : ["dev", "-p", opts.port];
|
|
926
|
-
const nextBin =
|
|
927
|
-
const finalCmd =
|
|
1061
|
+
const nextBin = path3.join(webDir, "node_modules", ".bin", "next");
|
|
1062
|
+
const finalCmd = fs4.existsSync(nextBin) ? nextBin : cmd;
|
|
928
1063
|
const child = spawn(finalCmd, args, {
|
|
929
1064
|
cwd: webDir,
|
|
930
1065
|
env,
|
|
@@ -947,12 +1082,12 @@ var webCommand = new Command9("web").description("Start the Unforgit web dashboa
|
|
|
947
1082
|
});
|
|
948
1083
|
function findWebDir() {
|
|
949
1084
|
const candidates = [
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
1085
|
+
path3.resolve(import.meta.dirname, "../../../web"),
|
|
1086
|
+
path3.resolve(import.meta.dirname, "../../web"),
|
|
1087
|
+
path3.join(process.cwd(), "web")
|
|
953
1088
|
];
|
|
954
1089
|
for (const dir of candidates) {
|
|
955
|
-
if (
|
|
1090
|
+
if (fs4.existsSync(dir) && fs4.existsSync(path3.join(dir, "package.json"))) {
|
|
956
1091
|
return dir;
|
|
957
1092
|
}
|
|
958
1093
|
}
|
|
@@ -960,14 +1095,14 @@ function findWebDir() {
|
|
|
960
1095
|
}
|
|
961
1096
|
|
|
962
1097
|
// src/commands/link.ts
|
|
963
|
-
import { Command as
|
|
1098
|
+
import { Command as Command11 } from "commander";
|
|
964
1099
|
var VALID_LINK_TYPES = [
|
|
965
1100
|
"related_to",
|
|
966
1101
|
"derived_from",
|
|
967
1102
|
"contradicts",
|
|
968
1103
|
"depends_on"
|
|
969
1104
|
];
|
|
970
|
-
var linkCommand = new
|
|
1105
|
+
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
1106
|
"--type <link-type>",
|
|
972
1107
|
"Link type (related_to, derived_from, contradicts, depends_on)"
|
|
973
1108
|
).option("--remote", "Create link on remote").addHelpText("after", `
|
|
@@ -1037,7 +1172,7 @@ Examples:
|
|
|
1037
1172
|
store.close();
|
|
1038
1173
|
}
|
|
1039
1174
|
});
|
|
1040
|
-
var unlinkCommand = new
|
|
1175
|
+
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
1176
|
"--type <link-type>",
|
|
1042
1177
|
"Link type (related_to, derived_from, contradicts, depends_on)"
|
|
1043
1178
|
).option("--remote", "Remove link on remote").action(async (sourceId, targetId, opts) => {
|
|
@@ -1077,7 +1212,7 @@ var unlinkCommand = new Command10("unlink").description("Remove a link between t
|
|
|
1077
1212
|
store.close();
|
|
1078
1213
|
}
|
|
1079
1214
|
});
|
|
1080
|
-
var linksCommand = new
|
|
1215
|
+
var linksCommand = new Command11("links").description("List all links for a memory").argument("<memory-id>", "Memory ID to get links for").option(
|
|
1081
1216
|
"--type <link-type>",
|
|
1082
1217
|
"Filter by link type (related_to, derived_from, contradicts, depends_on)"
|
|
1083
1218
|
).option("--remote", "List links on remote").action(async (memoryId, opts) => {
|
|
@@ -1147,9 +1282,9 @@ var linksCommand = new Command10("links").description("List all links for a memo
|
|
|
1147
1282
|
});
|
|
1148
1283
|
|
|
1149
1284
|
// src/commands/merge.ts
|
|
1150
|
-
import { Command as
|
|
1285
|
+
import { Command as Command12 } from "commander";
|
|
1151
1286
|
var cwd = process.cwd();
|
|
1152
|
-
var mergeCommand = new
|
|
1287
|
+
var mergeCommand = new Command12("merge").description(
|
|
1153
1288
|
"Consolidate multiple local memories into one unified memory while preserving history"
|
|
1154
1289
|
).argument("<ids...>", "Memory IDs to consolidate (minimum 2)").requiredOption(
|
|
1155
1290
|
"-t, --text <text>",
|
|
@@ -1221,7 +1356,7 @@ var mergeCommand = new Command11("merge").description(
|
|
|
1221
1356
|
store.close();
|
|
1222
1357
|
}
|
|
1223
1358
|
});
|
|
1224
|
-
var remergeCommand = new
|
|
1359
|
+
var remergeCommand = new Command12("remerge").description(
|
|
1225
1360
|
"Update an existing consolidation with new information or additional sources"
|
|
1226
1361
|
).argument("<consolidation-id>", "ID of existing consolidated memory to update").requiredOption("-t, --text <text>", "Updated consolidated text").option(
|
|
1227
1362
|
"--add <ids>",
|
|
@@ -1260,7 +1395,7 @@ var remergeCommand = new Command11("remerge").description(
|
|
|
1260
1395
|
store.close();
|
|
1261
1396
|
}
|
|
1262
1397
|
});
|
|
1263
|
-
var similarCommand = new
|
|
1398
|
+
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
1399
|
"-k, --limit <n>",
|
|
1265
1400
|
"Max number of similar memories to return",
|
|
1266
1401
|
"10"
|
|
@@ -1324,7 +1459,7 @@ Examples:
|
|
|
1324
1459
|
store.close();
|
|
1325
1460
|
}
|
|
1326
1461
|
});
|
|
1327
|
-
var historyCommand = new
|
|
1462
|
+
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
1463
|
if (!isInitialized(cwd)) {
|
|
1329
1464
|
logger.error("Unforgit not initialized. Run 'unforgit init' first.");
|
|
1330
1465
|
process.exit(EXIT_CONFIG_ERROR);
|
|
@@ -1389,7 +1524,7 @@ var historyCommand = new Command11("history").description("Show consolidation hi
|
|
|
1389
1524
|
});
|
|
1390
1525
|
|
|
1391
1526
|
// src/commands/auto-consolidate.ts
|
|
1392
|
-
import { Command as
|
|
1527
|
+
import { Command as Command13 } from "commander";
|
|
1393
1528
|
import * as readline2 from "readline";
|
|
1394
1529
|
var cwd2 = process.cwd();
|
|
1395
1530
|
function createReadlineInterface() {
|
|
@@ -1405,7 +1540,7 @@ async function askConfirmation(rl, question) {
|
|
|
1405
1540
|
});
|
|
1406
1541
|
});
|
|
1407
1542
|
}
|
|
1408
|
-
var autoConsolidateCommand = new
|
|
1543
|
+
var autoConsolidateCommand = new Command13("auto-consolidate").description(
|
|
1409
1544
|
"Automatically find and consolidate similar memories using AI"
|
|
1410
1545
|
).option(
|
|
1411
1546
|
"--threshold <score>",
|
|
@@ -1570,8 +1705,8 @@ Original memories are preserved with status 'superseded'.`
|
|
|
1570
1705
|
});
|
|
1571
1706
|
|
|
1572
1707
|
// src/commands/unconsolidate.ts
|
|
1573
|
-
import { Command as
|
|
1574
|
-
var unconsolidateCommand = new
|
|
1708
|
+
import { Command as Command14 } from "commander";
|
|
1709
|
+
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
1710
|
Examples:
|
|
1576
1711
|
unforgit unconsolidate abc123 --dry-run Preview what would be restored
|
|
1577
1712
|
unforgit unconsolidate abc123 Revert a consolidation`).action(async (consolidationId, opts) => {
|
|
@@ -1645,8 +1780,8 @@ Examples:
|
|
|
1645
1780
|
});
|
|
1646
1781
|
|
|
1647
1782
|
// src/commands/status.ts
|
|
1648
|
-
import { Command as
|
|
1649
|
-
var statusCommand = new
|
|
1783
|
+
import { Command as Command15 } from "commander";
|
|
1784
|
+
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
1785
|
Examples:
|
|
1651
1786
|
unforgit status Show full sync status
|
|
1652
1787
|
unforgit status -s Short format
|
|
@@ -1664,32 +1799,50 @@ Examples:
|
|
|
1664
1799
|
const remoteUrl = config.remote.url;
|
|
1665
1800
|
const remoteName = "origin";
|
|
1666
1801
|
const pendingPush = store.getPendingPush();
|
|
1802
|
+
const pendingPull = store.getSyncStatesByStatus("pending_pull").map((syncState) => {
|
|
1803
|
+
const memory = store.getById(syncState.memoryId);
|
|
1804
|
+
return memory ? { memory, syncState } : void 0;
|
|
1805
|
+
}).filter((item) => Boolean(item));
|
|
1667
1806
|
const conflicts = store.getConflicts();
|
|
1668
1807
|
const untracked = store.getUntrackedMemories(orgId, repoId);
|
|
1669
1808
|
const summary = store.getSyncSummary(orgId, repoId);
|
|
1670
1809
|
if (isJsonMode()) {
|
|
1810
|
+
const clean = pendingPush.length === 0 && pendingPull.length === 0 && conflicts.length === 0 && untracked.length === 0;
|
|
1671
1811
|
outputJson({
|
|
1672
1812
|
remote: remoteUrl || null,
|
|
1813
|
+
remoteConfigured: Boolean(remoteUrl),
|
|
1814
|
+
synced: summary.synced,
|
|
1673
1815
|
pendingPush: pendingPush.length,
|
|
1816
|
+
pendingPull: pendingPull.length,
|
|
1674
1817
|
conflicts: conflicts.length,
|
|
1675
1818
|
untracked: untracked.length,
|
|
1676
|
-
|
|
1819
|
+
clean,
|
|
1820
|
+
recommendations: buildRecommendations(Boolean(remoteUrl), pendingPush.length, pendingPull.length, conflicts.length, untracked.length),
|
|
1821
|
+
details: {
|
|
1822
|
+
pendingPush: pendingPush.map(toDetailsItem),
|
|
1823
|
+
pendingPull: pendingPull.map(toDetailsItem),
|
|
1824
|
+
conflicts: conflicts.map(toDetailsItem),
|
|
1825
|
+
untracked: untracked.map((memory) => ({ id: memory.id, preview: truncate(memory.text, 80), status: memory.status }))
|
|
1826
|
+
}
|
|
1677
1827
|
});
|
|
1678
1828
|
return;
|
|
1679
1829
|
}
|
|
1680
1830
|
if (opts.short) {
|
|
1681
|
-
printShortStatus(pendingPush, conflicts, untracked);
|
|
1831
|
+
printShortStatus(pendingPush, pendingPull, conflicts, untracked);
|
|
1682
1832
|
} else {
|
|
1683
|
-
printLongStatus(remoteName, remoteUrl, pendingPush, conflicts, untracked, summary);
|
|
1833
|
+
printLongStatus(remoteName, remoteUrl, pendingPush, pendingPull, conflicts, untracked, summary);
|
|
1684
1834
|
}
|
|
1685
1835
|
} finally {
|
|
1686
1836
|
store.close();
|
|
1687
1837
|
}
|
|
1688
1838
|
});
|
|
1689
|
-
function printShortStatus(pendingPush, conflicts, untracked) {
|
|
1839
|
+
function printShortStatus(pendingPush, pendingPull, conflicts, untracked) {
|
|
1690
1840
|
for (const { memory } of pendingPush) {
|
|
1691
1841
|
logger.info(`M ${memory.id.slice(0, 8)} ${truncate(memory.text, 50)}`);
|
|
1692
1842
|
}
|
|
1843
|
+
for (const { memory } of pendingPull) {
|
|
1844
|
+
logger.info(`P ${memory.id.slice(0, 8)} ${truncate(memory.text, 50)}`);
|
|
1845
|
+
}
|
|
1693
1846
|
for (const { memory } of conflicts) {
|
|
1694
1847
|
logger.info(`C ${memory.id.slice(0, 8)} ${truncate(memory.text, 50)}`);
|
|
1695
1848
|
}
|
|
@@ -1697,15 +1850,15 @@ function printShortStatus(pendingPush, conflicts, untracked) {
|
|
|
1697
1850
|
logger.info(`?? ${memory.id.slice(0, 8)} ${truncate(memory.text, 50)}`);
|
|
1698
1851
|
}
|
|
1699
1852
|
}
|
|
1700
|
-
function printLongStatus(remoteName, remoteUrl, pendingPush, conflicts, untracked, summary) {
|
|
1853
|
+
function printLongStatus(remoteName, remoteUrl, pendingPush, pendingPull, conflicts, untracked, summary) {
|
|
1701
1854
|
if (remoteUrl) {
|
|
1702
1855
|
logger.info(`Remote '${remoteName}' at ${remoteUrl}`);
|
|
1703
1856
|
} else {
|
|
1704
1857
|
logger.info("No remote configured. Use 'unforgit remote add origin <url>' to add one.");
|
|
1705
1858
|
}
|
|
1706
1859
|
logger.info("");
|
|
1707
|
-
if (pendingPush.length === 0 && conflicts.length === 0 && untracked.length === 0) {
|
|
1708
|
-
logger.info("Nothing to push, working tree clean");
|
|
1860
|
+
if (pendingPush.length === 0 && pendingPull.length === 0 && conflicts.length === 0 && untracked.length === 0) {
|
|
1861
|
+
logger.info("Nothing to push or pull, working tree clean");
|
|
1709
1862
|
if (summary.synced > 0) {
|
|
1710
1863
|
logger.info(` ${summary.synced} memories synced with remote`);
|
|
1711
1864
|
}
|
|
@@ -1721,6 +1874,15 @@ function printLongStatus(remoteName, remoteUrl, pendingPush, conflicts, untracke
|
|
|
1721
1874
|
}
|
|
1722
1875
|
logger.info("");
|
|
1723
1876
|
}
|
|
1877
|
+
if (pendingPull.length > 0) {
|
|
1878
|
+
logger.info("Changes to be pulled:");
|
|
1879
|
+
logger.info(' (use "unforgit pull" to sync from remote)');
|
|
1880
|
+
logger.info("");
|
|
1881
|
+
for (const { memory } of pendingPull) {
|
|
1882
|
+
logger.info(` remote update: ${memory.id.slice(0, 8)}... "${truncate(memory.text, 40)}"`);
|
|
1883
|
+
}
|
|
1884
|
+
logger.info("");
|
|
1885
|
+
}
|
|
1724
1886
|
if (conflicts.length > 0) {
|
|
1725
1887
|
logger.info("Conflicts:");
|
|
1726
1888
|
logger.info(' (use "unforgit push --force" to overwrite remote or "unforgit pull --force" to accept remote)');
|
|
@@ -1740,13 +1902,43 @@ function printLongStatus(remoteName, remoteUrl, pendingPush, conflicts, untracke
|
|
|
1740
1902
|
}
|
|
1741
1903
|
logger.info("");
|
|
1742
1904
|
}
|
|
1743
|
-
const total = pendingPush.length + conflicts.length + untracked.length;
|
|
1905
|
+
const total = pendingPush.length + pendingPull.length + conflicts.length + untracked.length;
|
|
1744
1906
|
logger.info(`${total} change(s) pending`);
|
|
1745
1907
|
}
|
|
1908
|
+
function buildRecommendations(remoteConfigured, pendingPush, pendingPull, conflicts, untracked) {
|
|
1909
|
+
const recommendations = [];
|
|
1910
|
+
if (!remoteConfigured && (pendingPush > 0 || pendingPull > 0 || untracked > 0)) {
|
|
1911
|
+
recommendations.push("Configure a remote with 'unforgit remote add origin <url>' or keep this repository intentionally local-only.");
|
|
1912
|
+
return recommendations;
|
|
1913
|
+
}
|
|
1914
|
+
if (conflicts > 0) {
|
|
1915
|
+
recommendations.push("Review conflicts, then run 'unforgit pull --force' to accept remote or 'unforgit push --force' to keep local.");
|
|
1916
|
+
}
|
|
1917
|
+
if (pendingPush > 0 || untracked > 0) {
|
|
1918
|
+
recommendations.push("Run 'unforgit push' to publish local memory changes.");
|
|
1919
|
+
}
|
|
1920
|
+
if (pendingPull > 0) {
|
|
1921
|
+
recommendations.push("Run 'unforgit pull' to fetch remote memory changes.");
|
|
1922
|
+
}
|
|
1923
|
+
return recommendations;
|
|
1924
|
+
}
|
|
1925
|
+
function toDetailsItem(item) {
|
|
1926
|
+
const { memory, syncState } = item;
|
|
1927
|
+
return {
|
|
1928
|
+
id: memory.id,
|
|
1929
|
+
preview: truncate(memory.text, 80),
|
|
1930
|
+
status: memory.status,
|
|
1931
|
+
syncStatus: syncState.syncStatus,
|
|
1932
|
+
localVersion: syncState.localVersion,
|
|
1933
|
+
remoteVersion: syncState.remoteVersion,
|
|
1934
|
+
lastPushedAt: syncState.lastPushedAt?.toISOString(),
|
|
1935
|
+
lastPulledAt: syncState.lastPulledAt?.toISOString()
|
|
1936
|
+
};
|
|
1937
|
+
}
|
|
1746
1938
|
|
|
1747
1939
|
// src/commands/push.ts
|
|
1748
|
-
import { Command as
|
|
1749
|
-
var pushCommand = new
|
|
1940
|
+
import { Command as Command16 } from "commander";
|
|
1941
|
+
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
1942
|
if (!isInitialized()) {
|
|
1751
1943
|
logger.fatal("not an unforgit repository");
|
|
1752
1944
|
process.exit(EXIT_CONFIG_ERROR);
|
|
@@ -1890,8 +2082,8 @@ Total: ${allToPush.length} memories, ${supersededToSync.length} status updates,
|
|
|
1890
2082
|
});
|
|
1891
2083
|
|
|
1892
2084
|
// src/commands/pull.ts
|
|
1893
|
-
import { Command as
|
|
1894
|
-
var pullCommand = new
|
|
2085
|
+
import { Command as Command17 } from "commander";
|
|
2086
|
+
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
2087
|
if (!isInitialized()) {
|
|
1896
2088
|
logger.fatal("not an unforgit repository");
|
|
1897
2089
|
process.exit(EXIT_CONFIG_ERROR);
|
|
@@ -2036,8 +2228,8 @@ Would pull:`);
|
|
|
2036
2228
|
});
|
|
2037
2229
|
|
|
2038
2230
|
// src/commands/remote.ts
|
|
2039
|
-
import { Command as
|
|
2040
|
-
var remoteCommand = new
|
|
2231
|
+
import { Command as Command18 } from "commander";
|
|
2232
|
+
var remoteCommand = new Command18("remote").description("Manage set of tracked remote repositories").addHelpText("after", `
|
|
2041
2233
|
Examples:
|
|
2042
2234
|
unforgit remote List remotes
|
|
2043
2235
|
unforgit remote add origin <url> Add a remote
|
|
@@ -2057,7 +2249,7 @@ Examples:
|
|
|
2057
2249
|
logger.info(`${name} ${remote.url}`);
|
|
2058
2250
|
}
|
|
2059
2251
|
});
|
|
2060
|
-
var remoteAddCommand = new
|
|
2252
|
+
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
2253
|
if (!isInitialized()) {
|
|
2062
2254
|
logger.fatal("not an unforgit repository");
|
|
2063
2255
|
process.exit(EXIT_CONFIG_ERROR);
|
|
@@ -2077,7 +2269,7 @@ var remoteAddCommand = new Command17("add").description("Add a new remote").argu
|
|
|
2077
2269
|
saveRemotes(config, remotes);
|
|
2078
2270
|
logger.info(`Remote '${name}' added: ${url}`);
|
|
2079
2271
|
});
|
|
2080
|
-
var remoteRemoveCommand = new
|
|
2272
|
+
var remoteRemoveCommand = new Command18("remove").alias("rm").description("Remove a remote").argument("<name>", "Name of the remote to remove").action((name) => {
|
|
2081
2273
|
if (!isInitialized()) {
|
|
2082
2274
|
logger.fatal("not an unforgit repository");
|
|
2083
2275
|
process.exit(EXIT_CONFIG_ERROR);
|
|
@@ -2092,7 +2284,7 @@ var remoteRemoveCommand = new Command17("remove").alias("rm").description("Remov
|
|
|
2092
2284
|
saveRemotes(config, remotes);
|
|
2093
2285
|
logger.info(`Remote '${name}' removed.`);
|
|
2094
2286
|
});
|
|
2095
|
-
var remoteSetUrlCommand = new
|
|
2287
|
+
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
2288
|
if (!isInitialized()) {
|
|
2097
2289
|
logger.fatal("not an unforgit repository");
|
|
2098
2290
|
process.exit(EXIT_CONFIG_ERROR);
|
|
@@ -2108,7 +2300,7 @@ var remoteSetUrlCommand = new Command17("set-url").description("Change the URL f
|
|
|
2108
2300
|
saveRemotes(config, remotes);
|
|
2109
2301
|
logger.info(`Remote '${name}' URL changed to: ${newurl}`);
|
|
2110
2302
|
});
|
|
2111
|
-
var remoteShowCommand = new
|
|
2303
|
+
var remoteShowCommand = new Command18("show").description("Show information about a remote").argument("<name>", "Name of the remote").action((name) => {
|
|
2112
2304
|
if (!isInitialized()) {
|
|
2113
2305
|
logger.fatal("not an unforgit repository");
|
|
2114
2306
|
process.exit(EXIT_CONFIG_ERROR);
|
|
@@ -2158,8 +2350,8 @@ function saveRemotes(config, remotes) {
|
|
|
2158
2350
|
}
|
|
2159
2351
|
|
|
2160
2352
|
// src/commands/log.ts
|
|
2161
|
-
import { Command as
|
|
2162
|
-
var logCommand = new
|
|
2353
|
+
import { Command as Command19 } from "commander";
|
|
2354
|
+
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
2355
|
Examples:
|
|
2164
2356
|
unforgit log Show recent memories
|
|
2165
2357
|
unforgit log --all Include deprecated/superseded
|
|
@@ -2251,8 +2443,8 @@ Examples:
|
|
|
2251
2443
|
});
|
|
2252
2444
|
|
|
2253
2445
|
// src/commands/diff.ts
|
|
2254
|
-
import { Command as
|
|
2255
|
-
var diffCommand = new
|
|
2446
|
+
import { Command as Command20 } from "commander";
|
|
2447
|
+
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
2448
|
Examples:
|
|
2257
2449
|
unforgit diff Show all differences
|
|
2258
2450
|
unforgit diff --stat Show difference statistics only
|
|
@@ -2418,7 +2610,7 @@ function findFullId(store, partialId, orgId, repoId) {
|
|
|
2418
2610
|
}
|
|
2419
2611
|
|
|
2420
2612
|
// src/commands/keys.ts
|
|
2421
|
-
import { Command as
|
|
2613
|
+
import { Command as Command21 } from "commander";
|
|
2422
2614
|
function requireRemote() {
|
|
2423
2615
|
if (!isInitialized()) {
|
|
2424
2616
|
logger.fatal("not an unforgit repository");
|
|
@@ -2438,7 +2630,7 @@ function requireRemote() {
|
|
|
2438
2630
|
}
|
|
2439
2631
|
return { url: config.remote.url, apiKey };
|
|
2440
2632
|
}
|
|
2441
|
-
var keysCommand = new
|
|
2633
|
+
var keysCommand = new Command21("keys").description("Manage API keys for remote authentication");
|
|
2442
2634
|
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
2635
|
Examples:
|
|
2444
2636
|
unforgit keys create --name "CI pipeline" --org my-org
|
|
@@ -2507,8 +2699,8 @@ keysCommand.command("revoke").description("Revoke an API key").argument("<id>",
|
|
|
2507
2699
|
});
|
|
2508
2700
|
|
|
2509
2701
|
// src/commands/auth.ts
|
|
2510
|
-
import { Command as
|
|
2511
|
-
var authCommand = new
|
|
2702
|
+
import { Command as Command22 } from "commander";
|
|
2703
|
+
var authCommand = new Command22("auth").description("Check authentication status for remote server and APIs");
|
|
2512
2704
|
authCommand.command("status").description("Check authentication status").action(async () => {
|
|
2513
2705
|
if (!isInitialized()) {
|
|
2514
2706
|
logger.fatal("not an unforgit repository");
|
|
@@ -2559,8 +2751,8 @@ authCommand.command("status").description("Check authentication status").action(
|
|
|
2559
2751
|
});
|
|
2560
2752
|
|
|
2561
2753
|
// src/commands/config.ts
|
|
2562
|
-
import { Command as
|
|
2563
|
-
var configCommand = new
|
|
2754
|
+
import { Command as Command23 } from "commander";
|
|
2755
|
+
var configCommand = new Command23("config").description("Manage unforgit configuration");
|
|
2564
2756
|
configCommand.command("list").alias("ls").description("List all configuration values").action(() => {
|
|
2565
2757
|
if (!isInitialized()) {
|
|
2566
2758
|
logger.fatal("not an unforgit repository");
|
|
@@ -2721,19 +2913,49 @@ function coerceConfigValue(value) {
|
|
|
2721
2913
|
}
|
|
2722
2914
|
|
|
2723
2915
|
// src/commands/embeddings.ts
|
|
2724
|
-
import { Command as
|
|
2916
|
+
import { Command as Command24 } from "commander";
|
|
2725
2917
|
var cwd3 = process.cwd();
|
|
2726
|
-
|
|
2727
|
-
|
|
2918
|
+
function embeddingCoverage(stats) {
|
|
2919
|
+
return stats.total > 0 ? Number((stats.withEmbedding / stats.total * 100).toFixed(1)) : 0;
|
|
2920
|
+
}
|
|
2921
|
+
function buildBackfillJsonPayload(options) {
|
|
2922
|
+
const statsAfter = options.statsAfter ?? options.statsBefore;
|
|
2923
|
+
const failures = options.failures ?? [];
|
|
2924
|
+
return {
|
|
2925
|
+
dryRun: options.dryRun,
|
|
2926
|
+
model: options.model,
|
|
2927
|
+
...options.provider ? { provider: options.provider } : {},
|
|
2928
|
+
planned: options.planned,
|
|
2929
|
+
processed: options.processed,
|
|
2930
|
+
errors: failures.length,
|
|
2931
|
+
failures,
|
|
2932
|
+
statsBefore: {
|
|
2933
|
+
...options.statsBefore,
|
|
2934
|
+
coverage: embeddingCoverage(options.statsBefore)
|
|
2935
|
+
},
|
|
2936
|
+
statsAfter: {
|
|
2937
|
+
...statsAfter,
|
|
2938
|
+
coverage: embeddingCoverage(statsAfter)
|
|
2939
|
+
},
|
|
2940
|
+
...options.previews ? { memories: options.previews, truncated: Boolean(options.truncated) } : {}
|
|
2941
|
+
};
|
|
2942
|
+
}
|
|
2943
|
+
var embeddingsCommand = new Command24("embeddings").description("Manage memory embeddings for semantic search");
|
|
2944
|
+
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("--provider <provider>", "Embedding provider: auto, local, openai, disabled").option("--model <model>", "Embedding model (defaults to configured provider model)").action(async (opts) => {
|
|
2728
2945
|
if (!isInitialized(cwd3)) {
|
|
2729
2946
|
logger.error("Unforgit not initialized. Run 'unforgit init' first.");
|
|
2730
2947
|
process.exit(EXIT_CONFIG_ERROR);
|
|
2731
2948
|
}
|
|
2732
2949
|
const config = loadConfig(cwd3);
|
|
2733
|
-
const
|
|
2734
|
-
|
|
2735
|
-
|
|
2736
|
-
|
|
2950
|
+
const embeddingConfig = {
|
|
2951
|
+
provider: opts.provider ?? config.embeddings?.provider ?? "auto",
|
|
2952
|
+
model: opts.model ?? config.embeddings?.model,
|
|
2953
|
+
apiKey: process.env.OPENAI_API_KEY
|
|
2954
|
+
};
|
|
2955
|
+
const provider = resolveEmbeddingProvider(embeddingConfig);
|
|
2956
|
+
if (!provider.available && !opts.dryRun) {
|
|
2957
|
+
logger.error(provider.reason ?? "Embedding provider is not available.");
|
|
2958
|
+
logger.error("Use 'unforgit config set embeddings.provider local' for no-key local embeddings.");
|
|
2737
2959
|
process.exit(EXIT_ERROR);
|
|
2738
2960
|
}
|
|
2739
2961
|
const dbPath = getDbPath(cwd3);
|
|
@@ -2741,18 +2963,51 @@ embeddingsCommand.command("backfill").description("Generate embeddings for memor
|
|
|
2741
2963
|
const orgId = config.remote.orgId || "local";
|
|
2742
2964
|
const repoId = config.remote.repoId || "local";
|
|
2743
2965
|
try {
|
|
2744
|
-
const memories = store.getMemoriesWithoutEmbeddings(orgId, repoId
|
|
2966
|
+
const memories = store.getMemoriesWithoutEmbeddings(orgId, repoId, {
|
|
2967
|
+
model: provider.model,
|
|
2968
|
+
provider: provider.provider,
|
|
2969
|
+
dimensions: provider.dimensions
|
|
2970
|
+
});
|
|
2745
2971
|
const stats = store.getEmbeddingStats(orgId, repoId);
|
|
2972
|
+
if (isJsonMode() && opts.dryRun) {
|
|
2973
|
+
outputJson(buildBackfillJsonPayload({
|
|
2974
|
+
dryRun: true,
|
|
2975
|
+
model: provider.model,
|
|
2976
|
+
provider: provider.provider,
|
|
2977
|
+
statsBefore: stats,
|
|
2978
|
+
planned: memories.length,
|
|
2979
|
+
processed: 0,
|
|
2980
|
+
previews: memories.slice(0, 10).map((memory) => ({
|
|
2981
|
+
id: memory.id,
|
|
2982
|
+
textPreview: memory.text.slice(0, 80)
|
|
2983
|
+
})),
|
|
2984
|
+
truncated: memories.length > 10
|
|
2985
|
+
}));
|
|
2986
|
+
return;
|
|
2987
|
+
}
|
|
2746
2988
|
logger.info(`Embedding stats:`);
|
|
2747
2989
|
logger.info(` Total memories: ${stats.total}`);
|
|
2748
2990
|
logger.info(` With embedding: ${stats.withEmbedding}`);
|
|
2749
2991
|
logger.info(` Without embedding: ${stats.withoutEmbedding}`);
|
|
2750
2992
|
logger.info("");
|
|
2751
2993
|
if (memories.length === 0) {
|
|
2994
|
+
if (isJsonMode()) {
|
|
2995
|
+
outputJson(buildBackfillJsonPayload({
|
|
2996
|
+
dryRun: Boolean(opts.dryRun),
|
|
2997
|
+
model: provider.model,
|
|
2998
|
+
provider: provider.provider,
|
|
2999
|
+
statsBefore: stats,
|
|
3000
|
+
planned: 0,
|
|
3001
|
+
processed: 0
|
|
3002
|
+
}));
|
|
3003
|
+
return;
|
|
3004
|
+
}
|
|
2752
3005
|
logger.info("All memories already have embeddings.");
|
|
2753
3006
|
return;
|
|
2754
3007
|
}
|
|
2755
3008
|
logger.info(`Found ${memories.length} memories without embeddings.`);
|
|
3009
|
+
logger.info(`Embedding provider: ${provider.provider}`);
|
|
3010
|
+
logger.info(`Embedding model: ${provider.model}`);
|
|
2756
3011
|
if (opts.dryRun) {
|
|
2757
3012
|
logger.info("\n[Dry run - no changes made]");
|
|
2758
3013
|
logger.info("Would generate embeddings for:");
|
|
@@ -2767,7 +3022,7 @@ embeddingsCommand.command("backfill").description("Generate embeddings for memor
|
|
|
2767
3022
|
const batchSize = parsePositiveInt(opts.batchSize, "batch-size");
|
|
2768
3023
|
const delay = parsePositiveInt(opts.delay, "delay");
|
|
2769
3024
|
let processed = 0;
|
|
2770
|
-
|
|
3025
|
+
const failures = [];
|
|
2771
3026
|
logger.info(`
|
|
2772
3027
|
Generating embeddings (batch size: ${batchSize}, delay: ${delay}ms)...`);
|
|
2773
3028
|
for (let i = 0; i < memories.length; i += batchSize) {
|
|
@@ -2776,15 +3031,19 @@ Generating embeddings (batch size: ${batchSize}, delay: ${delay}ms)...`);
|
|
|
2776
3031
|
batch.map(async (memory) => {
|
|
2777
3032
|
try {
|
|
2778
3033
|
const result = await generateEmbedding(memory.text, {
|
|
2779
|
-
|
|
2780
|
-
model: opts.model
|
|
3034
|
+
...embeddingConfig
|
|
2781
3035
|
});
|
|
2782
|
-
await store.storeEmbedding(memory.id, result.embedding, result.model);
|
|
3036
|
+
await store.storeEmbedding(memory.id, result.embedding, result.model, result.provider);
|
|
2783
3037
|
processed++;
|
|
2784
3038
|
logger.progress(processed, memories.length, "embeddings");
|
|
2785
3039
|
} catch (err) {
|
|
2786
|
-
|
|
2787
|
-
|
|
3040
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
3041
|
+
failures.push({
|
|
3042
|
+
id: memory.id,
|
|
3043
|
+
textPreview: memory.text.slice(0, 80),
|
|
3044
|
+
error: message
|
|
3045
|
+
});
|
|
3046
|
+
logger.error(`${memory.id.slice(0, 8)}: ${message}`);
|
|
2788
3047
|
}
|
|
2789
3048
|
})
|
|
2790
3049
|
);
|
|
@@ -2795,7 +3054,23 @@ Generating embeddings (batch size: ${batchSize}, delay: ${delay}ms)...`);
|
|
|
2795
3054
|
logger.info(`
|
|
2796
3055
|
Backfill complete:`);
|
|
2797
3056
|
logger.info(` Processed: ${processed}`);
|
|
2798
|
-
logger.info(` Errors: ${
|
|
3057
|
+
logger.info(` Errors: ${failures.length}`);
|
|
3058
|
+
const statsAfter = store.getEmbeddingStats(orgId, repoId);
|
|
3059
|
+
if (isJsonMode()) {
|
|
3060
|
+
outputJson(buildBackfillJsonPayload({
|
|
3061
|
+
dryRun: false,
|
|
3062
|
+
model: provider.model,
|
|
3063
|
+
provider: provider.provider,
|
|
3064
|
+
statsBefore: stats,
|
|
3065
|
+
statsAfter,
|
|
3066
|
+
planned: memories.length,
|
|
3067
|
+
processed,
|
|
3068
|
+
failures
|
|
3069
|
+
}));
|
|
3070
|
+
}
|
|
3071
|
+
if (failures.length > 0) {
|
|
3072
|
+
process.exitCode = EXIT_ERROR;
|
|
3073
|
+
}
|
|
2799
3074
|
} finally {
|
|
2800
3075
|
store.close();
|
|
2801
3076
|
}
|
|
@@ -2831,7 +3106,7 @@ Run 'unforgit embeddings backfill' to generate missing embeddings.`);
|
|
|
2831
3106
|
store.close();
|
|
2832
3107
|
}
|
|
2833
3108
|
});
|
|
2834
|
-
embeddingsCommand.command("clear").description("Remove all embeddings (requires regeneration)").option("--yes", "Skip confirmation").action(async (opts) => {
|
|
3109
|
+
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
3110
|
if (!isInitialized(cwd3)) {
|
|
2836
3111
|
logger.error("Unforgit not initialized. Run 'unforgit init' first.");
|
|
2837
3112
|
process.exit(EXIT_CONFIG_ERROR);
|
|
@@ -2842,6 +3117,19 @@ embeddingsCommand.command("clear").description("Remove all embeddings (requires
|
|
|
2842
3117
|
return;
|
|
2843
3118
|
}
|
|
2844
3119
|
const dbPath = getDbPath(cwd3);
|
|
3120
|
+
if (opts.backup !== false) {
|
|
3121
|
+
try {
|
|
3122
|
+
const backup = createLocalDatabaseBackup(dbPath, "embeddings-clear");
|
|
3123
|
+
if (backup) {
|
|
3124
|
+
logger.info(`Created local embeddings backup: ${backup.dir}`);
|
|
3125
|
+
}
|
|
3126
|
+
} catch (err) {
|
|
3127
|
+
logger.error(
|
|
3128
|
+
`Failed to create local embeddings backup: ${err instanceof Error ? err.message : String(err)}`
|
|
3129
|
+
);
|
|
3130
|
+
process.exit(EXIT_ERROR);
|
|
3131
|
+
}
|
|
3132
|
+
}
|
|
2845
3133
|
const store = new LocalStore(dbPath);
|
|
2846
3134
|
try {
|
|
2847
3135
|
const deleted = store.clearEmbeddings();
|
|
@@ -2853,121 +3141,6 @@ embeddingsCommand.command("clear").description("Remove all embeddings (requires
|
|
|
2853
3141
|
|
|
2854
3142
|
// src/commands/reset.ts
|
|
2855
3143
|
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
3144
|
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
3145
|
Examples:
|
|
2973
3146
|
unforgit reset Reset both local and remote
|
|
@@ -3211,22 +3384,45 @@ var doctorCommand = new Command27("doctor").description("Check system health and
|
|
|
3211
3384
|
status: "ok",
|
|
3212
3385
|
message: `${memoryStats.total} memories (${memoryStats.byType.episodic} episodic, ${memoryStats.byType.semantic} semantic, ${memoryStats.byType.procedural} procedural)`
|
|
3213
3386
|
});
|
|
3387
|
+
const provider = resolveEmbeddingProvider({
|
|
3388
|
+
provider: config.embeddings?.provider ?? "auto",
|
|
3389
|
+
model: config.embeddings?.model,
|
|
3390
|
+
apiKey: process.env.OPENAI_API_KEY
|
|
3391
|
+
});
|
|
3392
|
+
results.push({
|
|
3393
|
+
check: "embedding-provider",
|
|
3394
|
+
status: provider.available ? "ok" : "warn",
|
|
3395
|
+
message: `${provider.provider} embeddings (${provider.model}, ${provider.dimensions} dimensions)${provider.reason ? `: ${provider.reason}` : ""}`,
|
|
3396
|
+
details: {
|
|
3397
|
+
provider: provider.provider,
|
|
3398
|
+
model: provider.model,
|
|
3399
|
+
dimensions: provider.dimensions,
|
|
3400
|
+
available: provider.available
|
|
3401
|
+
}
|
|
3402
|
+
});
|
|
3214
3403
|
const stats = store.getEmbeddingStats(orgId, repoId);
|
|
3404
|
+
const incompatibleOrMissing = store.getMemoriesWithoutEmbeddings(orgId, repoId, {
|
|
3405
|
+
model: provider.model,
|
|
3406
|
+
provider: provider.provider,
|
|
3407
|
+
dimensions: provider.dimensions
|
|
3408
|
+
}).length;
|
|
3215
3409
|
if (stats.total === 0) {
|
|
3216
3410
|
results.push({ check: "embeddings", status: "ok", message: "No memories yet" });
|
|
3217
|
-
} else if (
|
|
3218
|
-
results.push({ check: "embeddings", status: "ok", message: `All ${stats.total} memories have embeddings` });
|
|
3411
|
+
} else if (incompatibleOrMissing === 0) {
|
|
3412
|
+
results.push({ check: "embeddings", status: "ok", message: `All ${stats.total} memories have compatible embeddings` });
|
|
3219
3413
|
} else {
|
|
3220
|
-
const
|
|
3414
|
+
const compatible = stats.total - incompatibleOrMissing;
|
|
3415
|
+
const pct = (compatible / stats.total * 100).toFixed(1);
|
|
3221
3416
|
results.push({
|
|
3222
3417
|
check: "embeddings",
|
|
3223
3418
|
status: "warn",
|
|
3224
|
-
message: `${
|
|
3419
|
+
message: `${incompatibleOrMissing}/${stats.total} memories lack compatible embeddings for ${provider.provider}/${provider.model} (${pct}% coverage). Run 'unforgit embeddings backfill'`,
|
|
3225
3420
|
fix: "Run 'unforgit embeddings backfill'."
|
|
3226
3421
|
});
|
|
3227
3422
|
}
|
|
3228
3423
|
const pendingPush = store.getPendingPush();
|
|
3229
3424
|
const conflicts = store.getConflicts();
|
|
3425
|
+
const syncSummary = store.getSyncSummary(orgId, repoId);
|
|
3230
3426
|
const unsyncedTombstones = store.getUnsyncedTombstones(orgId, repoId);
|
|
3231
3427
|
if (unsyncedTombstones.length > 0) {
|
|
3232
3428
|
results.push({
|
|
@@ -3243,16 +3439,22 @@ var doctorCommand = new Command27("doctor").description("Check system health and
|
|
|
3243
3439
|
check: "sync",
|
|
3244
3440
|
status: "warn",
|
|
3245
3441
|
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."
|
|
3442
|
+
fix: "Resolve conflicts with 'unforgit pull --force' or 'unforgit push --force' after reviewing the desired source of truth.",
|
|
3443
|
+
details: syncSummary
|
|
3247
3444
|
});
|
|
3248
|
-
} else if (pendingPush.length > 0) {
|
|
3445
|
+
} else if (pendingPush.length > 0 || syncSummary.pendingPull > 0) {
|
|
3446
|
+
const parts = [];
|
|
3447
|
+
if (pendingPush.length > 0) parts.push(`${pendingPush.length} memory(s) pending push`);
|
|
3448
|
+
if (syncSummary.pendingPull > 0) parts.push(`${syncSummary.pendingPull} memory(s) pending pull`);
|
|
3249
3449
|
results.push({
|
|
3250
3450
|
check: "sync",
|
|
3251
|
-
status: "
|
|
3252
|
-
message:
|
|
3451
|
+
status: "warn",
|
|
3452
|
+
message: parts.join(", "),
|
|
3453
|
+
fix: "Run 'unforgit push' to publish local memory changes, or configure/disable sync if this repository is intentionally local-only.",
|
|
3454
|
+
details: syncSummary
|
|
3253
3455
|
});
|
|
3254
3456
|
} else {
|
|
3255
|
-
results.push({ check: "sync", status: "ok", message: "Sync state clean" });
|
|
3457
|
+
results.push({ check: "sync", status: "ok", message: "Sync state clean", details: syncSummary });
|
|
3256
3458
|
}
|
|
3257
3459
|
} finally {
|
|
3258
3460
|
store.close();
|
|
@@ -3300,11 +3502,12 @@ var doctorCommand = new Command27("doctor").description("Check system health and
|
|
|
3300
3502
|
});
|
|
3301
3503
|
}
|
|
3302
3504
|
} catch (err) {
|
|
3505
|
+
const localhostRemote = isLocalhostUrl(config.remote.url);
|
|
3303
3506
|
results.push({
|
|
3304
3507
|
check: "remote",
|
|
3305
3508
|
status: "error",
|
|
3306
|
-
message: `Cannot connect to ${config.remote.url}: ${err instanceof Error ? err.message : err}`,
|
|
3307
|
-
fix: "Start the Unforgit API server or update remote.url in unforgit.yaml."
|
|
3509
|
+
message: localhostRemote ? `Cannot connect to local remote API at ${config.remote.url}: ${err instanceof Error ? err.message : err}. Local memory and local embeddings still work; only remote sync is unavailable.` : `Cannot connect to ${config.remote.url}: ${err instanceof Error ? err.message : err}`,
|
|
3510
|
+
fix: localhostRemote ? "remote.url points to localhost. Start the Unforgit API server on this machine/port, update remote.url, or disable sync if this repository is intentionally local-only." : "Start the Unforgit API server or update remote.url in unforgit.yaml."
|
|
3308
3511
|
});
|
|
3309
3512
|
}
|
|
3310
3513
|
} else {
|
|
@@ -3315,15 +3518,19 @@ var doctorCommand = new Command27("doctor").description("Check system health and
|
|
|
3315
3518
|
fix: "Run 'unforgit remote add origin <url>' if this repo should sync remotely."
|
|
3316
3519
|
});
|
|
3317
3520
|
}
|
|
3318
|
-
|
|
3319
|
-
|
|
3320
|
-
results.push({ check: "openai", status: "ok", message: "OpenAI API key configured ([REDACTED])" });
|
|
3521
|
+
if (isOpenAIConfigured()) {
|
|
3522
|
+
results.push({ check: "openai", status: "ok", message: "OpenAI API key configured ([REDACTED]) for optional cloud AI features" });
|
|
3321
3523
|
} else {
|
|
3524
|
+
const embeddingProvider = resolveEmbeddingProvider({
|
|
3525
|
+
provider: config.embeddings?.provider ?? "auto",
|
|
3526
|
+
model: config.embeddings?.model
|
|
3527
|
+
});
|
|
3528
|
+
const message = embeddingProvider.provider === "local" ? "No OpenAI API key configured; local embeddings are active. OpenAI is only needed for OpenAI-backed embeddings or AI consolidation." : "No OpenAI API key. OpenAI-backed embeddings and AI consolidation require it.";
|
|
3322
3529
|
results.push({
|
|
3323
3530
|
check: "openai",
|
|
3324
|
-
status: "warn",
|
|
3325
|
-
message
|
|
3326
|
-
fix: "
|
|
3531
|
+
status: embeddingProvider.provider === "local" ? "ok" : "warn",
|
|
3532
|
+
message,
|
|
3533
|
+
fix: embeddingProvider.provider === "local" ? void 0 : "Set OPENAI_API_KEY or configure embeddings.provider=local."
|
|
3327
3534
|
});
|
|
3328
3535
|
}
|
|
3329
3536
|
} catch (err) {
|
|
@@ -3352,6 +3559,14 @@ function summarize(results) {
|
|
|
3352
3559
|
function buildPayload(results) {
|
|
3353
3560
|
return { summary: summarize(results), results };
|
|
3354
3561
|
}
|
|
3562
|
+
function isLocalhostUrl(value) {
|
|
3563
|
+
try {
|
|
3564
|
+
const url = new URL(value);
|
|
3565
|
+
return ["localhost", "127.0.0.1", "::1", "[::1]"].includes(url.hostname);
|
|
3566
|
+
} catch {
|
|
3567
|
+
return false;
|
|
3568
|
+
}
|
|
3569
|
+
}
|
|
3355
3570
|
function exitForResults(results) {
|
|
3356
3571
|
if (results.some((r) => r.status === "error")) {
|
|
3357
3572
|
process.exit(EXIT_ERROR);
|