unforgit 0.1.0 → 0.3.1
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 +304 -76
- package/dist/index.js.map +1 -1
- package/package.json +13 -4
package/dist/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// src/index.ts
|
|
4
|
-
import { Command as
|
|
4
|
+
import { Command as Command29 } from "commander";
|
|
5
5
|
|
|
6
6
|
// src/commands/init.ts
|
|
7
7
|
import fs2 from "fs";
|
|
@@ -156,46 +156,66 @@ Trigger a save with the \`unforgit_add\` tool immediately after:
|
|
|
156
156
|
- Do NOT save trivial changes, obvious things, or info already in the codebase docs
|
|
157
157
|
- Quality over quantity \u2014 only save what's genuinely useful for future sessions
|
|
158
158
|
- **Always write memory text in English**, regardless of the language the user is speaking`;
|
|
159
|
-
function fileContainsUnforgit(filePath) {
|
|
160
|
-
if (!fs.existsSync(filePath)) return false;
|
|
161
|
-
return fs.readFileSync(filePath, "utf-8").includes(UNFORGIT_MARKER);
|
|
162
|
-
}
|
|
163
159
|
function upsertJsonMcp(filePath, serverKey, mcpEntry) {
|
|
164
160
|
const dir = path.dirname(filePath);
|
|
165
161
|
fs.mkdirSync(dir, { recursive: true });
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
fs.
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
162
|
+
const fd = fs.openSync(filePath, "a+");
|
|
163
|
+
try {
|
|
164
|
+
const raw = fs.readFileSync(fd, "utf-8");
|
|
165
|
+
if (raw.trim().length === 0) {
|
|
166
|
+
const config = { [serverKey]: { unforgit: mcpEntry } };
|
|
167
|
+
fs.writeSync(fd, JSON.stringify(config, null, 2) + "\n", 0, "utf-8");
|
|
168
|
+
return { path: filePath, action: "created" };
|
|
169
|
+
}
|
|
170
|
+
const existing = JSON.parse(raw);
|
|
171
|
+
const servers = existing[serverKey];
|
|
172
|
+
if (servers?.unforgit) {
|
|
173
|
+
return { path: filePath, action: "exists" };
|
|
174
|
+
}
|
|
175
|
+
existing[serverKey] = existing[serverKey] || {};
|
|
176
|
+
existing[serverKey].unforgit = mcpEntry;
|
|
177
|
+
fs.ftruncateSync(fd, 0);
|
|
178
|
+
fs.writeSync(fd, JSON.stringify(existing, null, 2) + "\n", 0, "utf-8");
|
|
179
|
+
return { path: filePath, action: "updated" };
|
|
180
|
+
} finally {
|
|
181
|
+
fs.closeSync(fd);
|
|
182
|
+
}
|
|
184
183
|
}
|
|
185
184
|
function appendOrCreateMarkdown(filePath, content) {
|
|
186
185
|
const dir = path.dirname(filePath);
|
|
187
186
|
fs.mkdirSync(dir, { recursive: true });
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
187
|
+
const fd = fs.openSync(filePath, "a+");
|
|
188
|
+
try {
|
|
189
|
+
const existing = fs.readFileSync(fd, "utf-8");
|
|
190
|
+
if (existing.length === 0) {
|
|
191
|
+
fs.writeSync(fd, content + "\n", 0, "utf-8");
|
|
192
|
+
return { path: filePath, action: "created" };
|
|
193
|
+
}
|
|
194
|
+
if (existing.includes(UNFORGIT_MARKER)) {
|
|
195
|
+
return { path: filePath, action: "exists" };
|
|
196
|
+
}
|
|
197
|
+
const separator = existing.endsWith("\n") ? "\n" : "\n\n";
|
|
198
|
+
fs.writeSync(fd, separator + content + "\n", void 0, "utf-8");
|
|
199
|
+
return { path: filePath, action: "updated" };
|
|
200
|
+
} finally {
|
|
201
|
+
fs.closeSync(fd);
|
|
191
202
|
}
|
|
192
|
-
|
|
193
|
-
|
|
203
|
+
}
|
|
204
|
+
function replaceMarkdownIfMissingMarker(filePath, content) {
|
|
205
|
+
const dir = path.dirname(filePath);
|
|
206
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
207
|
+
const fd = fs.openSync(filePath, "a+");
|
|
208
|
+
try {
|
|
209
|
+
const existing = fs.readFileSync(fd, "utf-8");
|
|
210
|
+
if (existing.includes(UNFORGIT_MARKER)) {
|
|
211
|
+
return { path: filePath, action: "exists" };
|
|
212
|
+
}
|
|
213
|
+
fs.ftruncateSync(fd, 0);
|
|
214
|
+
fs.writeSync(fd, content, 0, "utf-8");
|
|
215
|
+
return { path: filePath, action: existing.length === 0 ? "created" : "updated" };
|
|
216
|
+
} finally {
|
|
217
|
+
fs.closeSync(fd);
|
|
194
218
|
}
|
|
195
|
-
const existing = fs.readFileSync(filePath, "utf-8");
|
|
196
|
-
const separator = existing.endsWith("\n") ? "\n" : "\n\n";
|
|
197
|
-
fs.writeFileSync(filePath, existing + separator + content + "\n", "utf-8");
|
|
198
|
-
return { path: filePath, action: "updated" };
|
|
199
219
|
}
|
|
200
220
|
var CURSOR_RULE_CONTENT = `---
|
|
201
221
|
description: Auto-manage repository memory with Unforgit via MCP tools
|
|
@@ -207,14 +227,7 @@ ${MEMORY_INSTRUCTIONS}
|
|
|
207
227
|
function setupCursor(cwd4) {
|
|
208
228
|
const rulesDir = path.join(cwd4, ".cursor", "rules");
|
|
209
229
|
const rulePath = path.join(rulesDir, "unforgit-memory.mdc");
|
|
210
|
-
|
|
211
|
-
if (fileContainsUnforgit(rulePath)) {
|
|
212
|
-
rules = { path: rulePath, action: "exists" };
|
|
213
|
-
} else {
|
|
214
|
-
fs.mkdirSync(rulesDir, { recursive: true });
|
|
215
|
-
fs.writeFileSync(rulePath, CURSOR_RULE_CONTENT, "utf-8");
|
|
216
|
-
rules = { path: rulePath, action: "created" };
|
|
217
|
-
}
|
|
230
|
+
const rules = replaceMarkdownIfMissingMarker(rulePath, CURSOR_RULE_CONTENT);
|
|
218
231
|
const mcpPath = path.join(cwd4, ".cursor", "mcp.json");
|
|
219
232
|
const mcp = upsertJsonMcp(mcpPath, "mcpServers", {
|
|
220
233
|
command: "unforgit-mcp",
|
|
@@ -2895,16 +2908,133 @@ embeddingsCommand.command("clear").description("Remove all embeddings (requires
|
|
|
2895
2908
|
});
|
|
2896
2909
|
|
|
2897
2910
|
// src/commands/reset.ts
|
|
2898
|
-
import { Command as
|
|
2899
|
-
import { loadConfig as loadConfig21, getDbPath as
|
|
2911
|
+
import { Command as Command25 } from "commander";
|
|
2912
|
+
import { loadConfig as loadConfig21, getDbPath as getDbPath19 } from "unforgit-config";
|
|
2900
2913
|
import { LocalStore as LocalStore18 } from "unforgit-db";
|
|
2901
2914
|
import { RemoteClient as RemoteClient12 } from "unforgit-config";
|
|
2902
|
-
|
|
2915
|
+
|
|
2916
|
+
// src/commands/backups.ts
|
|
2917
|
+
import fs4 from "fs";
|
|
2918
|
+
import path3 from "path";
|
|
2919
|
+
import { Command as Command24 } from "commander";
|
|
2920
|
+
import { getDbPath as getDbPath18 } from "unforgit-config";
|
|
2921
|
+
function formatBackupTimestamp(date) {
|
|
2922
|
+
return date.toISOString().replace(/[-:]/g, "").replace("T", "-").replace(/\.\d{3}Z$/, "");
|
|
2923
|
+
}
|
|
2924
|
+
function backupRootForDb(dbPath) {
|
|
2925
|
+
return path3.join(path3.dirname(dbPath), "backups");
|
|
2926
|
+
}
|
|
2927
|
+
function describeBackup(dir) {
|
|
2928
|
+
const files = fs4.readdirSync(dir).filter((file) => file === "local.db" || file === "local.db-wal" || file === "local.db-shm").sort();
|
|
2929
|
+
const sizeBytes = files.reduce((total, file) => total + fs4.statSync(path3.join(dir, file)).size, 0);
|
|
2930
|
+
const stat = fs4.statSync(dir);
|
|
2931
|
+
return {
|
|
2932
|
+
name: path3.basename(dir),
|
|
2933
|
+
dir,
|
|
2934
|
+
files,
|
|
2935
|
+
sizeBytes,
|
|
2936
|
+
createdAt: stat.mtime.toISOString()
|
|
2937
|
+
};
|
|
2938
|
+
}
|
|
2939
|
+
function resolveBackupDir(backupRoot, backupName) {
|
|
2940
|
+
if (backupName !== path3.basename(backupName)) {
|
|
2941
|
+
throw new Error("Invalid backup name");
|
|
2942
|
+
}
|
|
2943
|
+
const resolvedRoot = path3.resolve(backupRoot);
|
|
2944
|
+
const resolvedBackup = path3.resolve(resolvedRoot, backupName);
|
|
2945
|
+
if (!resolvedBackup.startsWith(`${resolvedRoot}${path3.sep}`)) {
|
|
2946
|
+
throw new Error("Invalid backup name");
|
|
2947
|
+
}
|
|
2948
|
+
return resolvedBackup;
|
|
2949
|
+
}
|
|
2950
|
+
function createLocalResetBackup(dbPath, now = /* @__PURE__ */ new Date()) {
|
|
2951
|
+
if (!fs4.existsSync(dbPath)) {
|
|
2952
|
+
return null;
|
|
2953
|
+
}
|
|
2954
|
+
const backupRoot = backupRootForDb(dbPath);
|
|
2955
|
+
const baseName = `reset-${formatBackupTimestamp(now)}`;
|
|
2956
|
+
let backupDir = path3.join(backupRoot, baseName);
|
|
2957
|
+
let suffix = 1;
|
|
2958
|
+
while (fs4.existsSync(backupDir)) {
|
|
2959
|
+
backupDir = path3.join(backupRoot, `${baseName}-${suffix++}`);
|
|
2960
|
+
}
|
|
2961
|
+
fs4.mkdirSync(backupDir, { recursive: true, mode: 448 });
|
|
2962
|
+
for (const source of [dbPath, `${dbPath}-wal`, `${dbPath}-shm`]) {
|
|
2963
|
+
if (!fs4.existsSync(source)) continue;
|
|
2964
|
+
fs4.copyFileSync(source, path3.join(backupDir, path3.basename(source)));
|
|
2965
|
+
}
|
|
2966
|
+
return describeBackup(backupDir);
|
|
2967
|
+
}
|
|
2968
|
+
function listLocalResetBackups(dbPath) {
|
|
2969
|
+
const backupRoot = backupRootForDb(dbPath);
|
|
2970
|
+
if (!fs4.existsSync(backupRoot)) {
|
|
2971
|
+
return [];
|
|
2972
|
+
}
|
|
2973
|
+
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));
|
|
2974
|
+
}
|
|
2975
|
+
function restoreLocalResetBackup(dbPath, backupName, now = /* @__PURE__ */ new Date()) {
|
|
2976
|
+
const backupRoot = backupRootForDb(dbPath);
|
|
2977
|
+
const backupDir = resolveBackupDir(backupRoot, backupName);
|
|
2978
|
+
if (!fs4.existsSync(backupDir) || !fs4.statSync(backupDir).isDirectory()) {
|
|
2979
|
+
throw new Error(`Backup not found: ${backupName}`);
|
|
2980
|
+
}
|
|
2981
|
+
const restoredFrom = describeBackup(backupDir);
|
|
2982
|
+
if (!restoredFrom.files.includes("local.db")) {
|
|
2983
|
+
throw new Error(`Backup is missing local.db: ${backupName}`);
|
|
2984
|
+
}
|
|
2985
|
+
const safetyBackup = createLocalResetBackup(dbPath, now);
|
|
2986
|
+
fs4.mkdirSync(path3.dirname(dbPath), { recursive: true });
|
|
2987
|
+
for (const target of [dbPath, `${dbPath}-wal`, `${dbPath}-shm`]) {
|
|
2988
|
+
if (fs4.existsSync(target)) {
|
|
2989
|
+
fs4.rmSync(target, { force: true });
|
|
2990
|
+
}
|
|
2991
|
+
}
|
|
2992
|
+
for (const file of restoredFrom.files) {
|
|
2993
|
+
fs4.copyFileSync(path3.join(backupDir, file), path3.join(path3.dirname(dbPath), file));
|
|
2994
|
+
}
|
|
2995
|
+
return { restoredFrom, safetyBackup };
|
|
2996
|
+
}
|
|
2997
|
+
var backupsCommand = new Command24("backups").description("List and restore local reset backups");
|
|
2998
|
+
backupsCommand.command("list").description("List local backups created before destructive resets/restores").action(() => {
|
|
2999
|
+
const backups = listLocalResetBackups(getDbPath18());
|
|
3000
|
+
if (backups.length === 0) {
|
|
3001
|
+
logger.info("No local reset backups found.");
|
|
3002
|
+
return;
|
|
3003
|
+
}
|
|
3004
|
+
for (const backup of backups) {
|
|
3005
|
+
logger.info(`${backup.name} ${backup.sizeBytes} bytes ${backup.dir}`);
|
|
3006
|
+
}
|
|
3007
|
+
});
|
|
3008
|
+
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) => {
|
|
3009
|
+
if (!opts.force) {
|
|
3010
|
+
const ok = await confirm(
|
|
3011
|
+
"This will replace the active local database after creating a safety backup. Continue?"
|
|
3012
|
+
);
|
|
3013
|
+
if (!ok) {
|
|
3014
|
+
logger.info("Aborted.");
|
|
3015
|
+
return;
|
|
3016
|
+
}
|
|
3017
|
+
}
|
|
3018
|
+
try {
|
|
3019
|
+
const result = restoreLocalResetBackup(getDbPath18(), name);
|
|
3020
|
+
logger.info(`Restored local database from ${result.restoredFrom.name}`);
|
|
3021
|
+
if (result.safetyBackup) {
|
|
3022
|
+
logger.info(`Previous local database safety backup: ${result.safetyBackup.dir}`);
|
|
3023
|
+
}
|
|
3024
|
+
} catch (err) {
|
|
3025
|
+
logger.error(err instanceof Error ? err.message : String(err));
|
|
3026
|
+
process.exit(EXIT_ERROR);
|
|
3027
|
+
}
|
|
3028
|
+
});
|
|
3029
|
+
|
|
3030
|
+
// src/commands/reset.ts
|
|
3031
|
+
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", `
|
|
2903
3032
|
Examples:
|
|
2904
3033
|
unforgit reset Reset both local and remote
|
|
2905
3034
|
unforgit reset --local Reset local store only
|
|
2906
3035
|
unforgit reset --remote Reset remote store only
|
|
2907
|
-
unforgit reset --force Skip confirmation
|
|
3036
|
+
unforgit reset --force Skip confirmation
|
|
3037
|
+
unforgit reset --no-backup Skip local backup before reset`).action(async (opts) => {
|
|
2908
3038
|
const resetLocal = opts.local || !opts.local && !opts.remote;
|
|
2909
3039
|
const resetRemote = opts.remote || !opts.local && !opts.remote;
|
|
2910
3040
|
const targets = [
|
|
@@ -2924,7 +3054,21 @@ Examples:
|
|
|
2924
3054
|
}
|
|
2925
3055
|
const config = loadConfig21();
|
|
2926
3056
|
if (resetLocal) {
|
|
2927
|
-
const
|
|
3057
|
+
const dbPath = getDbPath19();
|
|
3058
|
+
if (opts.backup !== false) {
|
|
3059
|
+
try {
|
|
3060
|
+
const backup = createLocalResetBackup(dbPath);
|
|
3061
|
+
if (backup) {
|
|
3062
|
+
logger.info(`Created local reset backup: ${backup.dir}`);
|
|
3063
|
+
}
|
|
3064
|
+
} catch (err) {
|
|
3065
|
+
logger.error(
|
|
3066
|
+
`Failed to create local reset backup: ${err instanceof Error ? err.message : String(err)}`
|
|
3067
|
+
);
|
|
3068
|
+
process.exit(EXIT_ERROR);
|
|
3069
|
+
}
|
|
3070
|
+
}
|
|
3071
|
+
const store = new LocalStore18(dbPath);
|
|
2928
3072
|
try {
|
|
2929
3073
|
const result = store.resetAll();
|
|
2930
3074
|
logger.info(`Local reset complete:`);
|
|
@@ -2965,22 +3109,24 @@ Examples:
|
|
|
2965
3109
|
});
|
|
2966
3110
|
|
|
2967
3111
|
// src/commands/doctor.ts
|
|
2968
|
-
import { Command as
|
|
2969
|
-
import { loadConfig as loadConfig22, getDbPath as
|
|
3112
|
+
import { Command as Command26 } from "commander";
|
|
3113
|
+
import { loadConfig as loadConfig22, getDbPath as getDbPath20, isInitialized as isInitialized16, getConfigPath as getConfigPath2 } from "unforgit-config";
|
|
2970
3114
|
import { LocalStore as LocalStore19 } from "unforgit-db";
|
|
2971
3115
|
import { RemoteClient as RemoteClient13 } from "unforgit-config";
|
|
2972
|
-
import
|
|
2973
|
-
|
|
3116
|
+
import fs5 from "fs";
|
|
3117
|
+
import YAML from "yaml";
|
|
3118
|
+
var doctorCommand = new Command26("doctor").description("Check system health and diagnose common issues").action(async () => {
|
|
2974
3119
|
const results = [];
|
|
2975
3120
|
if (!isInitialized16()) {
|
|
2976
3121
|
results.push({
|
|
2977
3122
|
check: "initialization",
|
|
2978
3123
|
status: "error",
|
|
2979
|
-
message: "Not an unforgit repository. Run 'unforgit init' first."
|
|
3124
|
+
message: "Not an unforgit repository. Run 'unforgit init' first.",
|
|
3125
|
+
fix: "Run 'unforgit init' in the repository root."
|
|
2980
3126
|
});
|
|
2981
3127
|
if (isJsonMode()) {
|
|
2982
|
-
outputJson(
|
|
2983
|
-
|
|
3128
|
+
outputJson(buildPayload(results));
|
|
3129
|
+
process.exit(EXIT_CONFIG_ERROR);
|
|
2984
3130
|
}
|
|
2985
3131
|
printResults(results);
|
|
2986
3132
|
process.exit(EXIT_CONFIG_ERROR);
|
|
@@ -2990,15 +3136,27 @@ var doctorCommand = new Command25("doctor").description("Check system health and
|
|
|
2990
3136
|
try {
|
|
2991
3137
|
const config = loadConfig22();
|
|
2992
3138
|
results.push({ check: "config", status: "ok", message: `Valid config at ${configPath}` });
|
|
3139
|
+
const deprecatedConfigKeys = findDeprecatedConfigKeys(configPath);
|
|
3140
|
+
if (deprecatedConfigKeys.length > 0) {
|
|
3141
|
+
results.push({
|
|
3142
|
+
check: "config-deprecated-secrets",
|
|
3143
|
+
status: "warn",
|
|
3144
|
+
message: `Deprecated secret-bearing config key(s): ${deprecatedConfigKeys.join(", ")}`,
|
|
3145
|
+
fix: "Move secrets to environment variables and remove them from unforgit.yaml."
|
|
3146
|
+
});
|
|
3147
|
+
} else {
|
|
3148
|
+
results.push({ check: "config-deprecated-secrets", status: "ok", message: "No deprecated secret keys in config" });
|
|
3149
|
+
}
|
|
2993
3150
|
if (process.platform !== "win32") {
|
|
2994
3151
|
try {
|
|
2995
|
-
const stat =
|
|
3152
|
+
const stat = fs5.statSync(configPath);
|
|
2996
3153
|
const mode = stat.mode & 511;
|
|
2997
3154
|
if (mode & 36) {
|
|
2998
3155
|
results.push({
|
|
2999
3156
|
check: "config-permissions",
|
|
3000
3157
|
status: "warn",
|
|
3001
|
-
message: `Config readable by others (mode ${mode.toString(8)}). Run: chmod 600 ${configPath}
|
|
3158
|
+
message: `Config readable by others (mode ${mode.toString(8)}). Run: chmod 600 ${configPath}`,
|
|
3159
|
+
fix: `Run 'chmod 600 ${configPath}'.`
|
|
3002
3160
|
});
|
|
3003
3161
|
} else {
|
|
3004
3162
|
results.push({ check: "config-permissions", status: "ok", message: "Config file permissions are secure" });
|
|
@@ -3007,12 +3165,18 @@ var doctorCommand = new Command25("doctor").description("Check system health and
|
|
|
3007
3165
|
results.push({ check: "config-permissions", status: "warn", message: "Could not check config file permissions" });
|
|
3008
3166
|
}
|
|
3009
3167
|
}
|
|
3010
|
-
const store = new LocalStore19(
|
|
3168
|
+
const store = new LocalStore19(getDbPath20());
|
|
3011
3169
|
try {
|
|
3012
3170
|
const orgId = config.remote.orgId || "local";
|
|
3013
3171
|
const repoId = config.remote.repoId || "local";
|
|
3014
3172
|
store.list({ orgId, repoId, limit: 1 });
|
|
3015
3173
|
results.push({ check: "local-db", status: "ok", message: "SQLite database is accessible" });
|
|
3174
|
+
const memoryStats = store.stats(orgId, repoId);
|
|
3175
|
+
results.push({
|
|
3176
|
+
check: "memory-stats",
|
|
3177
|
+
status: "ok",
|
|
3178
|
+
message: `${memoryStats.total} memories (${memoryStats.byType.episodic} episodic, ${memoryStats.byType.semantic} semantic, ${memoryStats.byType.procedural} procedural)`
|
|
3179
|
+
});
|
|
3016
3180
|
const stats = store.getEmbeddingStats(orgId, repoId);
|
|
3017
3181
|
if (stats.total === 0) {
|
|
3018
3182
|
results.push({ check: "embeddings", status: "ok", message: "No memories yet" });
|
|
@@ -3023,16 +3187,29 @@ var doctorCommand = new Command25("doctor").description("Check system health and
|
|
|
3023
3187
|
results.push({
|
|
3024
3188
|
check: "embeddings",
|
|
3025
3189
|
status: "warn",
|
|
3026
|
-
message: `${stats.withoutEmbedding}/${stats.total} memories lack embeddings (${pct}% coverage). Run 'unforgit embeddings backfill'
|
|
3190
|
+
message: `${stats.withoutEmbedding}/${stats.total} memories lack embeddings (${pct}% coverage). Run 'unforgit embeddings backfill'`,
|
|
3191
|
+
fix: "Run 'unforgit embeddings backfill'."
|
|
3027
3192
|
});
|
|
3028
3193
|
}
|
|
3029
3194
|
const pendingPush = store.getPendingPush();
|
|
3030
3195
|
const conflicts = store.getConflicts();
|
|
3196
|
+
const unsyncedTombstones = store.getUnsyncedTombstones(orgId, repoId);
|
|
3197
|
+
if (unsyncedTombstones.length > 0) {
|
|
3198
|
+
results.push({
|
|
3199
|
+
check: "tombstones",
|
|
3200
|
+
status: "warn",
|
|
3201
|
+
message: `${unsyncedTombstones.length} deleted memory tombstone(s) pending sync`,
|
|
3202
|
+
fix: "Run 'unforgit push' to sync deletions."
|
|
3203
|
+
});
|
|
3204
|
+
} else {
|
|
3205
|
+
results.push({ check: "tombstones", status: "ok", message: "No deleted memory tombstones pending sync" });
|
|
3206
|
+
}
|
|
3031
3207
|
if (conflicts.length > 0) {
|
|
3032
3208
|
results.push({
|
|
3033
3209
|
check: "sync",
|
|
3034
3210
|
status: "warn",
|
|
3035
|
-
message: `${conflicts.length} sync conflict(s) need resolution
|
|
3211
|
+
message: `${conflicts.length} sync conflict(s) need resolution`,
|
|
3212
|
+
fix: "Resolve conflicts with 'unforgit pull --force' or 'unforgit push --force' after reviewing the desired source of truth."
|
|
3036
3213
|
});
|
|
3037
3214
|
} else if (pendingPush.length > 0) {
|
|
3038
3215
|
results.push({
|
|
@@ -3052,13 +3229,14 @@ var doctorCommand = new Command25("doctor").description("Check system health and
|
|
|
3052
3229
|
results.push({
|
|
3053
3230
|
check: "auth",
|
|
3054
3231
|
status: "warn",
|
|
3055
|
-
message: "No API key configured. Set the UNFORGIT_API_KEY environment variable."
|
|
3232
|
+
message: "No API key configured. Set the UNFORGIT_API_KEY environment variable.",
|
|
3233
|
+
fix: "Set UNFORGIT_API_KEY in your shell or secret manager before remote sync."
|
|
3056
3234
|
});
|
|
3057
3235
|
} else {
|
|
3058
3236
|
results.push({
|
|
3059
3237
|
check: "auth",
|
|
3060
3238
|
status: "ok",
|
|
3061
|
-
message:
|
|
3239
|
+
message: "API key configured ([REDACTED])"
|
|
3062
3240
|
});
|
|
3063
3241
|
}
|
|
3064
3242
|
try {
|
|
@@ -3071,58 +3249,107 @@ var doctorCommand = new Command25("doctor").description("Check system health and
|
|
|
3071
3249
|
await client.listApiKeys();
|
|
3072
3250
|
results.push({ check: "remote-auth", status: "ok", message: "API key is valid" });
|
|
3073
3251
|
} catch {
|
|
3074
|
-
results.push({
|
|
3252
|
+
results.push({
|
|
3253
|
+
check: "remote-auth",
|
|
3254
|
+
status: "error",
|
|
3255
|
+
message: "API key is invalid or expired",
|
|
3256
|
+
fix: "Create a new API key with 'unforgit keys create' or update UNFORGIT_API_KEY."
|
|
3257
|
+
});
|
|
3075
3258
|
}
|
|
3076
3259
|
}
|
|
3077
3260
|
} else {
|
|
3078
3261
|
results.push({
|
|
3079
3262
|
check: "remote",
|
|
3080
3263
|
status: "error",
|
|
3081
|
-
message: `Server returned HTTP ${res.status}
|
|
3264
|
+
message: `Server returned HTTP ${res.status}`,
|
|
3265
|
+
fix: "Check the Unforgit API server health endpoint and remote.url in unforgit.yaml."
|
|
3082
3266
|
});
|
|
3083
3267
|
}
|
|
3084
3268
|
} catch (err) {
|
|
3085
3269
|
results.push({
|
|
3086
3270
|
check: "remote",
|
|
3087
3271
|
status: "error",
|
|
3088
|
-
message: `Cannot connect to ${config.remote.url}: ${err instanceof Error ? err.message : err}
|
|
3272
|
+
message: `Cannot connect to ${config.remote.url}: ${err instanceof Error ? err.message : err}`,
|
|
3273
|
+
fix: "Start the Unforgit API server or update remote.url in unforgit.yaml."
|
|
3089
3274
|
});
|
|
3090
3275
|
}
|
|
3091
3276
|
} else {
|
|
3092
|
-
results.push({
|
|
3277
|
+
results.push({
|
|
3278
|
+
check: "remote",
|
|
3279
|
+
status: "warn",
|
|
3280
|
+
message: "No remote URL configured",
|
|
3281
|
+
fix: "Run 'unforgit remote add origin <url>' if this repo should sync remotely."
|
|
3282
|
+
});
|
|
3093
3283
|
}
|
|
3094
3284
|
const openaiKey = process.env.OPENAI_API_KEY;
|
|
3095
3285
|
if (openaiKey) {
|
|
3096
|
-
results.push({ check: "openai", status: "ok", message:
|
|
3286
|
+
results.push({ check: "openai", status: "ok", message: "OpenAI API key configured ([REDACTED])" });
|
|
3097
3287
|
} else {
|
|
3098
3288
|
results.push({
|
|
3099
3289
|
check: "openai",
|
|
3100
3290
|
status: "warn",
|
|
3101
|
-
message: "No OpenAI API key. Auto-consolidation and embeddings backfill require it."
|
|
3291
|
+
message: "No OpenAI API key. Auto-consolidation and embeddings backfill require it.",
|
|
3292
|
+
fix: "Set OPENAI_API_KEY before running embedding backfill or AI consolidation."
|
|
3102
3293
|
});
|
|
3103
3294
|
}
|
|
3104
3295
|
} catch (err) {
|
|
3105
3296
|
results.push({
|
|
3106
3297
|
check: "config",
|
|
3107
3298
|
status: "error",
|
|
3108
|
-
message: err instanceof Error ? err.message : String(err)
|
|
3299
|
+
message: err instanceof Error ? err.message : String(err),
|
|
3300
|
+
fix: `Fix ${configPath} or re-run 'unforgit init'.`
|
|
3109
3301
|
});
|
|
3110
3302
|
}
|
|
3111
3303
|
if (isJsonMode()) {
|
|
3112
|
-
outputJson(
|
|
3304
|
+
outputJson(buildPayload(results));
|
|
3305
|
+
exitForResults(results);
|
|
3113
3306
|
return;
|
|
3114
3307
|
}
|
|
3115
3308
|
printResults(results);
|
|
3309
|
+
exitForResults(results);
|
|
3116
3310
|
});
|
|
3311
|
+
function summarize(results) {
|
|
3312
|
+
return {
|
|
3313
|
+
ok: results.filter((r) => r.status === "ok").length,
|
|
3314
|
+
warnings: results.filter((r) => r.status === "warn").length,
|
|
3315
|
+
errors: results.filter((r) => r.status === "error").length
|
|
3316
|
+
};
|
|
3317
|
+
}
|
|
3318
|
+
function buildPayload(results) {
|
|
3319
|
+
return { summary: summarize(results), results };
|
|
3320
|
+
}
|
|
3321
|
+
function exitForResults(results) {
|
|
3322
|
+
if (results.some((r) => r.status === "error")) {
|
|
3323
|
+
process.exit(EXIT_ERROR);
|
|
3324
|
+
}
|
|
3325
|
+
}
|
|
3326
|
+
function findDeprecatedConfigKeys(configPath) {
|
|
3327
|
+
try {
|
|
3328
|
+
const raw = fs5.readFileSync(configPath, "utf-8");
|
|
3329
|
+
const parsed = YAML.parse(raw) ?? {};
|
|
3330
|
+
const deprecated = [];
|
|
3331
|
+
if (parsed.remote?.apiKey) {
|
|
3332
|
+
deprecated.push("remote.apiKey");
|
|
3333
|
+
}
|
|
3334
|
+
if (parsed.openaiApiKey) {
|
|
3335
|
+
deprecated.push("openaiApiKey");
|
|
3336
|
+
}
|
|
3337
|
+
return deprecated;
|
|
3338
|
+
} catch {
|
|
3339
|
+
return [];
|
|
3340
|
+
}
|
|
3341
|
+
}
|
|
3117
3342
|
function printResults(results) {
|
|
3118
3343
|
const icons = { ok: "[ok]", warn: "[!!]", error: "[ERR]" };
|
|
3119
3344
|
logger.info("Unforgit Doctor\n");
|
|
3120
3345
|
for (const r of results) {
|
|
3121
3346
|
const icon = icons[r.status];
|
|
3122
3347
|
logger.info(` ${icon} ${r.check}: ${r.message}`);
|
|
3348
|
+
if (r.fix) {
|
|
3349
|
+
logger.info(` fix: ${r.fix}`);
|
|
3350
|
+
}
|
|
3123
3351
|
}
|
|
3124
|
-
const errors = results
|
|
3125
|
-
const warnings = results.filter((r) => r.status === "warn").length;
|
|
3352
|
+
const { errors, warnings } = summarize(results);
|
|
3126
3353
|
logger.info("");
|
|
3127
3354
|
if (errors > 0) {
|
|
3128
3355
|
logger.info(`${errors} error(s), ${warnings} warning(s)`);
|
|
@@ -3134,7 +3361,7 @@ function printResults(results) {
|
|
|
3134
3361
|
}
|
|
3135
3362
|
|
|
3136
3363
|
// src/commands/completion.ts
|
|
3137
|
-
import { Command as
|
|
3364
|
+
import { Command as Command27 } from "commander";
|
|
3138
3365
|
var BASH_COMPLETION = `
|
|
3139
3366
|
_unforgit_completions() {
|
|
3140
3367
|
local cur prev commands
|
|
@@ -3279,7 +3506,7 @@ complete -c unforgit -l verbose -d 'Enable verbose output'
|
|
|
3279
3506
|
complete -c unforgit -l quiet -d 'Suppress non-essential output'
|
|
3280
3507
|
complete -c unforgit -l json -d 'Output as JSON'
|
|
3281
3508
|
`.trim();
|
|
3282
|
-
var completionCommand = new
|
|
3509
|
+
var completionCommand = new Command27("completion").description("Generate shell completion scripts").argument("<shell>", "Shell type (bash, zsh, fish)").addHelpText("after", `
|
|
3283
3510
|
Examples:
|
|
3284
3511
|
unforgit completion bash >> ~/.bashrc
|
|
3285
3512
|
unforgit completion zsh >> ~/.zshrc
|
|
@@ -3301,8 +3528,8 @@ Examples:
|
|
|
3301
3528
|
});
|
|
3302
3529
|
|
|
3303
3530
|
// src/commands/curate.ts
|
|
3304
|
-
import { Command as
|
|
3305
|
-
import { getDbPath as
|
|
3531
|
+
import { Command as Command28 } from "commander";
|
|
3532
|
+
import { getDbPath as getDbPath21, isInitialized as isInitialized17, loadConfig as loadConfig23 } from "unforgit-config";
|
|
3306
3533
|
import { LocalStore as LocalStore20 } from "unforgit-db";
|
|
3307
3534
|
import { RemoteClient as RemoteClient14 } from "unforgit-config";
|
|
3308
3535
|
import { runLocalLifecycleMaintenance } from "unforgit-core";
|
|
@@ -3318,7 +3545,7 @@ function formatCandidatePreview2(candidate) {
|
|
|
3318
3545
|
}
|
|
3319
3546
|
return lines.join("\n");
|
|
3320
3547
|
}
|
|
3321
|
-
var curateCommand = new
|
|
3548
|
+
var curateCommand = new Command28("curate").description("Preview or run lifecycle maintenance for repository memories").option("--remote", "Run maintenance against the remote server").option("--execute", "Apply changes instead of previewing them").option("--model <model>", "OpenAI model to use for consolidation").option("--no-preserve", "Do not preserve original memories during consolidation").addHelpText("after", `
|
|
3322
3549
|
Examples:
|
|
3323
3550
|
unforgit curate
|
|
3324
3551
|
unforgit curate --execute
|
|
@@ -3338,7 +3565,7 @@ Examples:
|
|
|
3338
3565
|
dryRun,
|
|
3339
3566
|
model: opts.model,
|
|
3340
3567
|
preserveOriginals: opts.preserve !== false
|
|
3341
|
-
}) : await runLocalCurate(
|
|
3568
|
+
}) : await runLocalCurate(getDbPath21(), orgId, repoId, {
|
|
3342
3569
|
dryRun,
|
|
3343
3570
|
model: opts.model,
|
|
3344
3571
|
preserveOriginals: opts.preserve !== false,
|
|
@@ -3452,7 +3679,7 @@ process.on("unhandledRejection", (err) => {
|
|
|
3452
3679
|
runCleanup();
|
|
3453
3680
|
process.exit(EXIT_ERROR);
|
|
3454
3681
|
});
|
|
3455
|
-
var program = new
|
|
3682
|
+
var program = new Command29();
|
|
3456
3683
|
program.name("unforgit").description("Unforgit \u2014 repository memory for agents and developers").version(pkg.version).option("--verbose", "Enable verbose output").option("--quiet", "Suppress non-essential output").option("--json", "Output results as JSON (for scripting)").hook("preAction", () => {
|
|
3457
3684
|
const opts = program.opts();
|
|
3458
3685
|
if (opts.quiet) setVerbosity(0);
|
|
@@ -3489,6 +3716,7 @@ program.addCommand(authCommand);
|
|
|
3489
3716
|
program.addCommand(configCommand);
|
|
3490
3717
|
program.addCommand(embeddingsCommand);
|
|
3491
3718
|
program.addCommand(resetCommand);
|
|
3719
|
+
program.addCommand(backupsCommand);
|
|
3492
3720
|
program.addCommand(doctorCommand);
|
|
3493
3721
|
program.addCommand(curateCommand);
|
|
3494
3722
|
program.addCommand(completionCommand);
|