unforgit 0.1.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 ADDED
@@ -0,0 +1,3504 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/index.ts
4
+ import { Command as Command28 } from "commander";
5
+
6
+ // src/commands/init.ts
7
+ import fs2 from "fs";
8
+ import { Command } from "commander";
9
+
10
+ // src/utils.ts
11
+ import readline from "readline";
12
+ function truncate(text, maxLength) {
13
+ const singleLine = text.replace(/\n/g, " ").trim();
14
+ if (singleLine.length <= maxLength) return singleLine;
15
+ return singleLine.slice(0, maxLength - 3) + "...";
16
+ }
17
+ function maskKey(key) {
18
+ if (key.length <= 10) return "***";
19
+ return `${key.slice(0, 6)}...${key.slice(-4)}`;
20
+ }
21
+ function confirm(message) {
22
+ const rl = readline.createInterface({
23
+ input: process.stdin,
24
+ output: process.stdout
25
+ });
26
+ return new Promise((resolve) => {
27
+ rl.question(`${message} (yes/no): `, (answer) => {
28
+ rl.close();
29
+ resolve(answer.trim().toLowerCase() === "yes");
30
+ });
31
+ });
32
+ }
33
+ var jsonMode = false;
34
+ function setJsonMode(enabled) {
35
+ jsonMode = enabled;
36
+ }
37
+ function isJsonMode() {
38
+ return jsonMode;
39
+ }
40
+ function outputJson(data) {
41
+ console.log(JSON.stringify(data, null, 2));
42
+ }
43
+ function paginate(items, page, perPage) {
44
+ const total = items.length;
45
+ const totalPages = Math.ceil(total / perPage);
46
+ const currentPage = Math.min(page, totalPages);
47
+ const start = (currentPage - 1) * perPage;
48
+ return {
49
+ items: items.slice(start, start + perPage),
50
+ totalPages,
51
+ currentPage,
52
+ total
53
+ };
54
+ }
55
+
56
+ // src/logger.ts
57
+ var level = 1;
58
+ function setVerbosity(v) {
59
+ level = v;
60
+ }
61
+ var REDACTED = "[REDACTED]";
62
+ var secretPatterns = [
63
+ /\b([A-Z][A-Z0-9_]*(?:KEY|TOKEN|SECRET|PASSWORD))\s*=\s*([^\s]+)/gi,
64
+ /\b(authorization\s*:\s*bearer)\s+([^\s]+)/gi,
65
+ /\b(client_secret)\s*=\s*([^\s&]+)/gi,
66
+ /(postgres(?:ql)?:\/\/[^:\s/@]+:)([^@\s]+)(@)/gi
67
+ ];
68
+ function redactSecrets(msg) {
69
+ return msg.replace(secretPatterns[0], (_match, prefix) => `${prefix}=${REDACTED}`).replace(secretPatterns[1], (_match, prefix) => `${prefix} ${REDACTED}`).replace(secretPatterns[2], (_match, prefix) => `${prefix}=${REDACTED}`).replace(secretPatterns[3], (_match, prefix, _secret, suffix) => `${prefix}${REDACTED}${suffix}`);
70
+ }
71
+ function safeMessage(msg) {
72
+ return redactSecrets(msg);
73
+ }
74
+ var logger = {
75
+ fatal(msg) {
76
+ if (!isJsonMode()) console.error(`fatal: ${safeMessage(msg)}`);
77
+ },
78
+ error(msg) {
79
+ if (!isJsonMode()) console.error(`error: ${safeMessage(msg)}`);
80
+ },
81
+ warn(msg) {
82
+ if (level >= 1 && !isJsonMode()) console.error(`warning: ${safeMessage(msg)}`);
83
+ },
84
+ info(msg) {
85
+ if (level >= 1 && !isJsonMode()) console.log(safeMessage(msg));
86
+ },
87
+ debug(msg) {
88
+ if (level >= 2 && !isJsonMode()) console.error(`debug: ${safeMessage(msg)}`);
89
+ },
90
+ progress(current, total, label) {
91
+ if (level >= 1 && !isJsonMode()) {
92
+ const pct = total > 0 ? Math.round(current / total * 100) : 0;
93
+ const suffix = label ? ` ${safeMessage(label)}` : "";
94
+ process.stdout.write(`\rProcessing ${current}/${total} (${pct}%)${suffix}`);
95
+ if (current === total) process.stdout.write("\n");
96
+ }
97
+ }
98
+ };
99
+
100
+ // src/commands/init.ts
101
+ import {
102
+ getDataDir,
103
+ getConfigPath,
104
+ getDbPath,
105
+ defaultConfig,
106
+ saveConfig,
107
+ isInitialized,
108
+ detectGitInfo
109
+ } from "unforgit-config";
110
+ import { LocalStore } from "unforgit-db";
111
+
112
+ // src/ide-integration.ts
113
+ import fs from "fs";
114
+ import path from "path";
115
+ var ALL_IDE_NAMES = [
116
+ "cursor",
117
+ "claude",
118
+ "vscode",
119
+ "windsurf"
120
+ ];
121
+ var UNFORGIT_MARKER = "Unforgit Memory Integration";
122
+ var MEMORY_INSTRUCTIONS = `# Unforgit Memory Integration
123
+
124
+ You have access to \`unforgit_recall\` and \`unforgit_add\` MCP tools for persistent repository memory across sessions.
125
+
126
+ ## 1. Recall at the Start
127
+
128
+ At the **beginning of every conversation**, use the \`unforgit_recall\` tool with a query based on the user's first message to retrieve relevant context from past sessions.
129
+
130
+ Use recalled memories to avoid repeating past mistakes, follow known conventions, and apply previous decisions.
131
+
132
+ ## 2. Save During the Conversation
133
+
134
+ Save memories **as soon as something noteworthy happens** \u2014 don't wait until the end.
135
+
136
+ Trigger a save with the \`unforgit_add\` tool immediately after:
137
+ - A bug is found and fixed
138
+ - An architectural or design decision is made
139
+ - A non-obvious gotcha or workaround is discovered
140
+ - A new convention or pattern is established
141
+ - A deployment/setup procedure is figured out
142
+
143
+ ### Memory types
144
+
145
+ | Type | When | Example |
146
+ |------|------|---------|
147
+ | \`semantic\` | Decisions, conventions, architecture facts | "We use UTC timestamps everywhere" |
148
+ | \`procedural\` | Workflows, how-tos, playbooks | "To deploy: run make release, then kubectl apply" |
149
+ | \`episodic\` | Bugs found, gotchas, observations | "Found race condition in queue worker" |
150
+
151
+ ### Rules
152
+
153
+ - Keep text concise but self-contained \u2014 a future reader should understand it without extra context
154
+ - Use meaningful tags for discoverability (e.g. \`["auth", "bug"]\`, \`["deploy", "playbook"]\`)
155
+ - Prefer \`semantic\` for stable facts, \`procedural\` for how-tos, \`episodic\` for transient observations
156
+ - Do NOT save trivial changes, obvious things, or info already in the codebase docs
157
+ - Quality over quantity \u2014 only save what's genuinely useful for future sessions
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
+ function upsertJsonMcp(filePath, serverKey, mcpEntry) {
164
+ const dir = path.dirname(filePath);
165
+ fs.mkdirSync(dir, { recursive: true });
166
+ if (!fs.existsSync(filePath)) {
167
+ const config = { [serverKey]: { unforgit: mcpEntry } };
168
+ fs.writeFileSync(filePath, JSON.stringify(config, null, 2) + "\n", "utf-8");
169
+ return { path: filePath, action: "created" };
170
+ }
171
+ const existing = JSON.parse(fs.readFileSync(filePath, "utf-8"));
172
+ const servers = existing[serverKey];
173
+ if (servers?.unforgit) {
174
+ return { path: filePath, action: "exists" };
175
+ }
176
+ existing[serverKey] = existing[serverKey] || {};
177
+ existing[serverKey].unforgit = mcpEntry;
178
+ fs.writeFileSync(
179
+ filePath,
180
+ JSON.stringify(existing, null, 2) + "\n",
181
+ "utf-8"
182
+ );
183
+ return { path: filePath, action: "updated" };
184
+ }
185
+ function appendOrCreateMarkdown(filePath, content) {
186
+ const dir = path.dirname(filePath);
187
+ fs.mkdirSync(dir, { recursive: true });
188
+ if (!fs.existsSync(filePath)) {
189
+ fs.writeFileSync(filePath, content + "\n", "utf-8");
190
+ return { path: filePath, action: "created" };
191
+ }
192
+ if (fileContainsUnforgit(filePath)) {
193
+ return { path: filePath, action: "exists" };
194
+ }
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
+ }
200
+ var CURSOR_RULE_CONTENT = `---
201
+ description: Auto-manage repository memory with Unforgit via MCP tools
202
+ alwaysApply: true
203
+ ---
204
+
205
+ ${MEMORY_INSTRUCTIONS}
206
+ `;
207
+ function setupCursor(cwd4) {
208
+ const rulesDir = path.join(cwd4, ".cursor", "rules");
209
+ const rulePath = path.join(rulesDir, "unforgit-memory.mdc");
210
+ let rules;
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
+ }
218
+ const mcpPath = path.join(cwd4, ".cursor", "mcp.json");
219
+ const mcp = upsertJsonMcp(mcpPath, "mcpServers", {
220
+ command: "unforgit-mcp",
221
+ args: []
222
+ });
223
+ return { ide: "cursor", rules, mcp };
224
+ }
225
+ function setupClaude(cwd4) {
226
+ const claudeMdPath = path.join(cwd4, "CLAUDE.md");
227
+ const rules = appendOrCreateMarkdown(claudeMdPath, MEMORY_INSTRUCTIONS);
228
+ const mcpPath = path.join(cwd4, ".mcp.json");
229
+ const mcp = upsertJsonMcp(mcpPath, "mcpServers", {
230
+ command: "unforgit-mcp",
231
+ args: []
232
+ });
233
+ return { ide: "claude", rules, mcp };
234
+ }
235
+ function setupVscode(cwd4) {
236
+ const instructionsPath = path.join(
237
+ cwd4,
238
+ ".github",
239
+ "copilot-instructions.md"
240
+ );
241
+ const rules = appendOrCreateMarkdown(instructionsPath, MEMORY_INSTRUCTIONS);
242
+ const mcpPath = path.join(cwd4, ".vscode", "mcp.json");
243
+ const mcp = upsertJsonMcp(mcpPath, "servers", {
244
+ type: "stdio",
245
+ command: "unforgit-mcp"
246
+ });
247
+ return { ide: "vscode", rules, mcp };
248
+ }
249
+ var WINDSURF_RULE_CONTENT = `${MEMORY_INSTRUCTIONS}
250
+ `;
251
+ function setupWindsurf(cwd4) {
252
+ const rulesPath = path.join(cwd4, ".windsurfrules");
253
+ const rules = appendOrCreateMarkdown(rulesPath, WINDSURF_RULE_CONTENT);
254
+ const mcpPath = path.join(cwd4, ".windsurf", "mcp.json");
255
+ const mcp = upsertJsonMcp(mcpPath, "mcpServers", {
256
+ command: "unforgit-mcp",
257
+ args: []
258
+ });
259
+ return { ide: "windsurf", rules, mcp };
260
+ }
261
+ var IDE_INDICATORS = {
262
+ cursor: [".cursor"],
263
+ claude: ["CLAUDE.md", ".claude"],
264
+ vscode: [".vscode"],
265
+ windsurf: [".windsurf", ".windsurfrules"]
266
+ };
267
+ function detectIdes(cwd4) {
268
+ const detected = [];
269
+ for (const [ide, indicators] of Object.entries(IDE_INDICATORS)) {
270
+ for (const indicator of indicators) {
271
+ if (fs.existsSync(path.join(cwd4, indicator))) {
272
+ detected.push(ide);
273
+ break;
274
+ }
275
+ }
276
+ }
277
+ return detected;
278
+ }
279
+ var IDE_HANDLERS = {
280
+ cursor: setupCursor,
281
+ claude: setupClaude,
282
+ vscode: setupVscode,
283
+ windsurf: setupWindsurf
284
+ };
285
+ var IDE_LABELS = {
286
+ cursor: "Cursor",
287
+ claude: "Claude Code",
288
+ vscode: "VS Code (Copilot)",
289
+ windsurf: "Windsurf"
290
+ };
291
+ function setupIdes(cwd4, ides) {
292
+ const results = [];
293
+ for (const ide of ides) {
294
+ const handler = IDE_HANDLERS[ide];
295
+ const result = handler(cwd4);
296
+ results.push(result);
297
+ }
298
+ return results;
299
+ }
300
+ function logIdeResults(results) {
301
+ if (results.length === 0) {
302
+ logger.info(" IDE integration: skipped");
303
+ return;
304
+ }
305
+ for (const result of results) {
306
+ const label = IDE_LABELS[result.ide];
307
+ logger.info(` ${label}:`);
308
+ if (result.rules) {
309
+ const verb = result.rules.action === "exists" ? "already exists" : result.rules.action;
310
+ logger.info(` Rules: ${verb} \u2192 ${result.rules.path}`);
311
+ }
312
+ if (result.mcp) {
313
+ const verb = result.mcp.action === "exists" ? "already configured" : result.mcp.action;
314
+ logger.info(` MCP: ${verb} \u2192 ${result.mcp.path}`);
315
+ }
316
+ }
317
+ }
318
+ function parseIdeOption(value) {
319
+ if (value === "all") return [...ALL_IDE_NAMES];
320
+ const names = value.split(",").map((s) => s.trim().toLowerCase());
321
+ const valid = [];
322
+ for (const name of names) {
323
+ if (ALL_IDE_NAMES.includes(name)) {
324
+ valid.push(name);
325
+ } else {
326
+ logger.warn(`Unknown IDE: "${name}". Valid options: ${ALL_IDE_NAMES.join(", ")}`);
327
+ }
328
+ }
329
+ return valid;
330
+ }
331
+
332
+ // src/commands/init.ts
333
+ var initCommand = new Command("init").description("Initialize Unforgit in the current repository").option("--org-id <orgId>", "Override auto-detected organization ID").option("--repo-id <repoId>", "Override auto-detected repository ID").option("--remote-url <url>", "Remote API URL", "http://localhost:3737").option(
334
+ "--ide <ides>",
335
+ `IDE integrations to set up: ${ALL_IDE_NAMES.join(", ")}, all (default: auto-detect)`
336
+ ).option("--no-ide", "Skip all IDE integrations").option("--no-cursor-rule", "Skip IDE integrations (deprecated, use --no-ide)").action((opts) => {
337
+ const cwd4 = process.cwd();
338
+ if (isInitialized(cwd4)) {
339
+ logger.info("Unforgit is already initialized in this directory.");
340
+ return;
341
+ }
342
+ const dataDir = getDataDir(cwd4);
343
+ fs2.mkdirSync(dataDir, { recursive: true });
344
+ const git = detectGitInfo(cwd4);
345
+ const config = defaultConfig();
346
+ config.remote.orgId = opts.orgId ?? git.orgId;
347
+ config.remote.repoId = opts.repoId ?? git.repoId;
348
+ if (opts.remoteUrl) config.remote.url = opts.remoteUrl;
349
+ saveConfig(config, cwd4);
350
+ const store = new LocalStore(getDbPath(cwd4));
351
+ store.close();
352
+ logger.info(`Initialized Unforgit at ${dataDir}`);
353
+ logger.info(` Config: ${getConfigPath(cwd4)}`);
354
+ logger.info(` Local DB: ${getDbPath(cwd4)}`);
355
+ if (config.remote.orgId || config.remote.repoId) {
356
+ logger.info(` Org: ${config.remote.orgId}`);
357
+ logger.info(` Repo: ${config.remote.repoId}`);
358
+ if (!opts.orgId && !opts.repoId) {
359
+ logger.info(" (auto-detected from git remote)");
360
+ }
361
+ } else {
362
+ logger.info(
363
+ "\nTip: Set org_id and repo_id in .unforgit/unforgit.yaml or add a git remote."
364
+ );
365
+ }
366
+ const skipIde = opts.ide === false || opts.cursorRule === false;
367
+ if (!skipIde) {
368
+ let ides;
369
+ if (opts.ide && typeof opts.ide === "string") {
370
+ ides = parseIdeOption(opts.ide);
371
+ } else {
372
+ ides = detectIdes(cwd4);
373
+ if (ides.length === 0) {
374
+ ides = ["cursor"];
375
+ logger.info(
376
+ "\n No IDE detected, defaulting to Cursor. Use --ide to specify."
377
+ );
378
+ } else {
379
+ logger.info(
380
+ `
381
+ Auto-detected IDEs: ${ides.join(", ")}`
382
+ );
383
+ }
384
+ }
385
+ const results = setupIdes(cwd4, ides);
386
+ logIdeResults(results);
387
+ } else {
388
+ logger.info("\n IDE integration: skipped");
389
+ }
390
+ });
391
+
392
+ // src/commands/add.ts
393
+ import { Command as Command2 } from "commander";
394
+ import { loadConfig, getDbPath as getDbPath2 } from "unforgit-config";
395
+
396
+ // src/exit-codes.ts
397
+ var EXIT_ERROR = 1;
398
+ var EXIT_CONFIG_ERROR = 2;
399
+ var EXIT_SIGINT = 130;
400
+ var EXIT_SIGTERM = 143;
401
+
402
+ // src/commands/add.ts
403
+ import { LocalStore as LocalStore2 } from "unforgit-db";
404
+ import { resolveVisibility } from "unforgit-core";
405
+ import { applyLifecycleDefaults } from "unforgit-core";
406
+ import { getTemplate, applyTemplate, formatTemplateList } from "unforgit-core";
407
+ import { validateMemoryType, parseConfidence, parseTtl } from "unforgit-config";
408
+ var addCommand = new Command2("add").description("Add a memory (local by default)").argument("<text>", "Memory text content").option(
409
+ "-t, --type <type>",
410
+ "Memory type (episodic|semantic|procedural)"
411
+ ).option("--tags <tags>", "Comma-separated tags", "").option(
412
+ "--visibility <visibility>",
413
+ "Visibility (private|repo|auto)"
414
+ ).option("--source-pr <url>", "Source PR URL").option("--source-commit <sha>", "Source commit SHA").option("--confidence <n>", "Confidence score 0-1").option("--ttl <seconds>", "TTL in seconds").option("--template <name>", "Use a template (decision, gotcha, playbook, etc.)").option("--list-templates", "List available templates").addHelpText("after", `
415
+ Examples:
416
+ unforgit add "We use UTC timestamps everywhere" -t semantic --tags time,convention
417
+ unforgit add "Found race condition in worker" -t episodic --tags bug
418
+ unforgit add "To deploy: run make release" --template playbook`).action((text, opts) => {
419
+ if (opts.listTemplates) {
420
+ logger.info(formatTemplateList());
421
+ return;
422
+ }
423
+ if (!text || !text.trim()) {
424
+ logger.error("Memory text cannot be empty.");
425
+ process.exit(EXIT_ERROR);
426
+ }
427
+ const config = loadConfig();
428
+ const store = new LocalStore2(getDbPath2());
429
+ try {
430
+ let userTags = [...new Set(
431
+ opts.tags ? opts.tags.split(",").map((t) => t.trim()).filter(Boolean) : []
432
+ )];
433
+ if (opts.type && !validateMemoryType(opts.type)) {
434
+ logger.error(`Invalid memory type "${opts.type}". Must be one of: episodic, semantic, procedural`);
435
+ process.exit(EXIT_ERROR);
436
+ }
437
+ let memoryType = opts.type ?? config.defaults.memoryType;
438
+ let memoryText = text;
439
+ let visibility = opts.visibility ?? config.defaults.visibility;
440
+ if (opts.template) {
441
+ const template = getTemplate(opts.template);
442
+ if (!template) {
443
+ logger.error(`Unknown template: ${opts.template}`);
444
+ logger.info("\n" + formatTemplateList());
445
+ process.exit(EXIT_ERROR);
446
+ }
447
+ const applied = applyTemplate(template, text, userTags);
448
+ memoryText = applied.text;
449
+ memoryType = applied.memoryType;
450
+ userTags = applied.tags;
451
+ if (visibility === "auto") {
452
+ visibility = applied.visibility;
453
+ }
454
+ logger.info(`Using template: ${template.name}`);
455
+ }
456
+ const sourceRefs = {};
457
+ if (opts.sourcePr) sourceRefs.pr_url = opts.sourcePr;
458
+ if (opts.sourceCommit) sourceRefs.commit_sha = opts.sourceCommit;
459
+ const input = applyLifecycleDefaults({
460
+ orgId: config.remote.orgId || "local",
461
+ repoId: config.remote.repoId || "local",
462
+ memoryType,
463
+ text: memoryText,
464
+ tags: userTags,
465
+ sourceRefs: Object.keys(sourceRefs).length > 0 ? sourceRefs : void 0,
466
+ confidence: opts.confidence ? parseConfidence(opts.confidence) : void 0,
467
+ ttlSeconds: opts.ttl ? parseTtl(opts.ttl) : void 0,
468
+ visibility
469
+ }, config.lifecycle);
470
+ const policy = resolveVisibility(input);
471
+ const memory = store.store({
472
+ ...input,
473
+ visibility: policy.visibility
474
+ });
475
+ logger.info(`Memory stored: ${memory.id}`);
476
+ logger.info(` Type: ${memory.memoryType}`);
477
+ logger.info(` Visibility: ${memory.visibility}`);
478
+ logger.info(` Tags: ${memory.tags.join(", ") || "(none)"}`);
479
+ if (policy.suggestion === "promote") {
480
+ logger.info(
481
+ `
482
+ Hint: This memory might be useful for the team. Use 'unforgit promote ${memory.id}' to share it.`
483
+ );
484
+ }
485
+ } finally {
486
+ store.close();
487
+ }
488
+ });
489
+
490
+ // src/commands/recall.ts
491
+ import { Command as Command3 } from "commander";
492
+ import { loadConfig as loadConfig2, getDbPath as getDbPath3 } from "unforgit-config";
493
+ import { LocalStore as LocalStore3 } from "unforgit-db";
494
+ import { RemoteClient } from "unforgit-config";
495
+ import { mergeAndRank } from "unforgit-core";
496
+ import { resolveLifecycleConfig } from "unforgit-core";
497
+ import { parsePositiveInt } from "unforgit-config";
498
+ var recallCommand = new Command3("recall").description("Recall memories matching a query").argument("<query>", "Search query").option(
499
+ "--types <types>",
500
+ "Comma-separated types (episodic,semantic,procedural)"
501
+ ).option("--tags <tags>", "Comma-separated tags to filter").option("-k, --limit <n>", "Max results", "10").option("--remote-only", "Only query remote").option("--local-only", "Only query local").option("--page <n>", "Page number for pagination", "1").option("--per-page <n>", "Items per page", "10").addHelpText("after", `
502
+ Examples:
503
+ unforgit recall "authentication" Search all memories
504
+ unforgit recall "deploy" --types procedural Filter by type
505
+ unforgit recall "bug" --tags auth,api Filter by tags
506
+ unforgit recall "setup" --local-only Local search only`).action(async (query, opts) => {
507
+ if (!query || !query.trim()) {
508
+ logger.error("Search query cannot be empty.");
509
+ process.exit(EXIT_ERROR);
510
+ }
511
+ const config = loadConfig2();
512
+ const usageTrackingLimit = resolveLifecycleConfig(config.lifecycle).usageBoost.topKToRecord;
513
+ const k = parsePositiveInt(opts.limit, "limit");
514
+ const VALID_TYPES = ["episodic", "semantic", "procedural"];
515
+ const types = opts.types ? opts.types.split(",").map((t) => t.trim()) : void 0;
516
+ if (types) {
517
+ const invalid = types.filter((t) => !VALID_TYPES.includes(t));
518
+ if (invalid.length > 0) {
519
+ logger.error(
520
+ `Invalid memory type(s): ${invalid.join(", ")}. Must be one of: ${VALID_TYPES.join(", ")}`
521
+ );
522
+ process.exit(EXIT_ERROR);
523
+ }
524
+ }
525
+ const tags = opts.tags ? opts.tags.split(",").map((t) => t.trim()) : void 0;
526
+ let localResults = [];
527
+ let remoteResults = [];
528
+ const recallQuery = {
529
+ orgId: config.remote.orgId || "local",
530
+ repoId: config.remote.repoId || "local",
531
+ query,
532
+ types,
533
+ tags,
534
+ k
535
+ };
536
+ if (!opts.remoteOnly) {
537
+ let store;
538
+ try {
539
+ store = new LocalStore3(getDbPath3());
540
+ localResults = store.recall(recallQuery);
541
+ const idsToRecord = localResults.slice(0, usageTrackingLimit).map((result) => result.id);
542
+ if (idsToRecord.length > 0) {
543
+ store.recordUsageBatch(idsToRecord, query);
544
+ }
545
+ } catch {
546
+ if (!opts.localOnly) {
547
+ logger.warn("Local store not available");
548
+ }
549
+ } finally {
550
+ store?.close();
551
+ }
552
+ }
553
+ if (!opts.localOnly && config.remote.url) {
554
+ try {
555
+ const client = new RemoteClient(config.remote.url);
556
+ const response = await client.recall(recallQuery);
557
+ remoteResults = response.results.map((r) => ({
558
+ ...r,
559
+ source: "remote"
560
+ }));
561
+ } catch {
562
+ if (!opts.remoteOnly) {
563
+ logger.warn("Remote API not available");
564
+ }
565
+ }
566
+ }
567
+ const results = mergeAndRank(localResults, remoteResults, k);
568
+ const page = parsePositiveInt(opts.page, "page");
569
+ const perPage = parsePositiveInt(opts.perPage, "per-page");
570
+ const paged = paginate(results, page, perPage);
571
+ if (isJsonMode()) {
572
+ outputJson({
573
+ results: paged.items.map((r) => ({
574
+ id: r.id,
575
+ source: r.source,
576
+ type: r.memoryType,
577
+ score: r.score,
578
+ text: r.text,
579
+ tags: r.tags
580
+ })),
581
+ page: paged.currentPage,
582
+ totalPages: paged.totalPages,
583
+ total: paged.total
584
+ });
585
+ return;
586
+ }
587
+ if (results.length === 0) {
588
+ logger.info("No memories found.");
589
+ return;
590
+ }
591
+ logger.info(`Found ${results.length} memories:
592
+ `);
593
+ for (const r of paged.items) {
594
+ const sourceTag = r.source === "local" ? "[local]" : "[remote]";
595
+ logger.info(
596
+ `${sourceTag} [${r.memoryType}] ${r.id.slice(0, 8)}... (score: ${r.score.toFixed(3)})`
597
+ );
598
+ logger.info(` ${r.text.slice(0, 120)}${r.text.length > 120 ? "..." : ""}`);
599
+ if (r.tags.length > 0) logger.info(` Tags: ${r.tags.join(", ")}`);
600
+ logger.info("");
601
+ }
602
+ if (paged.totalPages > 1) {
603
+ logger.info(`Page ${paged.currentPage}/${paged.totalPages} (${paged.total} total)`);
604
+ }
605
+ });
606
+
607
+ // src/commands/promote.ts
608
+ import { Command as Command4 } from "commander";
609
+ import { loadConfig as loadConfig3, getDbPath as getDbPath4 } from "unforgit-config";
610
+ import { LocalStore as LocalStore4 } from "unforgit-db";
611
+ import { RemoteClient as RemoteClient2 } from "unforgit-config";
612
+ var promoteCommand = new Command4("promote").description("Promote a local memory to remote (shared)").argument("<id>", "Memory ID to promote").option("--to <scope>", "Target scope", "repo").option("--source-pr <url>", "Source PR URL").option("--source-commit <sha>", "Source commit SHA").addHelpText("after", `
613
+ Examples:
614
+ unforgit promote abc123
615
+ unforgit promote abc123 --source-pr https://github.com/org/repo/pull/42`).action(async (id, opts) => {
616
+ const config = loadConfig3();
617
+ if (!config.remote.url) {
618
+ logger.error("Remote URL not configured. Update unforgit.yaml.");
619
+ process.exit(EXIT_ERROR);
620
+ }
621
+ const store = new LocalStore4(getDbPath4());
622
+ try {
623
+ const memory = store.getById(id);
624
+ if (!memory) {
625
+ logger.error(`Memory ${id} not found locally.`);
626
+ process.exit(EXIT_ERROR);
627
+ }
628
+ const sourceRefs = { ...memory.sourceRefs ?? {} };
629
+ if (opts.sourcePr) sourceRefs.pr_url = opts.sourcePr;
630
+ if (opts.sourceCommit) sourceRefs.commit_sha = opts.sourceCommit;
631
+ const client = new RemoteClient2(config.remote.url);
632
+ const result = await client.store({
633
+ orgId: config.remote.orgId || memory.orgId,
634
+ repoId: config.remote.repoId || memory.repoId,
635
+ memoryType: memory.memoryType,
636
+ text: memory.text,
637
+ summary: memory.summary,
638
+ tags: memory.tags,
639
+ sourceRefs,
640
+ confidence: memory.confidence,
641
+ visibility: "repo"
642
+ });
643
+ store.updateVisibility(id, "repo");
644
+ logger.info(`Promoted memory ${id.slice(0, 8)}... to remote.`);
645
+ logger.info(` Remote ID: ${result.id}`);
646
+ logger.info(` Scope: ${opts.to}`);
647
+ } catch (err) {
648
+ logger.error(
649
+ `Promoting memory: ${err instanceof Error ? err.message : String(err)}`
650
+ );
651
+ process.exit(EXIT_ERROR);
652
+ } finally {
653
+ store.close();
654
+ }
655
+ });
656
+
657
+ // src/commands/consolidate.ts
658
+ import { Command as Command5 } from "commander";
659
+ import { loadConfig as loadConfig4 } from "unforgit-config";
660
+ import { RemoteClient as RemoteClient3 } from "unforgit-config";
661
+ import { parsePositiveInt as parsePositiveInt2 } from "unforgit-config";
662
+ var consolidateCommand = new Command5("consolidate").description("Consolidate episodic memories into semantic/procedural").option("--from-pr <url>", "PR URL to consolidate from").option("--from-commit <sha>", "Commit SHA to consolidate from").option("--last-n <n>", "Consolidate the last N memories").action(async (opts) => {
663
+ const config = loadConfig4();
664
+ if (!config.remote.url) {
665
+ logger.error("Remote URL not configured. Update unforgit.yaml.");
666
+ process.exit(EXIT_CONFIG_ERROR);
667
+ }
668
+ const client = new RemoteClient3(config.remote.url);
669
+ const body = {
670
+ orgId: config.remote.orgId,
671
+ repoId: config.remote.repoId
672
+ };
673
+ if (opts.fromPr || opts.fromCommit) {
674
+ body.source = {
675
+ prUrl: opts.fromPr,
676
+ commitSha: opts.fromCommit
677
+ };
678
+ }
679
+ if (opts.lastN) {
680
+ body.lastN = parsePositiveInt2(opts.lastN, "last-n");
681
+ }
682
+ try {
683
+ const result = await client.consolidate(body);
684
+ logger.info("Consolidation complete:");
685
+ logger.info(` Created: ${result.created.length} memories`);
686
+ logger.info(` Superseded: ${result.superseded.length} memories`);
687
+ logger.info(` Processed: ${result.processedCount} total`);
688
+ if (result.created.length > 0) {
689
+ logger.info("\nNew memories:");
690
+ for (const id of result.created) {
691
+ logger.info(` - ${id}`);
692
+ }
693
+ }
694
+ } catch (err) {
695
+ logger.error(
696
+ `Consolidating: ${err instanceof Error ? err.message : err}`
697
+ );
698
+ process.exit(EXIT_ERROR);
699
+ }
700
+ });
701
+
702
+ // src/commands/deprecate.ts
703
+ import { Command as Command6 } from "commander";
704
+ import { loadConfig as loadConfig5, getDbPath as getDbPath5 } from "unforgit-config";
705
+ import { LocalStore as LocalStore5 } from "unforgit-db";
706
+ import { RemoteClient as RemoteClient4 } from "unforgit-config";
707
+ var deprecateCommand = new Command6("deprecate").description("Mark a memory as deprecated").argument("<id>", "Memory ID to deprecate").option("--reason <reason>", "Reason for deprecation").option("--remote", "Deprecate on remote").addHelpText("after", `
708
+ Examples:
709
+ unforgit deprecate abc123 --reason "No longer relevant"
710
+ unforgit deprecate abc123 --remote`).action(async (id, opts) => {
711
+ if (opts.remote) {
712
+ const config = loadConfig5();
713
+ const client = new RemoteClient4(config.remote.url);
714
+ try {
715
+ await client.deprecate(id, opts.reason);
716
+ logger.info(`Deprecated remote memory ${id.slice(0, 8)}...`);
717
+ if (opts.reason) logger.info(` Reason: ${opts.reason}`);
718
+ } catch (err) {
719
+ logger.error(
720
+ err instanceof Error ? err.message : String(err)
721
+ );
722
+ process.exit(EXIT_ERROR);
723
+ }
724
+ return;
725
+ }
726
+ const store = new LocalStore5(getDbPath5());
727
+ try {
728
+ const memory = store.getById(id);
729
+ if (!memory) {
730
+ logger.error(`Memory ${id} not found.`);
731
+ process.exit(EXIT_ERROR);
732
+ }
733
+ if (memory.status === "deprecated") {
734
+ logger.warn(`Memory ${id.slice(0, 8)} is already deprecated.`);
735
+ return;
736
+ }
737
+ const ok = store.deprecate(id, opts.reason);
738
+ if (!ok) {
739
+ logger.error(`Memory ${id} not found.`);
740
+ process.exit(EXIT_ERROR);
741
+ }
742
+ logger.info(`Deprecated local memory ${id.slice(0, 8)}...`);
743
+ if (opts.reason) logger.info(` Reason: ${opts.reason}`);
744
+ } finally {
745
+ store.close();
746
+ }
747
+ });
748
+
749
+ // src/commands/supersede.ts
750
+ import { Command as Command7 } from "commander";
751
+ import { loadConfig as loadConfig6, getDbPath as getDbPath6 } from "unforgit-config";
752
+ import { LocalStore as LocalStore6 } from "unforgit-db";
753
+ import { RemoteClient as RemoteClient5 } from "unforgit-config";
754
+ var supersedeCommand = new Command7("supersede").description("Mark a memory as superseded by another").argument("<old-id>", "Memory ID being superseded").requiredOption("--with <new-id>", "ID of the new memory that replaces it").option("--remote", "Supersede on remote").option("--force", "Skip confirmation").addHelpText("after", `
755
+ Examples:
756
+ unforgit supersede abc123 --with def456
757
+ unforgit supersede abc123 --with def456 --remote`).action(async (oldId, opts) => {
758
+ if (oldId === opts.with) {
759
+ logger.error("A memory cannot supersede itself.");
760
+ process.exit(EXIT_ERROR);
761
+ }
762
+ if (!opts.force) {
763
+ const confirmed = await confirm(
764
+ `Supersede memory ${oldId.slice(0, 8)}... with ${opts.with.slice(0, 8)}...?`
765
+ );
766
+ if (!confirmed) {
767
+ logger.info("Supersede cancelled.");
768
+ return;
769
+ }
770
+ }
771
+ if (opts.remote) {
772
+ const config = loadConfig6();
773
+ const client = new RemoteClient5(config.remote.url);
774
+ try {
775
+ await client.supersede(oldId, opts.with);
776
+ logger.info(
777
+ `Superseded remote memory ${oldId.slice(0, 8)}... with ${opts.with.slice(0, 8)}...`
778
+ );
779
+ } catch (err) {
780
+ logger.error(
781
+ err instanceof Error ? err.message : String(err)
782
+ );
783
+ process.exit(EXIT_ERROR);
784
+ }
785
+ return;
786
+ }
787
+ const store = new LocalStore6(getDbPath6());
788
+ try {
789
+ const ok = store.supersede(oldId, opts.with);
790
+ if (!ok) {
791
+ logger.error(`Memory ${oldId} not found.`);
792
+ process.exit(EXIT_ERROR);
793
+ }
794
+ logger.info(
795
+ `Superseded local memory ${oldId.slice(0, 8)}... with ${opts.with.slice(0, 8)}...`
796
+ );
797
+ } finally {
798
+ store.close();
799
+ }
800
+ });
801
+
802
+ // src/commands/delete.ts
803
+ import { Command as Command8 } from "commander";
804
+ import { loadConfig as loadConfig7, getDbPath as getDbPath7 } from "unforgit-config";
805
+ import { LocalStore as LocalStore7 } from "unforgit-db";
806
+ import { RemoteClient as RemoteClient6 } from "unforgit-config";
807
+ 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", `
808
+ Examples:
809
+ unforgit delete abc12345 Soft delete (can be restored)
810
+ unforgit delete abc12345 --hard Permanent delete
811
+ unforgit delete abc12345 --remote Delete on remote server`).action(async (id, opts) => {
812
+ if (opts.hard && !opts.force) {
813
+ const confirmed = await confirm(
814
+ `Permanently delete memory ${id.slice(0, 8)}...? This cannot be undone.`
815
+ );
816
+ if (!confirmed) {
817
+ logger.info("Delete cancelled.");
818
+ return;
819
+ }
820
+ }
821
+ if (opts.remote) {
822
+ const config = loadConfig7();
823
+ const client = new RemoteClient6(config.remote.url);
824
+ try {
825
+ await client.delete(id, opts.by, opts.hard);
826
+ const action = opts.hard ? "Hard deleted" : "Soft deleted";
827
+ logger.info(`${action} remote memory ${id.slice(0, 8)}...`);
828
+ } catch (err) {
829
+ logger.error(
830
+ err instanceof Error ? err.message : String(err)
831
+ );
832
+ process.exit(EXIT_ERROR);
833
+ }
834
+ return;
835
+ }
836
+ const store = new LocalStore7(getDbPath7());
837
+ try {
838
+ let ok;
839
+ if (opts.hard) {
840
+ ok = store.hardDelete(id);
841
+ } else {
842
+ ok = store.softDelete({ id, deletedBy: opts.by });
843
+ }
844
+ if (!ok) {
845
+ logger.error(`Memory ${id} not found.`);
846
+ process.exit(EXIT_ERROR);
847
+ }
848
+ const action = opts.hard ? "Hard deleted" : "Soft deleted";
849
+ logger.info(`${action} local memory ${id.slice(0, 8)}...`);
850
+ if (!opts.hard) {
851
+ logger.info(" This memory can be restored with 'unforgit restore'.");
852
+ }
853
+ } finally {
854
+ store.close();
855
+ }
856
+ });
857
+ 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) => {
858
+ if (opts.remote) {
859
+ const config = loadConfig7();
860
+ const client = new RemoteClient6(config.remote.url);
861
+ try {
862
+ await client.restore(id);
863
+ logger.info(`Restored remote memory ${id.slice(0, 8)}...`);
864
+ } catch (err) {
865
+ logger.error(
866
+ err instanceof Error ? err.message : String(err)
867
+ );
868
+ process.exit(EXIT_ERROR);
869
+ }
870
+ return;
871
+ }
872
+ const store = new LocalStore7(getDbPath7());
873
+ try {
874
+ const ok = store.restore(id);
875
+ if (!ok) {
876
+ logger.error(`Memory ${id} not found or not deleted.`);
877
+ process.exit(EXIT_ERROR);
878
+ }
879
+ logger.info(`Restored local memory ${id.slice(0, 8)}...`);
880
+ } finally {
881
+ store.close();
882
+ }
883
+ });
884
+
885
+ // src/commands/web.ts
886
+ import { Command as Command9 } from "commander";
887
+ import { spawn } from "child_process";
888
+ import path2 from "path";
889
+ import fs3 from "fs";
890
+ import { isInitialized as isInitialized2 } from "unforgit-config";
891
+ 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) => {
892
+ const cwd4 = process.cwd();
893
+ if (!isInitialized2(cwd4)) {
894
+ logger.error("Unforgit not initialized in this directory. Run 'unforgit init' first.");
895
+ process.exit(EXIT_CONFIG_ERROR);
896
+ }
897
+ const webDir = findWebDir();
898
+ if (!webDir) {
899
+ logger.error("Web dashboard not found. Make sure unforgit is installed correctly.");
900
+ process.exit(EXIT_ERROR);
901
+ }
902
+ const env = {
903
+ ...process.env,
904
+ UNFORGIT_WORKSPACE: cwd4,
905
+ PORT: opts.port
906
+ };
907
+ const dotenvPath = path2.join(cwd4, ".env");
908
+ if (fs3.existsSync(dotenvPath)) {
909
+ const content = fs3.readFileSync(dotenvPath, "utf-8");
910
+ for (const line of content.split("\n")) {
911
+ const match = line.match(/^\s*([^#=]+?)\s*=\s*(.+?)\s*$/);
912
+ if (match) {
913
+ env[match[1]] = match[2].replace(/^["']|["']$/g, "");
914
+ }
915
+ }
916
+ }
917
+ logger.info(`Starting Unforgit web dashboard on port ${opts.port}...`);
918
+ logger.info(`Workspace: ${cwd4}`);
919
+ const hasNextBuild = fs3.existsSync(path2.join(webDir, ".next"));
920
+ const cmd = hasNextBuild ? "next" : "next";
921
+ const args = hasNextBuild ? ["start", "-p", opts.port] : ["dev", "-p", opts.port];
922
+ const nextBin = path2.join(webDir, "node_modules", ".bin", "next");
923
+ const finalCmd = fs3.existsSync(nextBin) ? nextBin : cmd;
924
+ const child = spawn(finalCmd, args, {
925
+ cwd: webDir,
926
+ env,
927
+ stdio: "inherit"
928
+ });
929
+ if (opts.open !== false) {
930
+ setTimeout(() => {
931
+ const url = `http://localhost:${opts.port}`;
932
+ const openCmd = process.platform === "darwin" ? "open" : process.platform === "win32" ? "start" : "xdg-open";
933
+ spawn(openCmd, [url], { stdio: "ignore", detached: true }).unref();
934
+ }, 2e3);
935
+ }
936
+ child.on("error", (err) => {
937
+ logger.fatal(`Failed to start web dashboard: ${err.message}`);
938
+ process.exit(EXIT_ERROR);
939
+ });
940
+ child.on("exit", (code) => {
941
+ process.exit(code ?? 0);
942
+ });
943
+ });
944
+ function findWebDir() {
945
+ const candidates = [
946
+ path2.resolve(import.meta.dirname, "../../../web"),
947
+ path2.resolve(import.meta.dirname, "../../web"),
948
+ path2.join(process.cwd(), "web")
949
+ ];
950
+ for (const dir of candidates) {
951
+ if (fs3.existsSync(dir) && fs3.existsSync(path2.join(dir, "package.json"))) {
952
+ return dir;
953
+ }
954
+ }
955
+ return null;
956
+ }
957
+
958
+ // src/commands/link.ts
959
+ import { Command as Command10 } from "commander";
960
+ import { loadConfig as loadConfig8, getDbPath as getDbPath8 } from "unforgit-config";
961
+ import { LocalStore as LocalStore8 } from "unforgit-db";
962
+ import { RemoteClient as RemoteClient7 } from "unforgit-config";
963
+ var VALID_LINK_TYPES = [
964
+ "related_to",
965
+ "derived_from",
966
+ "contradicts",
967
+ "depends_on"
968
+ ];
969
+ 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(
970
+ "--type <link-type>",
971
+ "Link type (related_to, derived_from, contradicts, depends_on)"
972
+ ).option("--remote", "Create link on remote").addHelpText("after", `
973
+ Examples:
974
+ unforgit link abc123 def456 --type related_to
975
+ unforgit link abc123 def456 --type derived_from --remote`).action(async (sourceId, targetId, opts) => {
976
+ if (!VALID_LINK_TYPES.includes(opts.type)) {
977
+ logger.error(
978
+ `Invalid link type "${opts.type}". Must be one of: ${VALID_LINK_TYPES.join(", ")}`
979
+ );
980
+ process.exit(EXIT_ERROR);
981
+ }
982
+ if (sourceId === targetId) {
983
+ logger.error("Cannot link a memory to itself.");
984
+ process.exit(EXIT_ERROR);
985
+ }
986
+ if (opts.remote) {
987
+ const config = loadConfig8();
988
+ const client = new RemoteClient7(config.remote.url);
989
+ try {
990
+ const result = await client.link(sourceId, targetId, opts.type);
991
+ logger.info(
992
+ `Linked remote: ${sourceId.slice(0, 8)} --[${opts.type}]--> ${targetId.slice(0, 8)} (${result.link.id.slice(0, 8)})`
993
+ );
994
+ } catch (err) {
995
+ logger.error(
996
+ err instanceof Error ? err.message : String(err)
997
+ );
998
+ process.exit(EXIT_ERROR);
999
+ }
1000
+ return;
1001
+ }
1002
+ const store = new LocalStore8(getDbPath8());
1003
+ try {
1004
+ const source = store.getById(sourceId);
1005
+ if (!source) {
1006
+ logger.error(`Source memory ${sourceId} not found.`);
1007
+ process.exit(EXIT_ERROR);
1008
+ }
1009
+ if (source.status === "deleted") {
1010
+ logger.error(`Source memory ${sourceId.slice(0, 8)} is deleted. Restore it first.`);
1011
+ process.exit(EXIT_ERROR);
1012
+ }
1013
+ const target = store.getById(targetId);
1014
+ if (!target) {
1015
+ logger.error(`Target memory ${targetId} not found.`);
1016
+ process.exit(EXIT_ERROR);
1017
+ }
1018
+ if (target.status === "deleted") {
1019
+ logger.error(`Target memory ${targetId.slice(0, 8)} is deleted. Restore it first.`);
1020
+ process.exit(EXIT_ERROR);
1021
+ }
1022
+ const link = store.link({
1023
+ sourceId,
1024
+ targetId,
1025
+ linkType: opts.type
1026
+ });
1027
+ logger.info(
1028
+ `Linked: ${sourceId.slice(0, 8)} --[${opts.type}]--> ${targetId.slice(0, 8)} (${link.id.slice(0, 8)})`
1029
+ );
1030
+ } catch (err) {
1031
+ logger.error(
1032
+ err instanceof Error ? err.message : String(err)
1033
+ );
1034
+ process.exit(EXIT_ERROR);
1035
+ } finally {
1036
+ store.close();
1037
+ }
1038
+ });
1039
+ 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(
1040
+ "--type <link-type>",
1041
+ "Link type (related_to, derived_from, contradicts, depends_on)"
1042
+ ).option("--remote", "Remove link on remote").action(async (sourceId, targetId, opts) => {
1043
+ if (!VALID_LINK_TYPES.includes(opts.type)) {
1044
+ logger.error(
1045
+ `Invalid link type "${opts.type}". Must be one of: ${VALID_LINK_TYPES.join(", ")}`
1046
+ );
1047
+ process.exit(EXIT_ERROR);
1048
+ }
1049
+ if (opts.remote) {
1050
+ const config = loadConfig8();
1051
+ const client = new RemoteClient7(config.remote.url);
1052
+ try {
1053
+ await client.unlink(sourceId, targetId, opts.type);
1054
+ logger.info(
1055
+ `Unlinked remote: ${sourceId.slice(0, 8)} --[${opts.type}]--> ${targetId.slice(0, 8)}`
1056
+ );
1057
+ } catch (err) {
1058
+ logger.error(
1059
+ err instanceof Error ? err.message : String(err)
1060
+ );
1061
+ process.exit(EXIT_ERROR);
1062
+ }
1063
+ return;
1064
+ }
1065
+ const store = new LocalStore8(getDbPath8());
1066
+ try {
1067
+ const ok = store.unlink(sourceId, targetId, opts.type);
1068
+ if (!ok) {
1069
+ logger.error("Link not found.");
1070
+ process.exit(EXIT_ERROR);
1071
+ }
1072
+ logger.info(
1073
+ `Unlinked: ${sourceId.slice(0, 8)} --[${opts.type}]--> ${targetId.slice(0, 8)}`
1074
+ );
1075
+ } finally {
1076
+ store.close();
1077
+ }
1078
+ });
1079
+ var linksCommand = new Command10("links").description("List all links for a memory").argument("<memory-id>", "Memory ID to get links for").option(
1080
+ "--type <link-type>",
1081
+ "Filter by link type (related_to, derived_from, contradicts, depends_on)"
1082
+ ).option("--remote", "List links on remote").action(async (memoryId, opts) => {
1083
+ if (opts.type && !VALID_LINK_TYPES.includes(opts.type)) {
1084
+ logger.error(
1085
+ `Invalid link type "${opts.type}". Must be one of: ${VALID_LINK_TYPES.join(", ")}`
1086
+ );
1087
+ process.exit(EXIT_ERROR);
1088
+ }
1089
+ if (opts.remote) {
1090
+ const config = loadConfig8();
1091
+ const client = new RemoteClient7(config.remote.url);
1092
+ try {
1093
+ const result = await client.getLinks(memoryId, opts.type);
1094
+ if (isJsonMode()) {
1095
+ outputJson(result);
1096
+ return;
1097
+ }
1098
+ if (result.links.length === 0) {
1099
+ logger.info("No links found.");
1100
+ return;
1101
+ }
1102
+ logger.info(`Found ${result.links.length} links:
1103
+ `);
1104
+ for (const l of result.links) {
1105
+ const direction = l.sourceId === memoryId ? `--[${l.linkType}]--> ${l.targetId.slice(0, 8)}` : `<--[${l.linkType}]-- ${l.sourceId.slice(0, 8)}`;
1106
+ logger.info(` ${l.id.slice(0, 8)}: ${direction}`);
1107
+ }
1108
+ } catch (err) {
1109
+ logger.error(
1110
+ err instanceof Error ? err.message : String(err)
1111
+ );
1112
+ process.exit(EXIT_ERROR);
1113
+ }
1114
+ return;
1115
+ }
1116
+ const store = new LocalStore8(getDbPath8());
1117
+ try {
1118
+ const links = store.getLinks({
1119
+ memoryId,
1120
+ linkType: opts.type
1121
+ });
1122
+ if (isJsonMode()) {
1123
+ outputJson({
1124
+ links: links.map((l) => ({
1125
+ id: l.id,
1126
+ sourceId: l.sourceId,
1127
+ targetId: l.targetId,
1128
+ linkType: l.linkType
1129
+ }))
1130
+ });
1131
+ return;
1132
+ }
1133
+ if (links.length === 0) {
1134
+ logger.info("No links found.");
1135
+ return;
1136
+ }
1137
+ logger.info(`Found ${links.length} links:
1138
+ `);
1139
+ for (const l of links) {
1140
+ const direction = l.sourceId === memoryId ? `--[${l.linkType}]--> ${l.targetId.slice(0, 8)}` : `<--[${l.linkType}]-- ${l.sourceId.slice(0, 8)}`;
1141
+ logger.info(` ${l.id.slice(0, 8)}: ${direction}`);
1142
+ }
1143
+ } finally {
1144
+ store.close();
1145
+ }
1146
+ });
1147
+
1148
+ // src/commands/merge.ts
1149
+ import { Command as Command11 } from "commander";
1150
+ import { LocalStore as LocalStore9 } from "unforgit-db";
1151
+ import { loadConfig as loadConfig9, getDbPath as getDbPath9, isInitialized as isInitialized3 } from "unforgit-config";
1152
+ import {
1153
+ parseThreshold,
1154
+ parsePositiveInt as parsePositiveInt3,
1155
+ validateMemoryType as validateMemoryType2
1156
+ } from "unforgit-config";
1157
+ var cwd = process.cwd();
1158
+ var mergeCommand = new Command11("merge").description(
1159
+ "Consolidate multiple local memories into one unified memory while preserving history"
1160
+ ).argument("<ids...>", "Memory IDs to consolidate (minimum 2)").requiredOption(
1161
+ "-t, --text <text>",
1162
+ "Consolidated text combining insights from source memories"
1163
+ ).option(
1164
+ "--type <type>",
1165
+ "Memory type for consolidated memory (episodic, semantic, procedural)"
1166
+ ).option("--tags <tags>", "Comma-separated tags for the consolidated memory").option(
1167
+ "--no-supersede",
1168
+ "Do not mark original memories as superseded"
1169
+ ).action(async (ids, opts) => {
1170
+ if (!isInitialized3(cwd)) {
1171
+ logger.error("Unforgit not initialized. Run 'unforgit init' first.");
1172
+ process.exit(EXIT_CONFIG_ERROR);
1173
+ }
1174
+ if (ids.length < 2) {
1175
+ logger.error("At least 2 memory IDs are required for merge.");
1176
+ process.exit(EXIT_ERROR);
1177
+ }
1178
+ if (opts.type && !validateMemoryType2(opts.type)) {
1179
+ logger.error("--type must be one of: episodic, semantic, procedural");
1180
+ process.exit(EXIT_ERROR);
1181
+ }
1182
+ const config = loadConfig9(cwd);
1183
+ const store = new LocalStore9(getDbPath9(cwd));
1184
+ try {
1185
+ for (const id of ids) {
1186
+ const mem = store.getById(id);
1187
+ if (!mem) {
1188
+ logger.error(`Memory ${id} not found.`);
1189
+ process.exit(EXIT_ERROR);
1190
+ }
1191
+ if (mem.status === "deleted") {
1192
+ logger.error(`Memory ${id.slice(0, 8)} is deleted. Restore it before merging.`);
1193
+ process.exit(EXIT_ERROR);
1194
+ }
1195
+ if (mem.status === "superseded") {
1196
+ logger.error(`Memory ${id.slice(0, 8)} is already superseded. Cannot merge superseded memories.`);
1197
+ process.exit(EXIT_ERROR);
1198
+ }
1199
+ }
1200
+ const result = store.consolidateMemories({
1201
+ orgId: config.remote.orgId || "local",
1202
+ repoId: config.remote.repoId || "local",
1203
+ sourceIds: ids,
1204
+ consolidatedText: opts.text,
1205
+ memoryType: opts.type,
1206
+ tags: opts.tags ? opts.tags.split(",").map((t) => t.trim()) : void 0,
1207
+ preserveOriginals: opts.supersede !== false
1208
+ });
1209
+ logger.info("Merge complete!");
1210
+ logger.info(` Consolidated ID: ${result.consolidatedId}`);
1211
+ logger.info(` Version: ${result.version}`);
1212
+ logger.info(` Sources preserved: ${result.sourcesPreserved}`);
1213
+ logger.info(
1214
+ ` Source IDs: ${result.sourceIds.map((id) => id.slice(0, 8)).join(", ")}`
1215
+ );
1216
+ logger.info("");
1217
+ logger.info(
1218
+ "Original memories are linked via 'derived_from' and marked as 'superseded'."
1219
+ );
1220
+ logger.info("Use 'unforgit history <id>' to view the consolidation history.");
1221
+ } catch (err) {
1222
+ logger.error(
1223
+ `Merging memories: ${err instanceof Error ? err.message : err}`
1224
+ );
1225
+ process.exit(EXIT_ERROR);
1226
+ } finally {
1227
+ store.close();
1228
+ }
1229
+ });
1230
+ var remergeCommand = new Command11("remerge").description(
1231
+ "Update an existing consolidation with new information or additional sources"
1232
+ ).argument("<consolidation-id>", "ID of existing consolidated memory to update").requiredOption("-t, --text <text>", "Updated consolidated text").option(
1233
+ "--add <ids>",
1234
+ "Comma-separated IDs of additional memories to include"
1235
+ ).option("--tags <tags>", "Comma-separated tags (keeps existing if not provided)").action(async (consolidationId, opts) => {
1236
+ if (!isInitialized3(cwd)) {
1237
+ logger.error("Unforgit not initialized. Run 'unforgit init' first.");
1238
+ process.exit(EXIT_CONFIG_ERROR);
1239
+ }
1240
+ const config = loadConfig9(cwd);
1241
+ const store = new LocalStore9(getDbPath9(cwd));
1242
+ try {
1243
+ const result = store.reconsolidate({
1244
+ orgId: config.remote.orgId || "local",
1245
+ repoId: config.remote.repoId || "local",
1246
+ existingConsolidationId: consolidationId,
1247
+ additionalSourceIds: opts.add ? opts.add.split(",").map((t) => t.trim()) : void 0,
1248
+ newText: opts.text,
1249
+ tags: opts.tags ? opts.tags.split(",").map((t) => t.trim()) : void 0
1250
+ });
1251
+ logger.info("Remerge complete!");
1252
+ logger.info(` New consolidated ID: ${result.consolidatedId}`);
1253
+ logger.info(` New version: ${result.version}`);
1254
+ logger.info(` Total sources: ${result.sourcesPreserved}`);
1255
+ logger.info(
1256
+ ` Previous consolidation: ${consolidationId.slice(0, 8)} (now superseded)`
1257
+ );
1258
+ logger.info("");
1259
+ logger.info("Use 'unforgit history <id>' to view all versions.");
1260
+ } catch (err) {
1261
+ logger.error(
1262
+ `Remerging: ${err instanceof Error ? err.message : err}`
1263
+ );
1264
+ process.exit(EXIT_ERROR);
1265
+ } finally {
1266
+ store.close();
1267
+ }
1268
+ });
1269
+ 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(
1270
+ "-k, --limit <n>",
1271
+ "Max number of similar memories to return",
1272
+ "10"
1273
+ ).option(
1274
+ "--threshold <score>",
1275
+ "Minimum similarity score (0-1)",
1276
+ "0.3"
1277
+ ).addHelpText("after", `
1278
+ Examples:
1279
+ unforgit similar abc123
1280
+ unforgit similar abc123 --threshold 0.5 -k 5`).action(async (memoryId, opts) => {
1281
+ if (!isInitialized3(cwd)) {
1282
+ logger.error("Unforgit not initialized. Run 'unforgit init' first.");
1283
+ process.exit(EXIT_CONFIG_ERROR);
1284
+ }
1285
+ const config = loadConfig9(cwd);
1286
+ const store = new LocalStore9(getDbPath9(cwd));
1287
+ try {
1288
+ const similar = store.findSimilar({
1289
+ orgId: config.remote.orgId || "local",
1290
+ repoId: config.remote.repoId || "local",
1291
+ memoryId,
1292
+ threshold: parseThreshold(opts.threshold),
1293
+ k: parsePositiveInt3(opts.limit, "limit")
1294
+ });
1295
+ if (isJsonMode()) {
1296
+ outputJson({
1297
+ results: similar.map((m) => ({
1298
+ id: m.id,
1299
+ type: m.memoryType,
1300
+ score: m.score,
1301
+ text: m.text,
1302
+ tags: m.tags
1303
+ }))
1304
+ });
1305
+ return;
1306
+ }
1307
+ if (similar.length === 0) {
1308
+ logger.info("No similar memories found.");
1309
+ return;
1310
+ }
1311
+ logger.info(`Found ${similar.length} similar memories:
1312
+ `);
1313
+ for (const mem of similar) {
1314
+ logger.info(
1315
+ `[${mem.memoryType}] ${mem.id.slice(0, 8)} (score: ${mem.score.toFixed(3)})`
1316
+ );
1317
+ logger.info(` ${mem.text.slice(0, 100)}${mem.text.length > 100 ? "..." : ""}`);
1318
+ if (mem.tags.length > 0) {
1319
+ logger.info(` Tags: ${mem.tags.join(", ")}`);
1320
+ }
1321
+ logger.info("");
1322
+ }
1323
+ logger.info(`Tip: Use 'unforgit merge <id1> <id2> ... -t "merged text"' to consolidate.`);
1324
+ } catch (err) {
1325
+ logger.error(
1326
+ `Finding similar: ${err instanceof Error ? err.message : err}`
1327
+ );
1328
+ process.exit(EXIT_ERROR);
1329
+ } finally {
1330
+ store.close();
1331
+ }
1332
+ });
1333
+ var historyCommand = new Command11("history").description("Show consolidation history for a memory").argument("<memory-id>", "Memory ID to show history for").action(async (memoryId) => {
1334
+ if (!isInitialized3(cwd)) {
1335
+ logger.error("Unforgit not initialized. Run 'unforgit init' first.");
1336
+ process.exit(EXIT_CONFIG_ERROR);
1337
+ }
1338
+ const store = new LocalStore9(getDbPath9(cwd));
1339
+ try {
1340
+ const memory = store.getById(memoryId);
1341
+ if (!memory) {
1342
+ logger.error("Memory not found.");
1343
+ process.exit(EXIT_ERROR);
1344
+ }
1345
+ logger.info(`Memory: ${memory.id.slice(0, 8)}`);
1346
+ logger.info(`Type: ${memory.memoryType}`);
1347
+ logger.info(`Status: ${memory.status}`);
1348
+ logger.info(`Is Consolidation: ${memory.isConsolidation ? "Yes" : "No"}`);
1349
+ if (memory.isConsolidation) {
1350
+ logger.info(`Version: ${memory.consolidationVersion ?? 1}`);
1351
+ logger.info(`Text: ${memory.text}`);
1352
+ const sources = store.getConsolidatedSources(memoryId);
1353
+ if (sources.length > 0) {
1354
+ logger.info("\nSource memories:");
1355
+ for (const src of sources) {
1356
+ logger.info(
1357
+ ` \u2514\u2500 [${src.memoryType}] ${src.id.slice(0, 8)}: ${src.text.slice(0, 60)}...`
1358
+ );
1359
+ }
1360
+ }
1361
+ const history = store.getConsolidationHistory(memoryId);
1362
+ const previousVersions = history.filter((h) => h.isConsolidation);
1363
+ if (previousVersions.length > 0) {
1364
+ logger.info("\nPrevious consolidation versions:");
1365
+ for (const prev of previousVersions) {
1366
+ logger.info(
1367
+ ` \u2514\u2500 v${prev.consolidationVersion ?? 1} (${prev.id.slice(0, 8)}): ${prev.text.slice(0, 60)}...`
1368
+ );
1369
+ }
1370
+ }
1371
+ } else {
1372
+ logger.info(`Text: ${memory.text}`);
1373
+ const links = store.getLinks({ memoryId, linkType: "derived_from" });
1374
+ const consolidations = links.filter((l) => l.targetId === memoryId);
1375
+ if (consolidations.length > 0) {
1376
+ logger.info("\nIncluded in consolidations:");
1377
+ for (const link of consolidations) {
1378
+ const consol = store.getById(link.sourceId);
1379
+ if (consol) {
1380
+ logger.info(
1381
+ ` \u2514\u2500 v${consol.consolidationVersion ?? 1} (${consol.id.slice(0, 8)}): ${consol.text.slice(0, 60)}...`
1382
+ );
1383
+ }
1384
+ }
1385
+ }
1386
+ }
1387
+ } catch (err) {
1388
+ logger.error(
1389
+ `Fetching history: ${err instanceof Error ? err.message : err}`
1390
+ );
1391
+ process.exit(EXIT_ERROR);
1392
+ } finally {
1393
+ store.close();
1394
+ }
1395
+ });
1396
+
1397
+ // src/commands/auto-consolidate.ts
1398
+ import { Command as Command12 } from "commander";
1399
+ import { LocalStore as LocalStore10 } from "unforgit-db";
1400
+ import { loadConfig as loadConfig10, getDbPath as getDbPath10, isInitialized as isInitialized4 } from "unforgit-config";
1401
+ import {
1402
+ findConsolidationCandidates,
1403
+ executeConsolidation,
1404
+ formatCandidatePreview
1405
+ } from "unforgit-core";
1406
+ import * as readline2 from "readline";
1407
+ import {
1408
+ parseThreshold as parseThreshold2,
1409
+ parsePositiveInt as parsePositiveInt4,
1410
+ validateMemoryType as validateMemoryType3
1411
+ } from "unforgit-config";
1412
+ var cwd2 = process.cwd();
1413
+ function createReadlineInterface() {
1414
+ return readline2.createInterface({
1415
+ input: process.stdin,
1416
+ output: process.stdout
1417
+ });
1418
+ }
1419
+ async function askConfirmation(rl, question) {
1420
+ return new Promise((resolve) => {
1421
+ rl.question(question, (answer) => {
1422
+ resolve(answer.toLowerCase() === "y" || answer.toLowerCase() === "yes");
1423
+ });
1424
+ });
1425
+ }
1426
+ var autoConsolidateCommand = new Command12("auto-consolidate").description(
1427
+ "Automatically find and consolidate similar memories using AI"
1428
+ ).option(
1429
+ "--threshold <score>",
1430
+ "Minimum similarity score (0-1)",
1431
+ "0.4"
1432
+ ).option(
1433
+ "--min-group <n>",
1434
+ "Minimum group size for consolidation",
1435
+ "2"
1436
+ ).option(
1437
+ "--max-groups <n>",
1438
+ "Maximum number of groups to process",
1439
+ "10"
1440
+ ).option(
1441
+ "--type <type>",
1442
+ "Filter by memory type (episodic, semantic, procedural)"
1443
+ ).option(
1444
+ "--dry-run",
1445
+ "Show preview without consolidating"
1446
+ ).option(
1447
+ "-y, --yes",
1448
+ "Skip confirmation prompts"
1449
+ ).option(
1450
+ "--model <model>",
1451
+ "OpenAI model to use",
1452
+ "gpt-5.4"
1453
+ ).option(
1454
+ "--no-preserve",
1455
+ "Do not preserve original memories (hard delete)"
1456
+ ).action(async (opts) => {
1457
+ if (!isInitialized4(cwd2)) {
1458
+ logger.error("Unforgit not initialized. Run 'unforgit init' first.");
1459
+ process.exit(EXIT_CONFIG_ERROR);
1460
+ }
1461
+ if (opts.type && !validateMemoryType3(opts.type)) {
1462
+ logger.error("--type must be one of: episodic, semantic, procedural");
1463
+ process.exit(EXIT_ERROR);
1464
+ }
1465
+ const config = loadConfig10(cwd2);
1466
+ const store = new LocalStore10(getDbPath10(cwd2));
1467
+ const orgId = config.remote.orgId || "local";
1468
+ const repoId = config.remote.repoId || "local";
1469
+ const threshold = parseThreshold2(opts.threshold);
1470
+ const minGroupSize = parsePositiveInt4(opts.minGroup, "min-group");
1471
+ const maxGroups = parsePositiveInt4(opts.maxGroups, "max-groups");
1472
+ const types = opts.type ? [opts.type] : void 0;
1473
+ try {
1474
+ logger.info("Scanning memories for consolidation candidates...\n");
1475
+ const result = findConsolidationCandidates(store, orgId, repoId, {
1476
+ threshold,
1477
+ minGroupSize,
1478
+ maxGroups,
1479
+ types,
1480
+ excludeConsolidations: true
1481
+ });
1482
+ logger.info(`Scanned ${result.totalMemoriesScanned} active memories`);
1483
+ logger.info(`Found ${result.totalCandidateGroups} potential groups`);
1484
+ logger.info(`Showing top ${result.candidates.length} candidates
1485
+ `);
1486
+ if (result.candidates.length === 0) {
1487
+ logger.info("No consolidation candidates found.");
1488
+ logger.info(
1489
+ "Try lowering the threshold (--threshold 0.3) or the minimum group size (--min-group 2)."
1490
+ );
1491
+ return;
1492
+ }
1493
+ for (let i = 0; i < result.candidates.length; i++) {
1494
+ const candidate = result.candidates[i];
1495
+ logger.info(`
1496
+ ${"=".repeat(60)}`);
1497
+ logger.info(`Candidate ${i + 1}/${result.candidates.length}`);
1498
+ logger.info("=".repeat(60));
1499
+ logger.info(formatCandidatePreview(candidate));
1500
+ }
1501
+ if (opts.dryRun) {
1502
+ logger.info("\n[Dry run mode - no changes made]");
1503
+ logger.info(
1504
+ "Remove --dry-run flag to consolidate these memories."
1505
+ );
1506
+ return;
1507
+ }
1508
+ const openaiKey = process.env.OPENAI_API_KEY;
1509
+ if (!openaiKey) {
1510
+ logger.error("OpenAI API key not set.");
1511
+ logger.error("Set the OPENAI_API_KEY environment variable.");
1512
+ process.exit(EXIT_ERROR);
1513
+ }
1514
+ process.env.OPENAI_API_KEY = openaiKey;
1515
+ const rl = createReadlineInterface();
1516
+ let consolidatedCount = 0;
1517
+ let skippedCount = 0;
1518
+ try {
1519
+ for (let i = 0; i < result.candidates.length; i++) {
1520
+ const candidate = result.candidates[i];
1521
+ if (!opts.yes) {
1522
+ logger.info(`
1523
+ ${"\u2500".repeat(60)}`);
1524
+ logger.info(`Processing candidate ${i + 1}/${result.candidates.length}`);
1525
+ logger.info(formatCandidatePreview(candidate));
1526
+ const shouldConsolidate = await askConfirmation(
1527
+ rl,
1528
+ "\nConsolidate this group? [y/N] "
1529
+ );
1530
+ if (!shouldConsolidate) {
1531
+ logger.info("Skipped.");
1532
+ skippedCount++;
1533
+ logger.progress(i + 1, result.candidates.length, "candidates");
1534
+ continue;
1535
+ }
1536
+ }
1537
+ logger.info("\nGenerating consolidated text with AI...");
1538
+ try {
1539
+ const execResult = await executeConsolidation(
1540
+ store,
1541
+ candidate,
1542
+ orgId,
1543
+ repoId,
1544
+ {
1545
+ model: opts.model,
1546
+ preserveOriginals: opts.preserve !== false
1547
+ }
1548
+ );
1549
+ logger.info("\nConsolidation complete!");
1550
+ logger.info(` New memory ID: ${execResult.consolidatedId}`);
1551
+ logger.info(` Type: ${execResult.memoryType}`);
1552
+ logger.info(` Tags: ${execResult.suggestedTags.join(", ") || "none"}`);
1553
+ logger.info(` Sources consolidated: ${execResult.sourceIds.length}`);
1554
+ logger.info(`
1555
+ Generated text:`);
1556
+ logger.info(` ${execResult.generatedText}`);
1557
+ consolidatedCount++;
1558
+ } catch (err) {
1559
+ logger.error(
1560
+ `Consolidating: ${err instanceof Error ? err.message : err}`
1561
+ );
1562
+ skippedCount++;
1563
+ }
1564
+ logger.progress(i + 1, result.candidates.length, "candidates");
1565
+ }
1566
+ } finally {
1567
+ rl.close();
1568
+ }
1569
+ logger.info(`
1570
+ ${"=".repeat(60)}`);
1571
+ logger.info("Summary");
1572
+ logger.info("=".repeat(60));
1573
+ logger.info(`Consolidated: ${consolidatedCount} groups`);
1574
+ logger.info(`Skipped: ${skippedCount} groups`);
1575
+ logger.info(
1576
+ `
1577
+ Original memories are preserved with status 'superseded'.`
1578
+ );
1579
+ logger.info("Use 'unforgit history <id>' to view consolidation history.");
1580
+ } catch (err) {
1581
+ logger.error(
1582
+ `Auto-consolidate: ${err instanceof Error ? err.message : err}`
1583
+ );
1584
+ process.exit(EXIT_ERROR);
1585
+ } finally {
1586
+ store.close();
1587
+ }
1588
+ });
1589
+
1590
+ // src/commands/unconsolidate.ts
1591
+ import { Command as Command13 } from "commander";
1592
+ import { getDbPath as getDbPath11, isInitialized as isInitialized5 } from "unforgit-config";
1593
+ import { LocalStore as LocalStore11 } from "unforgit-db";
1594
+ 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", `
1595
+ Examples:
1596
+ unforgit unconsolidate abc123 --dry-run Preview what would be restored
1597
+ unforgit unconsolidate abc123 Revert a consolidation`).action(async (consolidationId, opts) => {
1598
+ const cwd4 = process.cwd();
1599
+ if (!isInitialized5(cwd4)) {
1600
+ logger.error("Unforgit not initialized. Run 'unforgit init' first.");
1601
+ process.exit(EXIT_CONFIG_ERROR);
1602
+ }
1603
+ const dbPath = getDbPath11(cwd4);
1604
+ const store = new LocalStore11(dbPath);
1605
+ try {
1606
+ const memory = store.getById(consolidationId);
1607
+ if (!memory) {
1608
+ logger.error(`Memory not found: ${consolidationId}`);
1609
+ process.exit(EXIT_ERROR);
1610
+ }
1611
+ if (!memory.isConsolidation) {
1612
+ logger.error(`Memory ${consolidationId} is not a consolidation.`);
1613
+ logger.error("Only consolidated memories can be unconsolidated.");
1614
+ process.exit(EXIT_ERROR);
1615
+ }
1616
+ const sourceLinks = store.getLinks({ memoryId: consolidationId, linkType: "derived_from" });
1617
+ const sourceIds = sourceLinks.filter((l) => l.sourceId === consolidationId).map((l) => l.targetId);
1618
+ logger.info(`Consolidation: ${consolidationId.slice(0, 8)}`);
1619
+ logger.info(`Version: ${memory.consolidationVersion ?? 1}`);
1620
+ logger.info(`Source memories: ${sourceIds.length}`);
1621
+ if (sourceIds.length === 0) {
1622
+ logger.error("No source memories found for this consolidation.");
1623
+ process.exit(EXIT_ERROR);
1624
+ }
1625
+ logger.info("Source memories to restore:");
1626
+ for (const sourceId of sourceIds) {
1627
+ const source = store.getById(sourceId);
1628
+ if (source) {
1629
+ const status = source.status === "superseded" ? "will restore" : `${source.status} (no change)`;
1630
+ const preview = source.text.slice(0, 60) + (source.text.length > 60 ? "..." : "");
1631
+ logger.info(` - ${sourceId.slice(0, 8)} [${source.memoryType}] (${status})`);
1632
+ logger.info(` ${preview}`);
1633
+ }
1634
+ }
1635
+ logger.info("");
1636
+ if (opts.dryRun) {
1637
+ logger.info("[Dry run - no changes made]");
1638
+ logger.info("Run without --dry-run to execute the unconsolidation.");
1639
+ return;
1640
+ }
1641
+ if (!opts.force) {
1642
+ const confirmed = await confirm(
1643
+ `Revert consolidation ${consolidationId.slice(0, 8)}... and restore ${sourceIds.length} source memories?`
1644
+ );
1645
+ if (!confirmed) {
1646
+ logger.info("Unconsolidation cancelled.");
1647
+ return;
1648
+ }
1649
+ }
1650
+ const result = store.unconsolidate(consolidationId);
1651
+ logger.info("Unconsolidation complete:");
1652
+ logger.info(` Restored: ${result.restoredIds.length} memories`);
1653
+ logger.info(` Links removed: ${result.linksRemoved}`);
1654
+ logger.info(` Consolidation deleted: ${result.consolidationDeleted ? "Yes" : "No"}`);
1655
+ if (result.restoredIds.length > 0) {
1656
+ logger.info("\nRestored memories:");
1657
+ for (const id of result.restoredIds) {
1658
+ logger.info(` - ${id.slice(0, 8)}`);
1659
+ }
1660
+ }
1661
+ logger.info("\nThe consolidated memory has been soft-deleted and can be restored with 'unforgit restore'.");
1662
+ } finally {
1663
+ store.close();
1664
+ }
1665
+ });
1666
+
1667
+ // src/commands/status.ts
1668
+ import { Command as Command14 } from "commander";
1669
+ import { loadConfig as loadConfig11, getDbPath as getDbPath12, isInitialized as isInitialized6 } from "unforgit-config";
1670
+ import { LocalStore as LocalStore12 } from "unforgit-db";
1671
+ 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", `
1672
+ Examples:
1673
+ unforgit status Show full sync status
1674
+ unforgit status -s Short format
1675
+ unforgit status --json Machine-readable output`).action((opts) => {
1676
+ if (!isInitialized6()) {
1677
+ logger.fatal("not an unforgit repository (or any of the parent directories)");
1678
+ logger.fatal("Run 'unforgit init' to initialize.");
1679
+ process.exit(EXIT_CONFIG_ERROR);
1680
+ }
1681
+ const config = loadConfig11();
1682
+ const store = new LocalStore12(getDbPath12());
1683
+ try {
1684
+ const orgId = config.remote.orgId || "local";
1685
+ const repoId = config.remote.repoId || "local";
1686
+ const remoteUrl = config.remote.url;
1687
+ const remoteName = "origin";
1688
+ const pendingPush = store.getPendingPush();
1689
+ const conflicts = store.getConflicts();
1690
+ const untracked = store.getUntrackedMemories(orgId, repoId);
1691
+ const summary = store.getSyncSummary(orgId, repoId);
1692
+ if (isJsonMode()) {
1693
+ outputJson({
1694
+ remote: remoteUrl || null,
1695
+ pendingPush: pendingPush.length,
1696
+ conflicts: conflicts.length,
1697
+ untracked: untracked.length,
1698
+ synced: summary.synced
1699
+ });
1700
+ return;
1701
+ }
1702
+ if (opts.short) {
1703
+ printShortStatus(pendingPush, conflicts, untracked);
1704
+ } else {
1705
+ printLongStatus(remoteName, remoteUrl, pendingPush, conflicts, untracked, summary);
1706
+ }
1707
+ } finally {
1708
+ store.close();
1709
+ }
1710
+ });
1711
+ function printShortStatus(pendingPush, conflicts, untracked) {
1712
+ for (const { memory } of pendingPush) {
1713
+ logger.info(`M ${memory.id.slice(0, 8)} ${truncate(memory.text, 50)}`);
1714
+ }
1715
+ for (const { memory } of conflicts) {
1716
+ logger.info(`C ${memory.id.slice(0, 8)} ${truncate(memory.text, 50)}`);
1717
+ }
1718
+ for (const memory of untracked) {
1719
+ logger.info(`?? ${memory.id.slice(0, 8)} ${truncate(memory.text, 50)}`);
1720
+ }
1721
+ }
1722
+ function printLongStatus(remoteName, remoteUrl, pendingPush, conflicts, untracked, summary) {
1723
+ if (remoteUrl) {
1724
+ logger.info(`Remote '${remoteName}' at ${remoteUrl}`);
1725
+ } else {
1726
+ logger.info("No remote configured. Use 'unforgit remote add origin <url>' to add one.");
1727
+ }
1728
+ logger.info("");
1729
+ if (pendingPush.length === 0 && conflicts.length === 0 && untracked.length === 0) {
1730
+ logger.info("Nothing to push, working tree clean");
1731
+ if (summary.synced > 0) {
1732
+ logger.info(` ${summary.synced} memories synced with remote`);
1733
+ }
1734
+ return;
1735
+ }
1736
+ if (pendingPush.length > 0) {
1737
+ logger.info("Changes to be pushed:");
1738
+ logger.info(' (use "unforgit push" to sync with remote)');
1739
+ logger.info("");
1740
+ for (const { memory } of pendingPush) {
1741
+ const action = memory.status === "active" ? "new memory" : "modified";
1742
+ logger.info(` ${action}: ${memory.id.slice(0, 8)}... "${truncate(memory.text, 40)}"`);
1743
+ }
1744
+ logger.info("");
1745
+ }
1746
+ if (conflicts.length > 0) {
1747
+ logger.info("Conflicts:");
1748
+ logger.info(' (use "unforgit push --force" to overwrite remote or "unforgit pull --force" to accept remote)');
1749
+ logger.info("");
1750
+ for (const { memory } of conflicts) {
1751
+ logger.info(` conflict: ${memory.id.slice(0, 8)}... "${truncate(memory.text, 40)}"`);
1752
+ }
1753
+ logger.info("");
1754
+ }
1755
+ if (untracked.length > 0) {
1756
+ logger.info("Untracked memories:");
1757
+ logger.info(" (these memories were created before sync tracking was enabled)");
1758
+ logger.info(' (use "unforgit push" to sync them)');
1759
+ logger.info("");
1760
+ for (const memory of untracked) {
1761
+ logger.info(` ${memory.id.slice(0, 8)}... "${truncate(memory.text, 40)}"`);
1762
+ }
1763
+ logger.info("");
1764
+ }
1765
+ const total = pendingPush.length + conflicts.length + untracked.length;
1766
+ logger.info(`${total} change(s) pending`);
1767
+ }
1768
+
1769
+ // src/commands/push.ts
1770
+ import { Command as Command15 } from "commander";
1771
+ import { loadConfig as loadConfig12, getDbPath as getDbPath13, isInitialized as isInitialized7 } from "unforgit-config";
1772
+ import { LocalStore as LocalStore13 } from "unforgit-db";
1773
+ import { RemoteClient as RemoteClient8 } from "unforgit-config";
1774
+ 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) => {
1775
+ if (!isInitialized7()) {
1776
+ logger.fatal("not an unforgit repository");
1777
+ process.exit(EXIT_CONFIG_ERROR);
1778
+ }
1779
+ const config = loadConfig12();
1780
+ const store = new LocalStore13(getDbPath13());
1781
+ try {
1782
+ const orgId = config.remote.orgId || "local";
1783
+ const repoId = config.remote.repoId || "local";
1784
+ if (!config.remote.url) {
1785
+ logger.fatal(`No remote '${remote}' configured.`);
1786
+ logger.error("Use 'unforgit remote add origin <url>' to add a remote.");
1787
+ process.exit(EXIT_CONFIG_ERROR);
1788
+ }
1789
+ const client = new RemoteClient8(config.remote.url);
1790
+ const pendingPush = store.getPendingPush();
1791
+ const untracked = opts.all ? store.getUntrackedMemories(orgId, repoId) : [];
1792
+ for (const memory of untracked) {
1793
+ store.initSyncStateForMemory(memory.id);
1794
+ }
1795
+ const allToPush = opts.all ? [...pendingPush, ...untracked.map((m) => ({ memory: m, syncState: store.getSyncState(m.id) }))] : pendingPush;
1796
+ const supersededToSync = store.getSupersededMemoriesToSync(orgId, repoId);
1797
+ const linksToSync = store.getLinksToSync(orgId, repoId);
1798
+ if (allToPush.length === 0 && supersededToSync.length === 0 && linksToSync.length === 0) {
1799
+ logger.info("Everything up-to-date");
1800
+ return;
1801
+ }
1802
+ logger.info(`Pushing to ${remote} (${config.remote.url})...`);
1803
+ if (opts.dryRun) {
1804
+ if (allToPush.length > 0) {
1805
+ logger.info("\nWould push the following memories:");
1806
+ for (const { memory } of allToPush) {
1807
+ logger.info(` ${memory.id.slice(0, 8)}... "${truncate(memory.text, 50)}"`);
1808
+ }
1809
+ }
1810
+ if (supersededToSync.length > 0) {
1811
+ logger.info("\nWould sync superseded status:");
1812
+ for (const { memory, newId } of supersededToSync) {
1813
+ logger.info(` ${memory.id.slice(0, 8)}... -> superseded by ${newId.slice(0, 8)}...`);
1814
+ }
1815
+ }
1816
+ if (linksToSync.length > 0) {
1817
+ logger.info("\nWould sync links:");
1818
+ for (const { link } of linksToSync) {
1819
+ logger.info(` ${link.sourceId.slice(0, 8)}... -> ${link.targetId.slice(0, 8)}... (${link.linkType})`);
1820
+ }
1821
+ }
1822
+ logger.info(`
1823
+ Total: ${allToPush.length} memories, ${supersededToSync.length} status updates, ${linksToSync.length} links`);
1824
+ return;
1825
+ }
1826
+ let pushed = 0;
1827
+ let errors = 0;
1828
+ for (const { memory, syncState } of allToPush) {
1829
+ try {
1830
+ const fullMemory = store.getById(memory.id);
1831
+ if (!fullMemory) continue;
1832
+ if (fullMemory.visibility !== "repo" && !opts.force) {
1833
+ logger.info(` Skipping ${memory.id.slice(0, 8)}... (private memory, use --force to push anyway)`);
1834
+ continue;
1835
+ }
1836
+ await client.store({
1837
+ id: fullMemory.id,
1838
+ orgId: fullMemory.orgId,
1839
+ repoId: fullMemory.repoId,
1840
+ memoryType: fullMemory.memoryType,
1841
+ text: fullMemory.text,
1842
+ summary: fullMemory.summary,
1843
+ tags: fullMemory.tags,
1844
+ sourceRefs: fullMemory.sourceRefs,
1845
+ confidence: fullMemory.confidence,
1846
+ ttlSeconds: fullMemory.ttlSeconds,
1847
+ visibility: "repo",
1848
+ authorId: fullMemory.authorId,
1849
+ authorName: fullMemory.authorName
1850
+ });
1851
+ store.markAsPushed(memory.id, fullMemory.version);
1852
+ pushed++;
1853
+ logger.progress(pushed + errors, allToPush.length, "memories");
1854
+ logger.debug(`pushed ${memory.id.slice(0, 8)}...`);
1855
+ } catch (err) {
1856
+ errors++;
1857
+ const errorMsg = err instanceof Error ? err.message : String(err);
1858
+ logger.error(` ${memory.id.slice(0, 8)}... failed: ${errorMsg}`);
1859
+ if (errorMsg.includes("409") || errorMsg.includes("conflict")) {
1860
+ if (opts.force) {
1861
+ logger.info(` Forcing overwrite...`);
1862
+ } else {
1863
+ store.markAsConflict(memory.id, syncState?.remoteVersion ?? 0);
1864
+ logger.info(` Marked as conflict. Use --force to overwrite.`);
1865
+ }
1866
+ }
1867
+ }
1868
+ }
1869
+ let supersededSynced = 0;
1870
+ for (const { memory, newId } of supersededToSync) {
1871
+ try {
1872
+ await client.supersede(memory.id, newId);
1873
+ store.markStatusSynced(memory.id);
1874
+ supersededSynced++;
1875
+ logger.info(` ${memory.id.slice(0, 8)}... marked as superseded on remote`);
1876
+ } catch (err) {
1877
+ const errorMsg = err instanceof Error ? err.message : String(err);
1878
+ if (!errorMsg.includes("404")) {
1879
+ errors++;
1880
+ logger.error(` ${memory.id.slice(0, 8)}... failed to sync superseded status: ${errorMsg}`);
1881
+ }
1882
+ }
1883
+ }
1884
+ let linksSynced = 0;
1885
+ for (const { link } of linksToSync) {
1886
+ try {
1887
+ await client.link(link.sourceId, link.targetId, link.linkType, link.metadata);
1888
+ store.markLinkSynced(link.id);
1889
+ linksSynced++;
1890
+ logger.info(` link ${link.sourceId.slice(0, 8)}... -> ${link.targetId.slice(0, 8)}... (${link.linkType})`);
1891
+ } catch (err) {
1892
+ const errorMsg = err instanceof Error ? err.message : String(err);
1893
+ if (!errorMsg.includes("404")) {
1894
+ errors++;
1895
+ logger.error(` link ${link.sourceId.slice(0, 8)}... failed: ${errorMsg}`);
1896
+ }
1897
+ }
1898
+ }
1899
+ logger.info("");
1900
+ if (pushed > 0) {
1901
+ logger.info(`${pushed} memory(s) pushed successfully`);
1902
+ }
1903
+ if (supersededSynced > 0) {
1904
+ logger.info(`${supersededSynced} memory(s) marked as superseded on remote`);
1905
+ }
1906
+ if (linksSynced > 0) {
1907
+ logger.info(`${linksSynced} link(s) synced`);
1908
+ }
1909
+ if (errors > 0) {
1910
+ logger.info(`${errors} error(s) during push`);
1911
+ }
1912
+ } finally {
1913
+ store.close();
1914
+ }
1915
+ });
1916
+
1917
+ // src/commands/pull.ts
1918
+ import { Command as Command16 } from "commander";
1919
+ import { loadConfig as loadConfig13, getDbPath as getDbPath14, isInitialized as isInitialized8 } from "unforgit-config";
1920
+ import { LocalStore as LocalStore14 } from "unforgit-db";
1921
+ import { RemoteClient as RemoteClient9 } from "unforgit-config";
1922
+ 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) => {
1923
+ if (!isInitialized8()) {
1924
+ logger.fatal("not an unforgit repository");
1925
+ process.exit(EXIT_CONFIG_ERROR);
1926
+ }
1927
+ const config = loadConfig13();
1928
+ const store = new LocalStore14(getDbPath14());
1929
+ try {
1930
+ const orgId = config.remote.orgId || "local";
1931
+ const repoId = config.remote.repoId || "local";
1932
+ if (!config.remote.url) {
1933
+ logger.fatal(`No remote '${remote}' configured.`);
1934
+ logger.fatal("Use 'unforgit remote add origin <url>' to add a remote.");
1935
+ process.exit(EXIT_CONFIG_ERROR);
1936
+ }
1937
+ const client = new RemoteClient9(config.remote.url);
1938
+ logger.info(`Fetching from ${remote} (${config.remote.url})...`);
1939
+ const response = await client.recall({
1940
+ orgId,
1941
+ repoId,
1942
+ query: "*",
1943
+ k: 1e3,
1944
+ includeDeprecated: true
1945
+ });
1946
+ const remoteMemories = response.results;
1947
+ if (remoteMemories.length === 0) {
1948
+ logger.info("Already up to date (no memories on remote)");
1949
+ return;
1950
+ }
1951
+ if (opts.dryRun) {
1952
+ let newCount = 0;
1953
+ let updateCount = 0;
1954
+ for (const remoteMem of remoteMemories) {
1955
+ const localMem = store.getById(remoteMem.id);
1956
+ if (!localMem) {
1957
+ newCount++;
1958
+ } else {
1959
+ updateCount++;
1960
+ }
1961
+ }
1962
+ logger.info(`
1963
+ Would pull:`);
1964
+ logger.info(` ${newCount} new memories`);
1965
+ logger.info(` ${updateCount} updates`);
1966
+ return;
1967
+ }
1968
+ let created = 0;
1969
+ let updated = 0;
1970
+ let skipped = 0;
1971
+ let conflicts = 0;
1972
+ for (const remoteMem of remoteMemories) {
1973
+ const localMem = store.getById(remoteMem.id);
1974
+ const remoteStatus = remoteMem.status ?? "active";
1975
+ if (!localMem) {
1976
+ store.upsertFromRemote({
1977
+ id: remoteMem.id,
1978
+ orgId,
1979
+ repoId,
1980
+ scopeType: "repo",
1981
+ memoryType: remoteMem.memoryType,
1982
+ visibility: "repo",
1983
+ status: remoteStatus,
1984
+ text: remoteMem.text,
1985
+ summary: remoteMem.summary,
1986
+ tags: remoteMem.tags,
1987
+ sourceRefs: remoteMem.sourceRefs,
1988
+ supersedesId: remoteMem.supersedesId,
1989
+ version: 1,
1990
+ createdAt: /* @__PURE__ */ new Date(),
1991
+ updatedAt: /* @__PURE__ */ new Date()
1992
+ });
1993
+ store.setSyncState({
1994
+ memoryId: remoteMem.id,
1995
+ localVersion: 1,
1996
+ remoteVersion: 1,
1997
+ lastPulledAt: /* @__PURE__ */ new Date(),
1998
+ syncStatus: "synced"
1999
+ });
2000
+ created++;
2001
+ const statusNote = remoteStatus !== "active" ? ` [${remoteStatus}]` : "";
2002
+ logger.info(` ${remoteMem.id.slice(0, 8)}... new memory${statusNote}`);
2003
+ logger.progress(created + updated + skipped + conflicts, remoteMemories.length, "memories");
2004
+ } else {
2005
+ const syncState = store.getSyncState(remoteMem.id);
2006
+ const localVersion = syncState?.localVersion ?? localMem.version;
2007
+ const remoteVersion = syncState?.remoteVersion ?? 0;
2008
+ if (syncState?.syncStatus === "pending_push" && !opts.force) {
2009
+ conflicts++;
2010
+ store.markAsConflict(remoteMem.id, remoteVersion + 1);
2011
+ logger.info(` ${remoteMem.id.slice(0, 8)}... conflict (local has unpushed changes)`);
2012
+ logger.progress(created + updated + skipped + conflicts, remoteMemories.length, "memories");
2013
+ continue;
2014
+ }
2015
+ const statusChanged = localMem.status !== remoteStatus;
2016
+ const textChanged = localMem.text !== remoteMem.text;
2017
+ if (!textChanged && !statusChanged) {
2018
+ skipped++;
2019
+ logger.progress(created + updated + skipped + conflicts, remoteMemories.length, "memories");
2020
+ continue;
2021
+ }
2022
+ if (opts.force || syncState?.syncStatus !== "pending_push") {
2023
+ store.upsertFromRemote({
2024
+ id: remoteMem.id,
2025
+ orgId,
2026
+ repoId,
2027
+ scopeType: "repo",
2028
+ memoryType: remoteMem.memoryType,
2029
+ visibility: "repo",
2030
+ status: remoteStatus,
2031
+ text: remoteMem.text,
2032
+ summary: remoteMem.summary,
2033
+ tags: remoteMem.tags,
2034
+ sourceRefs: remoteMem.sourceRefs,
2035
+ supersedesId: remoteMem.supersedesId,
2036
+ version: localVersion + 1,
2037
+ createdAt: localMem.createdAt,
2038
+ updatedAt: /* @__PURE__ */ new Date()
2039
+ });
2040
+ store.markAsPulled(remoteMem.id, localVersion + 1);
2041
+ updated++;
2042
+ const changeType = statusChanged && !textChanged ? "status updated" : "updated";
2043
+ const statusNote = remoteStatus !== "active" ? ` [${remoteStatus}]` : "";
2044
+ logger.info(` ${remoteMem.id.slice(0, 8)}... ${changeType}${statusNote}`);
2045
+ logger.progress(created + updated + skipped + conflicts, remoteMemories.length, "memories");
2046
+ }
2047
+ }
2048
+ }
2049
+ logger.info("");
2050
+ logger.info(`Pull complete:`);
2051
+ if (created > 0) logger.info(` ${created} new memories`);
2052
+ if (updated > 0) logger.info(` ${updated} updates`);
2053
+ if (skipped > 0) logger.info(` ${skipped} already up to date`);
2054
+ if (conflicts > 0) {
2055
+ logger.info(` ${conflicts} conflicts (use --force to overwrite local)`);
2056
+ }
2057
+ } catch (err) {
2058
+ const errorMsg = err instanceof Error ? err.message : String(err);
2059
+ logger.fatal(`Could not fetch from remote: ${errorMsg}`);
2060
+ process.exit(EXIT_ERROR);
2061
+ } finally {
2062
+ store.close();
2063
+ }
2064
+ });
2065
+
2066
+ // src/commands/remote.ts
2067
+ import { Command as Command17 } from "commander";
2068
+ import { isInitialized as isInitialized9, loadConfig as loadConfig14, saveConfig as saveConfig2 } from "unforgit-config";
2069
+ var remoteCommand = new Command17("remote").description("Manage set of tracked remote repositories").addHelpText("after", `
2070
+ Examples:
2071
+ unforgit remote List remotes
2072
+ unforgit remote add origin <url> Add a remote
2073
+ unforgit remote show origin Show remote details`).action(() => {
2074
+ if (!isInitialized9()) {
2075
+ logger.fatal("not an unforgit repository");
2076
+ process.exit(EXIT_CONFIG_ERROR);
2077
+ }
2078
+ const config = loadConfig14();
2079
+ const remotes = getRemotes(config);
2080
+ if (Object.keys(remotes).length === 0) {
2081
+ logger.info("No remotes configured.");
2082
+ logger.info("Use 'unforgit remote add <name> <url>' to add a remote.");
2083
+ return;
2084
+ }
2085
+ for (const [name, remote] of Object.entries(remotes)) {
2086
+ logger.info(`${name} ${remote.url}`);
2087
+ }
2088
+ });
2089
+ 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) => {
2090
+ if (!isInitialized9()) {
2091
+ logger.fatal("not an unforgit repository");
2092
+ process.exit(EXIT_CONFIG_ERROR);
2093
+ }
2094
+ const config = loadConfig14();
2095
+ const remotes = getRemotes(config);
2096
+ if (remotes[name]) {
2097
+ logger.fatal(`remote '${name}' already exists.`);
2098
+ logger.error(`Use 'unforgit remote set-url ${name} <newurl>' to change the URL.`);
2099
+ process.exit(EXIT_ERROR);
2100
+ }
2101
+ remotes[name] = {
2102
+ url,
2103
+ orgId: opts.org || config.remote?.orgId || "",
2104
+ repoId: opts.repo || config.remote?.repoId || ""
2105
+ };
2106
+ saveRemotes(config, remotes);
2107
+ logger.info(`Remote '${name}' added: ${url}`);
2108
+ });
2109
+ var remoteRemoveCommand = new Command17("remove").alias("rm").description("Remove a remote").argument("<name>", "Name of the remote to remove").action((name) => {
2110
+ if (!isInitialized9()) {
2111
+ logger.fatal("not an unforgit repository");
2112
+ process.exit(EXIT_CONFIG_ERROR);
2113
+ }
2114
+ const config = loadConfig14();
2115
+ const remotes = getRemotes(config);
2116
+ if (!remotes[name]) {
2117
+ logger.fatal(`No such remote: '${name}'`);
2118
+ process.exit(EXIT_ERROR);
2119
+ }
2120
+ delete remotes[name];
2121
+ saveRemotes(config, remotes);
2122
+ logger.info(`Remote '${name}' removed.`);
2123
+ });
2124
+ 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) => {
2125
+ if (!isInitialized9()) {
2126
+ logger.fatal("not an unforgit repository");
2127
+ process.exit(EXIT_CONFIG_ERROR);
2128
+ }
2129
+ const config = loadConfig14();
2130
+ const remotes = getRemotes(config);
2131
+ if (!remotes[name]) {
2132
+ logger.fatal(`No such remote: '${name}'`);
2133
+ logger.error(`Use 'unforgit remote add ${name} ${newurl}' to add it.`);
2134
+ process.exit(EXIT_ERROR);
2135
+ }
2136
+ remotes[name].url = newurl;
2137
+ saveRemotes(config, remotes);
2138
+ logger.info(`Remote '${name}' URL changed to: ${newurl}`);
2139
+ });
2140
+ var remoteShowCommand = new Command17("show").description("Show information about a remote").argument("<name>", "Name of the remote").action((name) => {
2141
+ if (!isInitialized9()) {
2142
+ logger.fatal("not an unforgit repository");
2143
+ process.exit(EXIT_CONFIG_ERROR);
2144
+ }
2145
+ const config = loadConfig14();
2146
+ const remotes = getRemotes(config);
2147
+ if (!remotes[name]) {
2148
+ logger.fatal(`No such remote: '${name}'`);
2149
+ process.exit(EXIT_ERROR);
2150
+ }
2151
+ const remote = remotes[name];
2152
+ logger.info(`* remote ${name}`);
2153
+ logger.info(` URL: ${remote.url}`);
2154
+ logger.info(` Org ID: ${remote.orgId || "(not set)"}`);
2155
+ logger.info(` Repo ID: ${remote.repoId || "(not set)"}`);
2156
+ });
2157
+ remoteCommand.addCommand(remoteAddCommand);
2158
+ remoteCommand.addCommand(remoteRemoveCommand);
2159
+ remoteCommand.addCommand(remoteSetUrlCommand);
2160
+ remoteCommand.addCommand(remoteShowCommand);
2161
+ function getRemotes(config) {
2162
+ if (config.remotes) {
2163
+ return { ...config.remotes };
2164
+ }
2165
+ if (config.remote?.url) {
2166
+ return {
2167
+ origin: {
2168
+ url: config.remote.url,
2169
+ orgId: config.remote.orgId,
2170
+ repoId: config.remote.repoId
2171
+ }
2172
+ };
2173
+ }
2174
+ return {};
2175
+ }
2176
+ function saveRemotes(config, remotes) {
2177
+ config.remotes = remotes;
2178
+ if (remotes.origin) {
2179
+ config.remote = {
2180
+ ...config.remote,
2181
+ url: remotes.origin.url,
2182
+ orgId: remotes.origin.orgId,
2183
+ repoId: remotes.origin.repoId
2184
+ };
2185
+ }
2186
+ saveConfig2(config);
2187
+ }
2188
+
2189
+ // src/commands/log.ts
2190
+ import { Command as Command18 } from "commander";
2191
+ import { loadConfig as loadConfig15, getDbPath as getDbPath15, isInitialized as isInitialized10 } from "unforgit-config";
2192
+ import { LocalStore as LocalStore15 } from "unforgit-db";
2193
+ import { parsePositiveInt as parsePositiveInt5, validateMemoryType as validateMemoryType4 } from "unforgit-config";
2194
+ 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", `
2195
+ Examples:
2196
+ unforgit log Show recent memories
2197
+ unforgit log --all Include deprecated/superseded
2198
+ unforgit log --type semantic Filter by type
2199
+ unforgit log --oneline Compact output
2200
+ unforgit log --page 2 Second page of results`).action((opts) => {
2201
+ if (!isInitialized10()) {
2202
+ logger.fatal("not an unforgit repository");
2203
+ process.exit(EXIT_CONFIG_ERROR);
2204
+ }
2205
+ const config = loadConfig15();
2206
+ const store = new LocalStore15(getDbPath15());
2207
+ try {
2208
+ const orgId = config.remote.orgId || "local";
2209
+ const repoId = config.remote.repoId || "local";
2210
+ const limit = parsePositiveInt5(opts.maxCount, "max-count");
2211
+ if (opts.type && !validateMemoryType4(opts.type)) {
2212
+ logger.error(`Invalid memory type "${opts.type}". Must be one of: episodic, semantic, procedural`);
2213
+ process.exit(EXIT_ERROR);
2214
+ }
2215
+ const types = opts.type ? [opts.type] : void 0;
2216
+ const status = opts.all ? void 0 : ["active"];
2217
+ const memories = store.list({
2218
+ orgId,
2219
+ repoId,
2220
+ types,
2221
+ status,
2222
+ limit,
2223
+ sortBy: "createdAt",
2224
+ sortOrder: "desc"
2225
+ });
2226
+ let filteredMemories = memories;
2227
+ if (opts.tags) {
2228
+ const filterTags = opts.tags.split(",").map((t) => t.trim());
2229
+ filteredMemories = memories.filter(
2230
+ (m) => m.tags.some((t) => filterTags.includes(t))
2231
+ );
2232
+ }
2233
+ const page = parsePositiveInt5(opts.page, "page");
2234
+ const perPage = parsePositiveInt5(opts.perPage, "per-page");
2235
+ const paged = paginate(filteredMemories, page, perPage);
2236
+ if (isJsonMode()) {
2237
+ outputJson({
2238
+ memories: paged.items.map((m) => ({
2239
+ id: m.id,
2240
+ type: m.memoryType,
2241
+ status: m.status,
2242
+ text: m.text,
2243
+ tags: m.tags,
2244
+ createdAt: m.createdAt.toISOString()
2245
+ })),
2246
+ page: paged.currentPage,
2247
+ totalPages: paged.totalPages,
2248
+ total: paged.total
2249
+ });
2250
+ return;
2251
+ }
2252
+ if (paged.items.length === 0) {
2253
+ logger.info("No memories found.");
2254
+ return;
2255
+ }
2256
+ if (opts.oneline) {
2257
+ for (const mem of paged.items) {
2258
+ const text = mem.text.replace(/\n/g, " ").slice(0, 60);
2259
+ logger.info(`${mem.id.slice(0, 7)} [${mem.memoryType}] ${text}${mem.text.length > 60 ? "..." : ""}`);
2260
+ }
2261
+ } else {
2262
+ for (const mem of paged.items) {
2263
+ const date = mem.createdAt.toISOString().split("T")[0];
2264
+ const time = mem.createdAt.toISOString().split("T")[1].slice(0, 5);
2265
+ logger.info(`memory ${mem.id}`);
2266
+ logger.info(`Type: ${mem.memoryType}`);
2267
+ logger.info(`Date: ${date} ${time}`);
2268
+ logger.info(`Status: ${mem.status}`);
2269
+ if (mem.tags.length > 0) {
2270
+ logger.info(`Tags: ${mem.tags.join(", ")}`);
2271
+ }
2272
+ logger.info("");
2273
+ logger.info(` ${mem.text.split("\n").join("\n ")}`);
2274
+ logger.info("");
2275
+ }
2276
+ }
2277
+ if (paged.totalPages > 1) {
2278
+ logger.info(`Page ${paged.currentPage}/${paged.totalPages} (${paged.total} total)`);
2279
+ }
2280
+ } finally {
2281
+ store.close();
2282
+ }
2283
+ });
2284
+
2285
+ // src/commands/diff.ts
2286
+ import { Command as Command19 } from "commander";
2287
+ import { loadConfig as loadConfig16, getDbPath as getDbPath16, isInitialized as isInitialized11 } from "unforgit-config";
2288
+ import { LocalStore as LocalStore16 } from "unforgit-db";
2289
+ import { RemoteClient as RemoteClient10 } from "unforgit-config";
2290
+ 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", `
2291
+ Examples:
2292
+ unforgit diff Show all differences
2293
+ unforgit diff --stat Show difference statistics only
2294
+ unforgit diff abc12345 Diff a specific memory`).action(async (memoryId, opts) => {
2295
+ if (!isInitialized11()) {
2296
+ logger.fatal("not an unforgit repository");
2297
+ process.exit(EXIT_CONFIG_ERROR);
2298
+ }
2299
+ const config = loadConfig16();
2300
+ const store = new LocalStore16(getDbPath16());
2301
+ try {
2302
+ const orgId = config.remote.orgId || "local";
2303
+ const repoId = config.remote.repoId || "local";
2304
+ if (!config.remote.url) {
2305
+ logger.fatal("No remote configured.");
2306
+ process.exit(EXIT_CONFIG_ERROR);
2307
+ }
2308
+ const client = new RemoteClient10(config.remote.url);
2309
+ if (memoryId) {
2310
+ await diffSingleMemory(store, client, memoryId, orgId, repoId);
2311
+ } else {
2312
+ await diffAll(store, client, orgId, repoId, opts.stat);
2313
+ }
2314
+ } finally {
2315
+ store.close();
2316
+ }
2317
+ });
2318
+ async function diffSingleMemory(store, client, memoryId, orgId, repoId) {
2319
+ const fullId = memoryId.length < 36 ? findFullId(store, memoryId, orgId, repoId) : memoryId;
2320
+ if (!fullId) {
2321
+ logger.error(`memory '${memoryId}' not found`);
2322
+ process.exit(EXIT_ERROR);
2323
+ }
2324
+ const localMem = store.getById(fullId);
2325
+ let remoteMem = null;
2326
+ try {
2327
+ const response = await client.recall({
2328
+ orgId,
2329
+ repoId,
2330
+ query: fullId,
2331
+ k: 1
2332
+ });
2333
+ remoteMem = response.results.find((r) => r.id === fullId);
2334
+ } catch {
2335
+ logger.warn("Could not fetch from remote");
2336
+ }
2337
+ if (!localMem && !remoteMem) {
2338
+ logger.error(`memory '${memoryId}' not found locally or remotely`);
2339
+ process.exit(EXIT_ERROR);
2340
+ }
2341
+ if (isJsonMode()) {
2342
+ outputJson({
2343
+ memoryId: fullId,
2344
+ local: localMem ? { text: localMem.text, status: localMem.status } : null,
2345
+ remote: remoteMem ? { text: remoteMem.text, status: remoteMem.status } : null,
2346
+ identical: localMem && remoteMem ? localMem.text === remoteMem.text : false
2347
+ });
2348
+ return;
2349
+ }
2350
+ logger.info(`diff ${fullId.slice(0, 8)}...`);
2351
+ logger.info("---");
2352
+ if (!localMem) {
2353
+ logger.info("(not in local)");
2354
+ logger.info("+ " + remoteMem.text.split("\n").join("\n+ "));
2355
+ return;
2356
+ }
2357
+ if (!remoteMem) {
2358
+ logger.info("(not in remote)");
2359
+ logger.info("+ " + localMem.text.split("\n").join("\n+ "));
2360
+ return;
2361
+ }
2362
+ if (localMem.text === remoteMem.text) {
2363
+ logger.info("No differences");
2364
+ return;
2365
+ }
2366
+ const localLines = localMem.text.split("\n");
2367
+ const remoteLines = remoteMem.text.split("\n");
2368
+ logger.info("--- local");
2369
+ logger.info("+++ remote");
2370
+ logger.info("");
2371
+ for (let i = 0; i < Math.max(localLines.length, remoteLines.length); i++) {
2372
+ const localLine = localLines[i];
2373
+ const remoteLine = remoteLines[i];
2374
+ if (localLine === remoteLine) {
2375
+ logger.info(` ${localLine ?? ""}`);
2376
+ } else if (localLine && !remoteLine) {
2377
+ logger.info(`- ${localLine}`);
2378
+ } else if (!localLine && remoteLine) {
2379
+ logger.info(`+ ${remoteLine}`);
2380
+ } else {
2381
+ logger.info(`- ${localLine}`);
2382
+ logger.info(`+ ${remoteLine}`);
2383
+ }
2384
+ }
2385
+ }
2386
+ async function diffAll(store, client, orgId, repoId, statOnly) {
2387
+ const pendingPush = store.getPendingPush();
2388
+ const conflicts = store.getConflicts();
2389
+ let remoteOnlyCount = 0;
2390
+ try {
2391
+ const response = await client.recall({ orgId, repoId, query: "*", k: 1e3 });
2392
+ for (const remoteMem of response.results) {
2393
+ const localMem = store.getById(remoteMem.id);
2394
+ if (!localMem) {
2395
+ remoteOnlyCount++;
2396
+ }
2397
+ }
2398
+ } catch {
2399
+ logger.warn("Could not fetch remote memories for full diff");
2400
+ }
2401
+ if (pendingPush.length === 0 && conflicts.length === 0 && remoteOnlyCount === 0) {
2402
+ if (isJsonMode()) {
2403
+ outputJson({ pendingPush: 0, conflicts: 0, remoteOnly: 0, total: 0 });
2404
+ return;
2405
+ }
2406
+ logger.info("No differences");
2407
+ return;
2408
+ }
2409
+ if (isJsonMode()) {
2410
+ outputJson({
2411
+ pendingPush: pendingPush.length,
2412
+ conflicts: conflicts.length,
2413
+ remoteOnly: remoteOnlyCount,
2414
+ total: pendingPush.length + conflicts.length + remoteOnlyCount
2415
+ });
2416
+ return;
2417
+ }
2418
+ if (statOnly) {
2419
+ logger.info(`${pendingPush.length} memories to push`);
2420
+ logger.info(`${conflicts.length} conflicts`);
2421
+ if (remoteOnlyCount > 0) {
2422
+ logger.info(`${remoteOnlyCount} remote-only memories (use 'unforgit pull')`);
2423
+ }
2424
+ return;
2425
+ }
2426
+ if (pendingPush.length > 0) {
2427
+ logger.info("Changes to push:");
2428
+ logger.info("");
2429
+ for (const { memory } of pendingPush) {
2430
+ logger.info(` + ${memory.id.slice(0, 8)}... "${truncate(memory.text, 50)}"`);
2431
+ }
2432
+ logger.info("");
2433
+ }
2434
+ if (conflicts.length > 0) {
2435
+ logger.info("Conflicts:");
2436
+ logger.info("");
2437
+ for (const { memory, syncState } of conflicts) {
2438
+ logger.info(` ! ${memory.id.slice(0, 8)}... local:v${syncState.localVersion} remote:v${syncState.remoteVersion ?? "?"}`);
2439
+ logger.info(` "${truncate(memory.text, 60)}"`);
2440
+ }
2441
+ logger.info("");
2442
+ }
2443
+ if (remoteOnlyCount > 0) {
2444
+ logger.info(`${remoteOnlyCount} remote-only memories (run 'unforgit pull' to fetch)`);
2445
+ logger.info("");
2446
+ }
2447
+ logger.info(`Total: ${pendingPush.length + conflicts.length + remoteOnlyCount} differences`);
2448
+ }
2449
+ function findFullId(store, partialId, orgId, repoId) {
2450
+ const memories = store.list({ orgId, repoId, limit: 1e3 });
2451
+ const match = memories.find((m) => m.id.startsWith(partialId));
2452
+ return match?.id ?? null;
2453
+ }
2454
+
2455
+ // src/commands/keys.ts
2456
+ import { Command as Command20 } from "commander";
2457
+ import { loadConfig as loadConfig17, isInitialized as isInitialized12 } from "unforgit-config";
2458
+ import { RemoteClient as RemoteClient11 } from "unforgit-config";
2459
+ function requireRemote() {
2460
+ if (!isInitialized12()) {
2461
+ logger.fatal("not an unforgit repository");
2462
+ process.exit(EXIT_CONFIG_ERROR);
2463
+ }
2464
+ const config = loadConfig17();
2465
+ if (!config.remote.url) {
2466
+ logger.fatal("No remote configured.");
2467
+ logger.error("Use 'unforgit remote add origin <url>' to add a remote.");
2468
+ process.exit(EXIT_CONFIG_ERROR);
2469
+ }
2470
+ const apiKey = process.env.UNFORGIT_API_KEY;
2471
+ if (!apiKey) {
2472
+ logger.fatal("No API key configured.");
2473
+ logger.error("Set the UNFORGIT_API_KEY environment variable.");
2474
+ process.exit(EXIT_CONFIG_ERROR);
2475
+ }
2476
+ return { url: config.remote.url, apiKey };
2477
+ }
2478
+ var keysCommand = new Command20("keys").description("Manage API keys for remote authentication");
2479
+ 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", `
2480
+ Examples:
2481
+ unforgit keys create --name "CI pipeline" --org my-org
2482
+ unforgit keys list --org my-org`).action(async (opts) => {
2483
+ const { url, apiKey } = requireRemote();
2484
+ const client = new RemoteClient11(url, apiKey);
2485
+ try {
2486
+ const result = await client.createApiKey(opts.name, opts.org);
2487
+ if (isJsonMode()) {
2488
+ outputJson(result);
2489
+ return;
2490
+ }
2491
+ logger.info("API key created successfully!");
2492
+ logger.info("");
2493
+ logger.info(` ID: ${result.id}`);
2494
+ logger.info(` Name: ${result.name}`);
2495
+ logger.info(` Org: ${result.orgId}`);
2496
+ logger.info(` Key: ${result.key}`);
2497
+ logger.info("");
2498
+ logger.info("Store this key securely. It will not be shown again.");
2499
+ logger.info("");
2500
+ logger.info("To use this key, set it as an environment variable:");
2501
+ logger.info("");
2502
+ logger.info(` export UNFORGIT_API_KEY="${result.key}"`);
2503
+ } catch (err) {
2504
+ logger.fatal(err instanceof Error ? err.message : String(err));
2505
+ process.exit(EXIT_ERROR);
2506
+ }
2507
+ });
2508
+ keysCommand.command("list").description("List all API keys").option("--org <orgId>", "Filter by organization ID").action(async (opts) => {
2509
+ const { url, apiKey } = requireRemote();
2510
+ const client = new RemoteClient11(url, apiKey);
2511
+ try {
2512
+ const result = await client.listApiKeys(opts.org);
2513
+ if (isJsonMode()) {
2514
+ outputJson(result);
2515
+ return;
2516
+ }
2517
+ if (result.keys.length === 0) {
2518
+ logger.info("No API keys found.");
2519
+ return;
2520
+ }
2521
+ logger.info(`Found ${result.keys.length} API key(s):
2522
+ `);
2523
+ for (const key of result.keys) {
2524
+ const status = key.isActive ? "[active]" : "[revoked]";
2525
+ const lastUsed = key.lastUsedAt ? `last used ${new Date(key.lastUsedAt).toLocaleDateString()}` : "never used";
2526
+ logger.info(`${status} ${key.id.slice(0, 8)} ${key.name}`);
2527
+ logger.info(` org: ${key.orgId} | ${lastUsed}`);
2528
+ }
2529
+ } catch (err) {
2530
+ logger.fatal(err instanceof Error ? err.message : String(err));
2531
+ process.exit(EXIT_ERROR);
2532
+ }
2533
+ });
2534
+ keysCommand.command("revoke").description("Revoke an API key").argument("<id>", "API key ID to revoke").action(async (id) => {
2535
+ const { url, apiKey } = requireRemote();
2536
+ const client = new RemoteClient11(url, apiKey);
2537
+ try {
2538
+ await client.revokeApiKey(id);
2539
+ logger.info(`API key ${id.slice(0, 8)}... revoked successfully.`);
2540
+ } catch (err) {
2541
+ logger.fatal(err instanceof Error ? err.message : String(err));
2542
+ process.exit(EXIT_ERROR);
2543
+ }
2544
+ });
2545
+
2546
+ // src/commands/auth.ts
2547
+ import { Command as Command21 } from "commander";
2548
+ import { loadConfig as loadConfig18, isInitialized as isInitialized13 } from "unforgit-config";
2549
+ var authCommand = new Command21("auth").description("Check authentication status for remote server and APIs");
2550
+ authCommand.command("status").description("Check authentication status").action(async () => {
2551
+ if (!isInitialized13()) {
2552
+ logger.fatal("not an unforgit repository");
2553
+ process.exit(EXIT_CONFIG_ERROR);
2554
+ }
2555
+ const config = loadConfig18();
2556
+ logger.info("Authentication status:");
2557
+ logger.info(` Remote URL: ${config.remote.url || "(not configured)"}`);
2558
+ logger.info(` Org ID: ${config.remote.orgId || "(not configured)"}`);
2559
+ logger.info(` Repo ID: ${config.remote.repoId || "(not configured)"}`);
2560
+ const apiKey = process.env.UNFORGIT_API_KEY;
2561
+ if (apiKey) {
2562
+ logger.info(` API Key: ${maskKey(apiKey)} (from UNFORGIT_API_KEY env var)`);
2563
+ if (config.remote.url) {
2564
+ logger.info("\nTesting connection...");
2565
+ try {
2566
+ const res = await fetch(`${config.remote.url}/health`);
2567
+ if (res.ok) {
2568
+ logger.info(" [ok] Server reachable");
2569
+ const authRes = await fetch(`${config.remote.url}/v1/api-keys`, {
2570
+ headers: { Authorization: `Bearer ${apiKey}` }
2571
+ });
2572
+ if (authRes.ok) {
2573
+ logger.info(" [ok] API key valid");
2574
+ } else if (authRes.status === 401) {
2575
+ logger.info(" [ERR] API key invalid or expired");
2576
+ } else {
2577
+ logger.info(` [??] Could not verify API key (HTTP ${authRes.status})`);
2578
+ }
2579
+ } else {
2580
+ logger.info(` [ERR] Server returned HTTP ${res.status}`);
2581
+ }
2582
+ } catch (err) {
2583
+ logger.info(` [ERR] Could not connect: ${err instanceof Error ? err.message : err}`);
2584
+ }
2585
+ }
2586
+ } else {
2587
+ logger.info(" API Key: (not configured)");
2588
+ logger.info("\nSet the UNFORGIT_API_KEY environment variable.");
2589
+ }
2590
+ const openaiKey = process.env.OPENAI_API_KEY;
2591
+ if (openaiKey) {
2592
+ logger.info(` OpenAI Key: ${maskKey(openaiKey)} (from OPENAI_API_KEY env var)`);
2593
+ } else {
2594
+ logger.info(" OpenAI Key: (not configured)");
2595
+ logger.info(" Set OPENAI_API_KEY env var for embeddings and auto-consolidation.");
2596
+ }
2597
+ });
2598
+
2599
+ // src/commands/config.ts
2600
+ import { Command as Command22 } from "commander";
2601
+ import { isInitialized as isInitialized14, loadConfig as loadConfig19, saveConfig as saveConfig3 } from "unforgit-config";
2602
+ var configCommand = new Command22("config").description("Manage unforgit configuration");
2603
+ configCommand.command("list").alias("ls").description("List all configuration values").action(() => {
2604
+ if (!isInitialized14()) {
2605
+ logger.fatal("not an unforgit repository");
2606
+ process.exit(EXIT_CONFIG_ERROR);
2607
+ }
2608
+ const config = loadConfig19();
2609
+ if (isJsonMode()) {
2610
+ outputJson(config);
2611
+ return;
2612
+ }
2613
+ logger.info("Current configuration:\n");
2614
+ logger.info(`remote.url = ${config.remote.url || "(not set)"}`);
2615
+ logger.info(`remote.orgId = ${config.remote.orgId || "(not set)"}`);
2616
+ logger.info(`remote.repoId = ${config.remote.repoId || "(not set)"}`);
2617
+ logger.info(`defaults.visibility = ${config.defaults.visibility}`);
2618
+ logger.info(`defaults.memoryType = ${config.defaults.memoryType}`);
2619
+ logger.info(`lifecycle.ttlSecondsByType.episodic = ${config.lifecycle?.ttlSecondsByType?.episodic ?? "(none)"}`);
2620
+ logger.info(`lifecycle.usageBoost.topKToRecord = ${config.lifecycle?.usageBoost?.topKToRecord ?? "(not set)"}`);
2621
+ logger.info(`lifecycle.usageBoost.minUsageCount = ${config.lifecycle?.usageBoost?.minUsageCount ?? "(not set)"}`);
2622
+ logger.info(`lifecycle.usageBoost.maxBoost = ${config.lifecycle?.usageBoost?.maxBoost ?? "(not set)"}`);
2623
+ logger.info(`lifecycle.usageBoost.halfLifeDays = ${config.lifecycle?.usageBoost?.halfLifeDays ?? "(not set)"}`);
2624
+ logger.info(`lifecycle.maintenance.staleEpisodicDays = ${config.lifecycle?.maintenance?.staleEpisodicDays ?? "(not set)"}`);
2625
+ logger.info(`lifecycle.maintenance.consolidationThreshold = ${config.lifecycle?.maintenance?.consolidationThreshold ?? "(not set)"}`);
2626
+ logger.info(`lifecycle.maintenance.consolidationMinGroupSize = ${config.lifecycle?.maintenance?.consolidationMinGroupSize ?? "(not set)"}`);
2627
+ logger.info(`lifecycle.maintenance.consolidationMaxGroups = ${config.lifecycle?.maintenance?.consolidationMaxGroups ?? "(not set)"}`);
2628
+ logger.info(`lifecycle.maintenance.promoteRecallCount = ${config.lifecycle?.maintenance?.promoteRecallCount ?? "(not set)"}`);
2629
+ logger.info(`lifecycle.maintenance.pinRecallCount = ${config.lifecycle?.maintenance?.pinRecallCount ?? "(not set)"}`);
2630
+ logger.info(`lifecycle.maintenance.dryRunDefault = ${config.lifecycle?.maintenance?.dryRunDefault ?? "(not set)"}`);
2631
+ logger.info(`lifecycle.maintenance.autoRunOnStore = ${config.lifecycle?.maintenance?.autoRunOnStore ?? "(not set)"}`);
2632
+ logger.info(`lifecycle.maintenance.autoRunOnRecall = ${config.lifecycle?.maintenance?.autoRunOnRecall ?? "(not set)"}`);
2633
+ logger.info(`lifecycle.maintenance.debounceMs = ${config.lifecycle?.maintenance?.debounceMs ?? "(not set)"}`);
2634
+ });
2635
+ configCommand.command("get").description("Get a configuration value").argument("<key>", "Configuration key (e.g., remote.url, defaults.memoryType)").action((key) => {
2636
+ if (!isInitialized14()) {
2637
+ logger.fatal("not an unforgit repository");
2638
+ process.exit(EXIT_CONFIG_ERROR);
2639
+ }
2640
+ const config = loadConfig19();
2641
+ const value = getConfigValue(config, key);
2642
+ if (value === void 0) {
2643
+ logger.fatal(`key '${key}' not found`);
2644
+ process.exit(EXIT_ERROR);
2645
+ }
2646
+ if (isJsonMode()) {
2647
+ outputJson({ key, value });
2648
+ return;
2649
+ }
2650
+ logger.info(String(value));
2651
+ });
2652
+ var VALID_CONFIG_KEYS = [
2653
+ "remote.url",
2654
+ "remote.orgId",
2655
+ "remote.repoId",
2656
+ "defaults.visibility",
2657
+ "defaults.memoryType",
2658
+ "lifecycle.ttlSecondsByType.episodic",
2659
+ "lifecycle.ttlSecondsByType.semantic",
2660
+ "lifecycle.ttlSecondsByType.procedural",
2661
+ "lifecycle.usageBoost.enabled",
2662
+ "lifecycle.usageBoost.topKToRecord",
2663
+ "lifecycle.usageBoost.minUsageCount",
2664
+ "lifecycle.usageBoost.maxBoost",
2665
+ "lifecycle.usageBoost.halfLifeDays",
2666
+ "lifecycle.maintenance.staleEpisodicDays",
2667
+ "lifecycle.maintenance.consolidationThreshold",
2668
+ "lifecycle.maintenance.consolidationMinGroupSize",
2669
+ "lifecycle.maintenance.consolidationMaxGroups",
2670
+ "lifecycle.maintenance.promoteRecallCount",
2671
+ "lifecycle.maintenance.pinRecallCount",
2672
+ "lifecycle.maintenance.dryRunDefault",
2673
+ "lifecycle.maintenance.autoRunOnStore",
2674
+ "lifecycle.maintenance.autoRunOnRecall",
2675
+ "lifecycle.maintenance.debounceMs",
2676
+ "configVersion"
2677
+ ];
2678
+ configCommand.command("set").description("Set a configuration value").argument("<key>", "Configuration key").argument("<value>", "Value to set").addHelpText("after", `
2679
+ Valid keys: ${VALID_CONFIG_KEYS.join(", ")}
2680
+
2681
+ Examples:
2682
+ unforgit config set remote.url http://my-server:3737
2683
+ unforgit config set defaults.memoryType semantic`).action((key, value) => {
2684
+ if (!isInitialized14()) {
2685
+ logger.fatal("not an unforgit repository");
2686
+ process.exit(EXIT_CONFIG_ERROR);
2687
+ }
2688
+ if (!VALID_CONFIG_KEYS.includes(key)) {
2689
+ logger.error(
2690
+ `Unknown config key "${key}". Valid keys: ${VALID_CONFIG_KEYS.join(", ")}`
2691
+ );
2692
+ process.exit(EXIT_ERROR);
2693
+ }
2694
+ const config = loadConfig19();
2695
+ setConfigValue(config, key, value);
2696
+ saveConfig3(config);
2697
+ logger.info(`${key} = ${value}`);
2698
+ });
2699
+ configCommand.command("unset").description("Remove a configuration value").argument("<key>", "Configuration key to remove").action((key) => {
2700
+ if (!isInitialized14()) {
2701
+ logger.fatal("not an unforgit repository");
2702
+ process.exit(EXIT_CONFIG_ERROR);
2703
+ }
2704
+ const config = loadConfig19();
2705
+ unsetConfigValue(config, key);
2706
+ saveConfig3(config);
2707
+ logger.info(`Unset ${key}`);
2708
+ });
2709
+ function getConfigValue(config, key) {
2710
+ const parts = key.split(".");
2711
+ let current = config;
2712
+ for (const part of parts) {
2713
+ if (current && typeof current === "object" && part in current) {
2714
+ current = current[part];
2715
+ } else {
2716
+ return void 0;
2717
+ }
2718
+ }
2719
+ return current;
2720
+ }
2721
+ function setConfigValue(config, key, value) {
2722
+ const parts = key.split(".");
2723
+ if (parts.length === 1) {
2724
+ config[key] = value;
2725
+ return;
2726
+ }
2727
+ let current = config;
2728
+ for (let i = 0; i < parts.length - 1; i++) {
2729
+ const part = parts[i];
2730
+ if (!(part in current) || typeof current[part] !== "object") {
2731
+ current[part] = {};
2732
+ }
2733
+ current = current[part];
2734
+ }
2735
+ current[parts[parts.length - 1]] = coerceConfigValue(value);
2736
+ }
2737
+ function unsetConfigValue(config, key) {
2738
+ const parts = key.split(".");
2739
+ if (parts.length === 1) {
2740
+ delete config[key];
2741
+ return;
2742
+ }
2743
+ let current = config;
2744
+ for (let i = 0; i < parts.length - 1; i++) {
2745
+ const part = parts[i];
2746
+ if (!(part in current) || typeof current[part] !== "object") {
2747
+ return;
2748
+ }
2749
+ current = current[part];
2750
+ }
2751
+ delete current[parts[parts.length - 1]];
2752
+ }
2753
+ function coerceConfigValue(value) {
2754
+ if (value === "true") return true;
2755
+ if (value === "false") return false;
2756
+ if (/^-?\d+(?:\.\d+)?$/.test(value)) {
2757
+ return Number(value);
2758
+ }
2759
+ return value;
2760
+ }
2761
+
2762
+ // src/commands/embeddings.ts
2763
+ import { Command as Command23 } from "commander";
2764
+ import { LocalStore as LocalStore17 } from "unforgit-db";
2765
+ import { loadConfig as loadConfig20, getDbPath as getDbPath17, isInitialized as isInitialized15 } from "unforgit-config";
2766
+ import { generateEmbedding } from "unforgit-core";
2767
+ import { parsePositiveInt as parsePositiveInt6 } from "unforgit-config";
2768
+ var cwd3 = process.cwd();
2769
+ var embeddingsCommand = new Command23("embeddings").description("Manage memory embeddings for semantic search");
2770
+ 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) => {
2771
+ if (!isInitialized15(cwd3)) {
2772
+ logger.error("Unforgit not initialized. Run 'unforgit init' first.");
2773
+ process.exit(EXIT_CONFIG_ERROR);
2774
+ }
2775
+ const config = loadConfig20(cwd3);
2776
+ const apiKey = process.env.OPENAI_API_KEY;
2777
+ if (!apiKey && !opts.dryRun) {
2778
+ logger.error("OpenAI API key not set.");
2779
+ logger.error("Set the OPENAI_API_KEY environment variable.");
2780
+ process.exit(EXIT_ERROR);
2781
+ }
2782
+ const dbPath = getDbPath17(cwd3);
2783
+ const store = new LocalStore17(dbPath);
2784
+ const orgId = config.remote.orgId || "local";
2785
+ const repoId = config.remote.repoId || "local";
2786
+ try {
2787
+ const memories = store.getMemoriesWithoutEmbeddings(orgId, repoId);
2788
+ const stats = store.getEmbeddingStats(orgId, repoId);
2789
+ logger.info(`Embedding stats:`);
2790
+ logger.info(` Total memories: ${stats.total}`);
2791
+ logger.info(` With embedding: ${stats.withEmbedding}`);
2792
+ logger.info(` Without embedding: ${stats.withoutEmbedding}`);
2793
+ logger.info("");
2794
+ if (memories.length === 0) {
2795
+ logger.info("All memories already have embeddings.");
2796
+ return;
2797
+ }
2798
+ logger.info(`Found ${memories.length} memories without embeddings.`);
2799
+ if (opts.dryRun) {
2800
+ logger.info("\n[Dry run - no changes made]");
2801
+ logger.info("Would generate embeddings for:");
2802
+ for (const m of memories.slice(0, 10)) {
2803
+ logger.info(` - ${m.id.slice(0, 8)}: ${m.text.slice(0, 50)}...`);
2804
+ }
2805
+ if (memories.length > 10) {
2806
+ logger.info(` ... and ${memories.length - 10} more`);
2807
+ }
2808
+ return;
2809
+ }
2810
+ const batchSize = parsePositiveInt6(opts.batchSize, "batch-size");
2811
+ const delay = parsePositiveInt6(opts.delay, "delay");
2812
+ let processed = 0;
2813
+ let errors = 0;
2814
+ logger.info(`
2815
+ Generating embeddings (batch size: ${batchSize}, delay: ${delay}ms)...`);
2816
+ for (let i = 0; i < memories.length; i += batchSize) {
2817
+ const batch = memories.slice(i, i + batchSize);
2818
+ await Promise.all(
2819
+ batch.map(async (memory) => {
2820
+ try {
2821
+ const result = await generateEmbedding(memory.text, {
2822
+ apiKey,
2823
+ model: opts.model
2824
+ });
2825
+ await store.storeEmbedding(memory.id, result.embedding, result.model);
2826
+ processed++;
2827
+ logger.progress(processed, memories.length, "embeddings");
2828
+ } catch (err) {
2829
+ errors++;
2830
+ logger.error(`${memory.id.slice(0, 8)}: ${err instanceof Error ? err.message : err}`);
2831
+ }
2832
+ })
2833
+ );
2834
+ if (i + batchSize < memories.length) {
2835
+ await new Promise((resolve) => setTimeout(resolve, delay));
2836
+ }
2837
+ }
2838
+ logger.info(`
2839
+ Backfill complete:`);
2840
+ logger.info(` Processed: ${processed}`);
2841
+ logger.info(` Errors: ${errors}`);
2842
+ } finally {
2843
+ store.close();
2844
+ }
2845
+ });
2846
+ embeddingsCommand.command("stats").description("Show embedding statistics").action(async () => {
2847
+ if (!isInitialized15(cwd3)) {
2848
+ logger.error("Unforgit not initialized. Run 'unforgit init' first.");
2849
+ process.exit(EXIT_CONFIG_ERROR);
2850
+ }
2851
+ const config = loadConfig20(cwd3);
2852
+ const dbPath = getDbPath17(cwd3);
2853
+ const store = new LocalStore17(dbPath);
2854
+ const orgId = config.remote.orgId || "local";
2855
+ const repoId = config.remote.repoId || "local";
2856
+ try {
2857
+ const stats = store.getEmbeddingStats(orgId, repoId);
2858
+ const coverage = stats.total > 0 ? (stats.withEmbedding / stats.total * 100).toFixed(1) : "0";
2859
+ if (isJsonMode()) {
2860
+ outputJson({ ...stats, coverage: parseFloat(coverage) });
2861
+ return;
2862
+ }
2863
+ logger.info("Embedding Statistics");
2864
+ logger.info("====================");
2865
+ logger.info(`Total memories: ${stats.total}`);
2866
+ logger.info(`With embedding: ${stats.withEmbedding}`);
2867
+ logger.info(`Without embedding: ${stats.withoutEmbedding}`);
2868
+ logger.info(`Coverage: ${coverage}%`);
2869
+ if (stats.withoutEmbedding > 0) {
2870
+ logger.info(`
2871
+ Run 'unforgit embeddings backfill' to generate missing embeddings.`);
2872
+ }
2873
+ } finally {
2874
+ store.close();
2875
+ }
2876
+ });
2877
+ embeddingsCommand.command("clear").description("Remove all embeddings (requires regeneration)").option("--yes", "Skip confirmation").action(async (opts) => {
2878
+ if (!isInitialized15(cwd3)) {
2879
+ logger.error("Unforgit not initialized. Run 'unforgit init' first.");
2880
+ process.exit(EXIT_CONFIG_ERROR);
2881
+ }
2882
+ if (!opts.yes) {
2883
+ logger.info("This will delete all embeddings. They will need to be regenerated.");
2884
+ logger.info("Use --yes to confirm.");
2885
+ return;
2886
+ }
2887
+ const dbPath = getDbPath17(cwd3);
2888
+ const store = new LocalStore17(dbPath);
2889
+ try {
2890
+ const deleted = store.clearEmbeddings();
2891
+ logger.info(`All embeddings cleared (${deleted} removed).`);
2892
+ } finally {
2893
+ store.close();
2894
+ }
2895
+ });
2896
+
2897
+ // src/commands/reset.ts
2898
+ import { Command as Command24 } from "commander";
2899
+ import { loadConfig as loadConfig21, getDbPath as getDbPath18 } from "unforgit-config";
2900
+ import { LocalStore as LocalStore18 } from "unforgit-db";
2901
+ import { RemoteClient as RemoteClient12 } from "unforgit-config";
2902
+ var resetCommand = new Command24("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").addHelpText("after", `
2903
+ Examples:
2904
+ unforgit reset Reset both local and remote
2905
+ unforgit reset --local Reset local store only
2906
+ unforgit reset --remote Reset remote store only
2907
+ unforgit reset --force Skip confirmation`).action(async (opts) => {
2908
+ const resetLocal = opts.local || !opts.local && !opts.remote;
2909
+ const resetRemote = opts.remote || !opts.local && !opts.remote;
2910
+ const targets = [
2911
+ resetLocal ? "local" : null,
2912
+ resetRemote ? "remote" : null
2913
+ ].filter(Boolean);
2914
+ if (!opts.force) {
2915
+ logger.info(
2916
+ `WARNING: This will permanently delete ALL memories, links, embeddings, and sync state from: ${targets.join(" + ")}`
2917
+ );
2918
+ logger.info("This action CANNOT be undone.");
2919
+ const confirmed = await confirm("Type 'yes' to confirm");
2920
+ if (!confirmed) {
2921
+ logger.info("Reset cancelled.");
2922
+ return;
2923
+ }
2924
+ }
2925
+ const config = loadConfig21();
2926
+ if (resetLocal) {
2927
+ const store = new LocalStore18(getDbPath18());
2928
+ try {
2929
+ const result = store.resetAll();
2930
+ logger.info(`Local reset complete:`);
2931
+ logger.info(` Memories deleted: ${result.memoriesDeleted}`);
2932
+ logger.info(` Links deleted: ${result.linksDeleted}`);
2933
+ logger.info(` Embeddings deleted: ${result.embeddingsDeleted}`);
2934
+ } finally {
2935
+ store.close();
2936
+ }
2937
+ }
2938
+ if (resetRemote) {
2939
+ if (!config.remote?.url) {
2940
+ if (opts.remote) {
2941
+ logger.error("No remote URL configured. Run 'unforgit init' first.");
2942
+ process.exit(EXIT_ERROR);
2943
+ }
2944
+ logger.info("No remote configured, skipping remote reset.");
2945
+ return;
2946
+ }
2947
+ const client = new RemoteClient12(config.remote.url);
2948
+ const orgId = config.remote.orgId;
2949
+ const repoId = config.remote.repoId;
2950
+ if (!orgId || !repoId) {
2951
+ logger.error("orgId and repoId must be configured for remote reset.");
2952
+ process.exit(EXIT_ERROR);
2953
+ }
2954
+ try {
2955
+ const result = await client.resetAll(orgId, repoId);
2956
+ logger.info(`Remote reset complete:`);
2957
+ logger.info(` Memories deleted: ${result.memoriesDeleted}`);
2958
+ logger.info(` Links deleted: ${result.linksDeleted}`);
2959
+ logger.info(` Embeddings deleted: ${result.embeddingsDeleted}`);
2960
+ } catch (err) {
2961
+ logger.error(err instanceof Error ? err.message : String(err));
2962
+ process.exit(EXIT_ERROR);
2963
+ }
2964
+ }
2965
+ });
2966
+
2967
+ // src/commands/doctor.ts
2968
+ import { Command as Command25 } from "commander";
2969
+ import { loadConfig as loadConfig22, getDbPath as getDbPath19, isInitialized as isInitialized16, getConfigPath as getConfigPath2 } from "unforgit-config";
2970
+ import { LocalStore as LocalStore19 } from "unforgit-db";
2971
+ import { RemoteClient as RemoteClient13 } from "unforgit-config";
2972
+ import fs4 from "fs";
2973
+ var doctorCommand = new Command25("doctor").description("Check system health and diagnose common issues").action(async () => {
2974
+ const results = [];
2975
+ if (!isInitialized16()) {
2976
+ results.push({
2977
+ check: "initialization",
2978
+ status: "error",
2979
+ message: "Not an unforgit repository. Run 'unforgit init' first."
2980
+ });
2981
+ if (isJsonMode()) {
2982
+ outputJson({ results });
2983
+ return;
2984
+ }
2985
+ printResults(results);
2986
+ process.exit(EXIT_CONFIG_ERROR);
2987
+ }
2988
+ results.push({ check: "initialization", status: "ok", message: "Repository initialized" });
2989
+ const configPath = getConfigPath2();
2990
+ try {
2991
+ const config = loadConfig22();
2992
+ results.push({ check: "config", status: "ok", message: `Valid config at ${configPath}` });
2993
+ if (process.platform !== "win32") {
2994
+ try {
2995
+ const stat = fs4.statSync(configPath);
2996
+ const mode = stat.mode & 511;
2997
+ if (mode & 36) {
2998
+ results.push({
2999
+ check: "config-permissions",
3000
+ status: "warn",
3001
+ message: `Config readable by others (mode ${mode.toString(8)}). Run: chmod 600 ${configPath}`
3002
+ });
3003
+ } else {
3004
+ results.push({ check: "config-permissions", status: "ok", message: "Config file permissions are secure" });
3005
+ }
3006
+ } catch {
3007
+ results.push({ check: "config-permissions", status: "warn", message: "Could not check config file permissions" });
3008
+ }
3009
+ }
3010
+ const store = new LocalStore19(getDbPath19());
3011
+ try {
3012
+ const orgId = config.remote.orgId || "local";
3013
+ const repoId = config.remote.repoId || "local";
3014
+ store.list({ orgId, repoId, limit: 1 });
3015
+ results.push({ check: "local-db", status: "ok", message: "SQLite database is accessible" });
3016
+ const stats = store.getEmbeddingStats(orgId, repoId);
3017
+ if (stats.total === 0) {
3018
+ results.push({ check: "embeddings", status: "ok", message: "No memories yet" });
3019
+ } else if (stats.withoutEmbedding === 0) {
3020
+ results.push({ check: "embeddings", status: "ok", message: `All ${stats.total} memories have embeddings` });
3021
+ } else {
3022
+ const pct = (stats.withEmbedding / stats.total * 100).toFixed(1);
3023
+ results.push({
3024
+ check: "embeddings",
3025
+ status: "warn",
3026
+ message: `${stats.withoutEmbedding}/${stats.total} memories lack embeddings (${pct}% coverage). Run 'unforgit embeddings backfill'`
3027
+ });
3028
+ }
3029
+ const pendingPush = store.getPendingPush();
3030
+ const conflicts = store.getConflicts();
3031
+ if (conflicts.length > 0) {
3032
+ results.push({
3033
+ check: "sync",
3034
+ status: "warn",
3035
+ message: `${conflicts.length} sync conflict(s) need resolution`
3036
+ });
3037
+ } else if (pendingPush.length > 0) {
3038
+ results.push({
3039
+ check: "sync",
3040
+ status: "ok",
3041
+ message: `${pendingPush.length} memory(s) pending push`
3042
+ });
3043
+ } else {
3044
+ results.push({ check: "sync", status: "ok", message: "Sync state clean" });
3045
+ }
3046
+ } finally {
3047
+ store.close();
3048
+ }
3049
+ const apiKey = process.env.UNFORGIT_API_KEY;
3050
+ if (config.remote.url) {
3051
+ if (!apiKey) {
3052
+ results.push({
3053
+ check: "auth",
3054
+ status: "warn",
3055
+ message: "No API key configured. Set the UNFORGIT_API_KEY environment variable."
3056
+ });
3057
+ } else {
3058
+ results.push({
3059
+ check: "auth",
3060
+ status: "ok",
3061
+ message: `API key configured: ${maskKey(apiKey)}`
3062
+ });
3063
+ }
3064
+ try {
3065
+ const res = await fetch(`${config.remote.url}/health`);
3066
+ if (res.ok) {
3067
+ results.push({ check: "remote", status: "ok", message: `Server reachable at ${config.remote.url}` });
3068
+ if (apiKey) {
3069
+ const client = new RemoteClient13(config.remote.url);
3070
+ try {
3071
+ await client.listApiKeys();
3072
+ results.push({ check: "remote-auth", status: "ok", message: "API key is valid" });
3073
+ } catch {
3074
+ results.push({ check: "remote-auth", status: "error", message: "API key is invalid or expired" });
3075
+ }
3076
+ }
3077
+ } else {
3078
+ results.push({
3079
+ check: "remote",
3080
+ status: "error",
3081
+ message: `Server returned HTTP ${res.status}`
3082
+ });
3083
+ }
3084
+ } catch (err) {
3085
+ results.push({
3086
+ check: "remote",
3087
+ status: "error",
3088
+ message: `Cannot connect to ${config.remote.url}: ${err instanceof Error ? err.message : err}`
3089
+ });
3090
+ }
3091
+ } else {
3092
+ results.push({ check: "remote", status: "warn", message: "No remote URL configured" });
3093
+ }
3094
+ const openaiKey = process.env.OPENAI_API_KEY;
3095
+ if (openaiKey) {
3096
+ results.push({ check: "openai", status: "ok", message: `OpenAI API key configured: ${maskKey(openaiKey)}` });
3097
+ } else {
3098
+ results.push({
3099
+ check: "openai",
3100
+ status: "warn",
3101
+ message: "No OpenAI API key. Auto-consolidation and embeddings backfill require it."
3102
+ });
3103
+ }
3104
+ } catch (err) {
3105
+ results.push({
3106
+ check: "config",
3107
+ status: "error",
3108
+ message: err instanceof Error ? err.message : String(err)
3109
+ });
3110
+ }
3111
+ if (isJsonMode()) {
3112
+ outputJson({ results });
3113
+ return;
3114
+ }
3115
+ printResults(results);
3116
+ });
3117
+ function printResults(results) {
3118
+ const icons = { ok: "[ok]", warn: "[!!]", error: "[ERR]" };
3119
+ logger.info("Unforgit Doctor\n");
3120
+ for (const r of results) {
3121
+ const icon = icons[r.status];
3122
+ logger.info(` ${icon} ${r.check}: ${r.message}`);
3123
+ }
3124
+ const errors = results.filter((r) => r.status === "error").length;
3125
+ const warnings = results.filter((r) => r.status === "warn").length;
3126
+ logger.info("");
3127
+ if (errors > 0) {
3128
+ logger.info(`${errors} error(s), ${warnings} warning(s)`);
3129
+ } else if (warnings > 0) {
3130
+ logger.info(`No errors, ${warnings} warning(s)`);
3131
+ } else {
3132
+ logger.info("All checks passed!");
3133
+ }
3134
+ }
3135
+
3136
+ // src/commands/completion.ts
3137
+ import { Command as Command26 } from "commander";
3138
+ var BASH_COMPLETION = `
3139
+ _unforgit_completions() {
3140
+ local cur prev commands
3141
+ COMPREPLY=()
3142
+ cur="\${COMP_WORDS[COMP_CWORD]}"
3143
+ prev="\${COMP_WORDS[COMP_CWORD-1]}"
3144
+ commands="init add recall promote consolidate deprecate supersede delete restore web link unlink links merge remerge similar history auto-consolidate unconsolidate status push pull remote log diff keys auth config embeddings reset doctor curate completion"
3145
+
3146
+ case "\${prev}" in
3147
+ unforgit)
3148
+ COMPREPLY=( $(compgen -W "\${commands}" -- "\${cur}") )
3149
+ return 0
3150
+ ;;
3151
+ remote)
3152
+ COMPREPLY=( $(compgen -W "add remove set-url show" -- "\${cur}") )
3153
+ return 0
3154
+ ;;
3155
+ keys)
3156
+ COMPREPLY=( $(compgen -W "create list revoke" -- "\${cur}") )
3157
+ return 0
3158
+ ;;
3159
+ auth)
3160
+ COMPREPLY=( $(compgen -W "set status remove openai openai-remove" -- "\${cur}") )
3161
+ return 0
3162
+ ;;
3163
+ config)
3164
+ COMPREPLY=( $(compgen -W "list get set unset" -- "\${cur}") )
3165
+ return 0
3166
+ ;;
3167
+ embeddings)
3168
+ COMPREPLY=( $(compgen -W "backfill stats clear" -- "\${cur}") )
3169
+ return 0
3170
+ ;;
3171
+ --type)
3172
+ COMPREPLY=( $(compgen -W "episodic semantic procedural" -- "\${cur}") )
3173
+ return 0
3174
+ ;;
3175
+ esac
3176
+
3177
+ if [[ "\${cur}" == -* ]]; then
3178
+ COMPREPLY=( $(compgen -W "--verbose --quiet --json --help --version" -- "\${cur}") )
3179
+ return 0
3180
+ fi
3181
+ }
3182
+ complete -F _unforgit_completions unforgit
3183
+ `.trim();
3184
+ var ZSH_COMPLETION = `
3185
+ #compdef unforgit
3186
+
3187
+ _unforgit() {
3188
+ local -a commands
3189
+ commands=(
3190
+ 'init:Initialize Unforgit in the current repository'
3191
+ 'add:Add a memory'
3192
+ 'recall:Recall memories matching a query'
3193
+ 'promote:Promote a local memory to remote'
3194
+ 'consolidate:Consolidate episodic memories'
3195
+ 'deprecate:Mark a memory as deprecated'
3196
+ 'supersede:Mark a memory as superseded'
3197
+ 'delete:Soft delete a memory'
3198
+ 'restore:Restore a soft-deleted memory'
3199
+ 'web:Start the web dashboard'
3200
+ 'link:Create a link between memories'
3201
+ 'unlink:Remove a link between memories'
3202
+ 'links:List links for a memory'
3203
+ 'merge:Consolidate multiple local memories'
3204
+ 'remerge:Update an existing consolidation'
3205
+ 'similar:Find similar memories'
3206
+ 'history:Show consolidation history'
3207
+ 'auto-consolidate:AI-powered consolidation'
3208
+ 'unconsolidate:Revert a consolidation'
3209
+ 'status:Show sync status'
3210
+ 'push:Push local memories to remote'
3211
+ 'pull:Pull remote memories to local'
3212
+ 'remote:Manage remotes'
3213
+ 'log:Show memory history log'
3214
+ 'diff:Show differences between local and remote'
3215
+ 'keys:Manage API keys'
3216
+ 'auth:Configure authentication'
3217
+ 'config:Manage configuration'
3218
+ 'embeddings:Manage embeddings'
3219
+ 'reset:Delete all memories'
3220
+ 'doctor:Check system health'
3221
+ 'curate:Preview or run lifecycle maintenance'
3222
+ 'completion:Generate shell completions'
3223
+ )
3224
+
3225
+ _arguments -C \\
3226
+ '--verbose[Enable verbose output]' \\
3227
+ '--quiet[Suppress non-essential output]' \\
3228
+ '--json[Output as JSON]' \\
3229
+ '--help[Show help]' \\
3230
+ '--version[Show version]' \\
3231
+ '1: :->cmd' \\
3232
+ '*::arg:->args'
3233
+
3234
+ case "$state" in
3235
+ cmd)
3236
+ _describe 'command' commands
3237
+ ;;
3238
+ esac
3239
+ }
3240
+
3241
+ _unforgit "$@"
3242
+ `.trim();
3243
+ var FISH_COMPLETION = `
3244
+ complete -c unforgit -f
3245
+ complete -c unforgit -n '__fish_use_subcommand' -a 'init' -d 'Initialize Unforgit'
3246
+ complete -c unforgit -n '__fish_use_subcommand' -a 'add' -d 'Add a memory'
3247
+ complete -c unforgit -n '__fish_use_subcommand' -a 'recall' -d 'Recall memories'
3248
+ complete -c unforgit -n '__fish_use_subcommand' -a 'promote' -d 'Promote to remote'
3249
+ complete -c unforgit -n '__fish_use_subcommand' -a 'consolidate' -d 'Consolidate memories'
3250
+ complete -c unforgit -n '__fish_use_subcommand' -a 'deprecate' -d 'Deprecate a memory'
3251
+ complete -c unforgit -n '__fish_use_subcommand' -a 'supersede' -d 'Supersede a memory'
3252
+ complete -c unforgit -n '__fish_use_subcommand' -a 'delete' -d 'Delete a memory'
3253
+ complete -c unforgit -n '__fish_use_subcommand' -a 'restore' -d 'Restore a memory'
3254
+ complete -c unforgit -n '__fish_use_subcommand' -a 'web' -d 'Start web dashboard'
3255
+ complete -c unforgit -n '__fish_use_subcommand' -a 'link' -d 'Link two memories'
3256
+ complete -c unforgit -n '__fish_use_subcommand' -a 'unlink' -d 'Unlink memories'
3257
+ complete -c unforgit -n '__fish_use_subcommand' -a 'links' -d 'List links'
3258
+ complete -c unforgit -n '__fish_use_subcommand' -a 'merge' -d 'Merge memories'
3259
+ complete -c unforgit -n '__fish_use_subcommand' -a 'remerge' -d 'Update consolidation'
3260
+ complete -c unforgit -n '__fish_use_subcommand' -a 'similar' -d 'Find similar memories'
3261
+ complete -c unforgit -n '__fish_use_subcommand' -a 'history' -d 'Consolidation history'
3262
+ complete -c unforgit -n '__fish_use_subcommand' -a 'auto-consolidate' -d 'AI consolidation'
3263
+ complete -c unforgit -n '__fish_use_subcommand' -a 'unconsolidate' -d 'Revert consolidation'
3264
+ complete -c unforgit -n '__fish_use_subcommand' -a 'status' -d 'Show sync status'
3265
+ complete -c unforgit -n '__fish_use_subcommand' -a 'push' -d 'Push to remote'
3266
+ complete -c unforgit -n '__fish_use_subcommand' -a 'pull' -d 'Pull from remote'
3267
+ complete -c unforgit -n '__fish_use_subcommand' -a 'remote' -d 'Manage remotes'
3268
+ complete -c unforgit -n '__fish_use_subcommand' -a 'log' -d 'Memory log'
3269
+ complete -c unforgit -n '__fish_use_subcommand' -a 'diff' -d 'Show differences'
3270
+ complete -c unforgit -n '__fish_use_subcommand' -a 'keys' -d 'Manage API keys'
3271
+ complete -c unforgit -n '__fish_use_subcommand' -a 'auth' -d 'Configure auth'
3272
+ complete -c unforgit -n '__fish_use_subcommand' -a 'config' -d 'Manage config'
3273
+ complete -c unforgit -n '__fish_use_subcommand' -a 'embeddings' -d 'Manage embeddings'
3274
+ complete -c unforgit -n '__fish_use_subcommand' -a 'reset' -d 'Delete all memories'
3275
+ complete -c unforgit -n '__fish_use_subcommand' -a 'doctor' -d 'Check system health'
3276
+ complete -c unforgit -n '__fish_use_subcommand' -a 'curate' -d 'Preview or run lifecycle maintenance'
3277
+ complete -c unforgit -n '__fish_use_subcommand' -a 'completion' -d 'Generate completions'
3278
+ complete -c unforgit -l verbose -d 'Enable verbose output'
3279
+ complete -c unforgit -l quiet -d 'Suppress non-essential output'
3280
+ complete -c unforgit -l json -d 'Output as JSON'
3281
+ `.trim();
3282
+ var completionCommand = new Command26("completion").description("Generate shell completion scripts").argument("<shell>", "Shell type (bash, zsh, fish)").addHelpText("after", `
3283
+ Examples:
3284
+ unforgit completion bash >> ~/.bashrc
3285
+ unforgit completion zsh >> ~/.zshrc
3286
+ unforgit completion fish > ~/.config/fish/completions/unforgit.fish`).action((shell) => {
3287
+ switch (shell.toLowerCase()) {
3288
+ case "bash":
3289
+ console.log(BASH_COMPLETION);
3290
+ break;
3291
+ case "zsh":
3292
+ console.log(ZSH_COMPLETION);
3293
+ break;
3294
+ case "fish":
3295
+ console.log(FISH_COMPLETION);
3296
+ break;
3297
+ default:
3298
+ logger.error(`Unsupported shell: ${shell}. Use bash, zsh, or fish.`);
3299
+ process.exit(1);
3300
+ }
3301
+ });
3302
+
3303
+ // src/commands/curate.ts
3304
+ import { Command as Command27 } from "commander";
3305
+ import { getDbPath as getDbPath20, isInitialized as isInitialized17, loadConfig as loadConfig23 } from "unforgit-config";
3306
+ import { LocalStore as LocalStore20 } from "unforgit-db";
3307
+ import { RemoteClient as RemoteClient14 } from "unforgit-config";
3308
+ import { runLocalLifecycleMaintenance } from "unforgit-core";
3309
+ function formatCandidatePreview2(candidate) {
3310
+ const lines = [
3311
+ `Group: ${candidate.reason}`,
3312
+ `Tags: ${candidate.suggestedTags.join(", ") || "none"}`,
3313
+ "Memories:"
3314
+ ];
3315
+ for (const memory of candidate.memories) {
3316
+ const text = memory.text.length > 80 ? `${memory.text.slice(0, 80)}...` : memory.text;
3317
+ lines.push(` - [${memory.memoryType}] ${memory.id.slice(0, 8)}: ${text}`);
3318
+ }
3319
+ return lines.join("\n");
3320
+ }
3321
+ var curateCommand = new Command27("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
+ Examples:
3323
+ unforgit curate
3324
+ unforgit curate --execute
3325
+ unforgit curate --remote --execute --model gpt-5.4`).action(async (opts) => {
3326
+ if (!isInitialized17()) {
3327
+ logger.error("Unforgit not initialized. Run 'unforgit init' first.");
3328
+ process.exit(EXIT_CONFIG_ERROR);
3329
+ }
3330
+ const config = loadConfig23();
3331
+ const orgId = config.remote.orgId || "local";
3332
+ const repoId = config.remote.repoId || "local";
3333
+ const dryRun = opts.execute ? false : void 0;
3334
+ try {
3335
+ const result = opts.remote ? await runRemoteCurate(config.remote.url, {
3336
+ orgId,
3337
+ repoId,
3338
+ dryRun,
3339
+ model: opts.model,
3340
+ preserveOriginals: opts.preserve !== false
3341
+ }) : await runLocalCurate(getDbPath20(), orgId, repoId, {
3342
+ dryRun,
3343
+ model: opts.model,
3344
+ preserveOriginals: opts.preserve !== false,
3345
+ lifecycle: config.lifecycle
3346
+ });
3347
+ if (isJsonMode()) {
3348
+ outputJson(result);
3349
+ return;
3350
+ }
3351
+ logger.info(result.dryRun ? "Lifecycle maintenance preview\n" : "Lifecycle maintenance execution\n");
3352
+ logger.info(`Active memories scanned: ${result.totalActiveMemories}`);
3353
+ logger.info(`Expiring episodic memories: ${result.expiredCandidates.length}`);
3354
+ logger.info(`Strengthened candidates: ${result.strengthenedCandidates.length}`);
3355
+ logger.info(`Consolidation candidates: ${result.consolidationCandidates.length}`);
3356
+ if (result.expiredCandidates.length > 0) {
3357
+ logger.info("\nExpired candidates:");
3358
+ for (const candidate of result.expiredCandidates.slice(0, 10)) {
3359
+ logger.info(` ${candidate.id.slice(0, 8)} (${candidate.ttlSeconds}s): ${candidate.textPreview}`);
3360
+ }
3361
+ }
3362
+ if (result.strengthenedCandidates.length > 0) {
3363
+ logger.info("\nStrengthened candidates:");
3364
+ for (const candidate of result.strengthenedCandidates.slice(0, 10)) {
3365
+ logger.info(
3366
+ ` ${candidate.id.slice(0, 8)} [${candidate.recommendedAction}] (${candidate.usageCount} recalls): ${candidate.textPreview}`
3367
+ );
3368
+ }
3369
+ }
3370
+ if (result.consolidationCandidates.length > 0) {
3371
+ logger.info("\nConsolidation candidates:");
3372
+ for (const candidate of result.consolidationCandidates.slice(0, 5)) {
3373
+ logger.info(formatCandidatePreview2(candidate));
3374
+ logger.info("");
3375
+ }
3376
+ }
3377
+ if (!result.dryRun && result.executedConsolidations.length > 0) {
3378
+ logger.info("Executed consolidations:");
3379
+ for (const executed of result.executedConsolidations) {
3380
+ logger.info(
3381
+ ` ${executed.consolidatedId.slice(0, 8)} from ${executed.sourceIds.length} memories`
3382
+ );
3383
+ }
3384
+ }
3385
+ for (const warning of result.warnings) {
3386
+ logger.warn(warning);
3387
+ }
3388
+ for (const error of result.errors) {
3389
+ logger.error(error);
3390
+ }
3391
+ if (result.dryRun) {
3392
+ logger.info("\nDry run complete. Re-run with --execute to apply expiry and consolidation.");
3393
+ }
3394
+ } catch (error) {
3395
+ logger.error(
3396
+ `Curate: ${error instanceof Error ? error.message : error}`
3397
+ );
3398
+ process.exit(EXIT_ERROR);
3399
+ }
3400
+ });
3401
+ async function runLocalCurate(dbPath, orgId, repoId, options) {
3402
+ const store = new LocalStore20(dbPath);
3403
+ try {
3404
+ return await runLocalLifecycleMaintenance(store, orgId, repoId, options);
3405
+ } finally {
3406
+ store.close();
3407
+ }
3408
+ }
3409
+ async function runRemoteCurate(remoteUrl, options) {
3410
+ if (!remoteUrl) {
3411
+ throw new Error("Remote URL not configured. Update unforgit.yaml or omit --remote.");
3412
+ }
3413
+ const client = new RemoteClient14(remoteUrl);
3414
+ return client.runLifecycle(options);
3415
+ }
3416
+
3417
+ // src/index.ts
3418
+ import { createRequire } from "module";
3419
+ var require2 = createRequire(import.meta.url);
3420
+ var pkg = require2("../package.json");
3421
+ var cleanupHandlers = [];
3422
+ function registerCleanup(fn) {
3423
+ cleanupHandlers.push(fn);
3424
+ }
3425
+ function unregisterCleanup(fn) {
3426
+ const idx = cleanupHandlers.indexOf(fn);
3427
+ if (idx >= 0) cleanupHandlers.splice(idx, 1);
3428
+ }
3429
+ function runCleanup() {
3430
+ for (const fn of cleanupHandlers) {
3431
+ try {
3432
+ fn();
3433
+ } catch {
3434
+ }
3435
+ }
3436
+ }
3437
+ process.on("SIGINT", () => {
3438
+ runCleanup();
3439
+ process.exit(EXIT_SIGINT);
3440
+ });
3441
+ process.on("SIGTERM", () => {
3442
+ runCleanup();
3443
+ process.exit(EXIT_SIGTERM);
3444
+ });
3445
+ process.on("uncaughtException", (err) => {
3446
+ console.error(`fatal: ${err.message}`);
3447
+ runCleanup();
3448
+ process.exit(EXIT_ERROR);
3449
+ });
3450
+ process.on("unhandledRejection", (err) => {
3451
+ console.error(`fatal: ${err instanceof Error ? err.message : err}`);
3452
+ runCleanup();
3453
+ process.exit(EXIT_ERROR);
3454
+ });
3455
+ var program = new Command28();
3456
+ 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
+ const opts = program.opts();
3458
+ if (opts.quiet) setVerbosity(0);
3459
+ else if (opts.verbose) setVerbosity(2);
3460
+ if (opts.json) setJsonMode(true);
3461
+ });
3462
+ program.addCommand(initCommand);
3463
+ program.addCommand(addCommand);
3464
+ program.addCommand(recallCommand);
3465
+ program.addCommand(promoteCommand);
3466
+ program.addCommand(consolidateCommand);
3467
+ program.addCommand(deprecateCommand);
3468
+ program.addCommand(supersedeCommand);
3469
+ program.addCommand(webCommand);
3470
+ program.addCommand(linkCommand);
3471
+ program.addCommand(unlinkCommand);
3472
+ program.addCommand(linksCommand);
3473
+ program.addCommand(mergeCommand);
3474
+ program.addCommand(remergeCommand);
3475
+ program.addCommand(similarCommand);
3476
+ program.addCommand(historyCommand);
3477
+ program.addCommand(deleteCommand);
3478
+ program.addCommand(restoreCommand);
3479
+ program.addCommand(autoConsolidateCommand);
3480
+ program.addCommand(unconsolidateCommand);
3481
+ program.addCommand(statusCommand);
3482
+ program.addCommand(pushCommand);
3483
+ program.addCommand(pullCommand);
3484
+ program.addCommand(remoteCommand);
3485
+ program.addCommand(logCommand);
3486
+ program.addCommand(diffCommand);
3487
+ program.addCommand(keysCommand);
3488
+ program.addCommand(authCommand);
3489
+ program.addCommand(configCommand);
3490
+ program.addCommand(embeddingsCommand);
3491
+ program.addCommand(resetCommand);
3492
+ program.addCommand(doctorCommand);
3493
+ program.addCommand(curateCommand);
3494
+ program.addCommand(completionCommand);
3495
+ program.parseAsync().catch((err) => {
3496
+ console.error(`fatal: ${err instanceof Error ? err.message : err}`);
3497
+ runCleanup();
3498
+ process.exit(EXIT_ERROR);
3499
+ });
3500
+ export {
3501
+ registerCleanup,
3502
+ unregisterCleanup
3503
+ };
3504
+ //# sourceMappingURL=index.js.map