@betterdb/memory 0.1.2 → 0.4.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/README.md +101 -10
- package/package.json +3 -1
- package/scripts/aging-worker.ts +4 -1
- package/scripts/docker-valkey.sh +101 -0
- package/scripts/register-hooks.ts +94 -0
- package/scripts/setup-index.ts +10 -3
- package/scripts/unregister-hooks.ts +79 -0
- package/src/client/memory-store.ts +406 -0
- package/src/client/model.ts +10 -10
- package/src/client/providers/local.ts +58 -0
- package/src/client/valkey.ts +9 -0
- package/src/config.ts +38 -6
- package/src/hooks/post-tool.ts +2 -0
- package/src/hooks/pre-tool.ts +12 -11
- package/src/hooks/session-end.ts +14 -4
- package/src/hooks/session-start.ts +33 -8
- package/src/index.ts +379 -21
- package/src/mcp/server.ts +82 -42
- package/src/memory/aging.ts +78 -196
- package/src/memory/recall.ts +169 -0
- package/src/memory/retrieval.ts +73 -70
package/src/hooks/session-end.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { readRawPayload, runHook } from "./_utils.js";
|
|
2
2
|
import { getValkeyClient } from "../client/valkey.js";
|
|
3
|
+
import { getPluginMemoryStore } from "../client/memory-store.js";
|
|
3
4
|
import { createModelClient } from "../client/model.js";
|
|
4
5
|
import {
|
|
5
6
|
SessionCapture,
|
|
@@ -8,7 +9,7 @@ import {
|
|
|
8
9
|
getCwdProject,
|
|
9
10
|
} from "../memory/capture.js";
|
|
10
11
|
import { SessionEventSchema, type EpisodicMemory } from "../memory/schema.js";
|
|
11
|
-
import { config } from "../config.js";
|
|
12
|
+
import { config, isConfigured } from "../config.js";
|
|
12
13
|
import { unlink } from "node:fs/promises";
|
|
13
14
|
|
|
14
15
|
/**
|
|
@@ -25,6 +26,7 @@ import { unlink } from "node:fs/promises";
|
|
|
25
26
|
* 3. If model client is unavailable, queue for later processing
|
|
26
27
|
*/
|
|
27
28
|
runHook(async () => {
|
|
29
|
+
if (!isConfigured()) return;
|
|
28
30
|
const payload = await readRawPayload();
|
|
29
31
|
const sessionId = payload["session_id"] as string;
|
|
30
32
|
const cwd = (payload["cwd"] as string) ?? process.cwd();
|
|
@@ -77,7 +79,14 @@ runHook(async () => {
|
|
|
77
79
|
transcript.slice(-half);
|
|
78
80
|
}
|
|
79
81
|
|
|
80
|
-
|
|
82
|
+
let valkeyClient;
|
|
83
|
+
try {
|
|
84
|
+
valkeyClient = await getValkeyClient();
|
|
85
|
+
} catch {
|
|
86
|
+
await cleanup(eventFilePath);
|
|
87
|
+
return; // Valkey unreachable — skip silently
|
|
88
|
+
}
|
|
89
|
+
|
|
81
90
|
const project = getCwdProject();
|
|
82
91
|
const branch = getGitBranch();
|
|
83
92
|
|
|
@@ -102,7 +111,6 @@ runHook(async () => {
|
|
|
102
111
|
|
|
103
112
|
const summary = await modelClient.summarize(transcript);
|
|
104
113
|
const importance = computeInitialImportance(summary);
|
|
105
|
-
const embedding = await modelClient.embed(summary.oneLineSummary);
|
|
106
114
|
|
|
107
115
|
const memory: EpisodicMemory = {
|
|
108
116
|
memoryId: crypto.randomUUID(),
|
|
@@ -115,7 +123,9 @@ runHook(async () => {
|
|
|
115
123
|
lastAccessed: new Date().toISOString(),
|
|
116
124
|
};
|
|
117
125
|
|
|
118
|
-
await
|
|
126
|
+
const store = await getPluginMemoryStore((t) => modelClient.embed(t));
|
|
127
|
+
await store.storeMemory(memory);
|
|
128
|
+
await store.close();
|
|
119
129
|
await valkeyClient.quit();
|
|
120
130
|
await cleanup(eventFilePath);
|
|
121
131
|
});
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { readRawPayload, runHook } from "./_utils.js";
|
|
2
|
-
import {
|
|
2
|
+
import { getPluginMemoryStore } from "../client/memory-store.js";
|
|
3
3
|
import { createModelClient } from "../client/model.js";
|
|
4
|
-
import { SessionCapture } from "../memory/capture.js";
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
4
|
+
import { SessionCapture, getGitBranch } from "../memory/capture.js";
|
|
5
|
+
import { formatForInjection } from "../memory/retrieval.js";
|
|
6
|
+
import { escalatingRecall } from "../memory/recall.js";
|
|
7
|
+
import { config, isConfigured } from "../config.js";
|
|
7
8
|
|
|
8
9
|
/**
|
|
9
10
|
* SessionStart hook: Retrieves relevant memories and injects context.
|
|
@@ -14,6 +15,13 @@ import { config } from "../config.js";
|
|
|
14
15
|
* - Exit 0 for success
|
|
15
16
|
*/
|
|
16
17
|
runHook(async () => {
|
|
18
|
+
if (!isConfigured()) {
|
|
19
|
+
process.stdout.write(
|
|
20
|
+
"[BetterDB Memory] Not configured yet. Run /betterdb-memory:setup to connect to Valkey.\n",
|
|
21
|
+
);
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
|
|
17
25
|
const payload = await readRawPayload();
|
|
18
26
|
const cwd = (payload["cwd"] as string) ?? process.cwd();
|
|
19
27
|
|
|
@@ -21,15 +29,32 @@ runHook(async () => {
|
|
|
21
29
|
process.chdir(cwd);
|
|
22
30
|
}
|
|
23
31
|
|
|
24
|
-
const valkeyClient = await getValkeyClient();
|
|
25
32
|
const modelClient = await createModelClient();
|
|
26
33
|
|
|
34
|
+
let store;
|
|
35
|
+
try {
|
|
36
|
+
store = await getPluginMemoryStore((t) => modelClient.embed(t));
|
|
37
|
+
} catch {
|
|
38
|
+
return; // Valkey unreachable — skip silently
|
|
39
|
+
}
|
|
40
|
+
|
|
27
41
|
const capture = new SessionCapture();
|
|
28
42
|
const queryContext = await capture.getQueryContext();
|
|
29
43
|
|
|
30
|
-
const retriever = new MemoryRetriever(valkeyClient, modelClient);
|
|
31
44
|
const project = queryContext.split("\n")[0]?.replace("Project: ", "") ?? "unknown";
|
|
32
|
-
const
|
|
45
|
+
const branch = getGitBranch();
|
|
46
|
+
// Project+branch-scoped, threshold-gated recall (no cross-project auto-inject
|
|
47
|
+
// at startup — nothing to consent to yet). Only memories clearing the
|
|
48
|
+
// relevance bar are injected, so we stop padding context with irrelevant
|
|
49
|
+
// top-N filler.
|
|
50
|
+
const result = await escalatingRecall(store, queryContext, {
|
|
51
|
+
project,
|
|
52
|
+
...(branch !== "unknown" ? { branch } : {}),
|
|
53
|
+
crossProjectRequested: false,
|
|
54
|
+
});
|
|
55
|
+
const memories = result.hits
|
|
56
|
+
.slice(0, config.memory.maxContextMemories)
|
|
57
|
+
.map((h) => h.memory);
|
|
33
58
|
|
|
34
59
|
if (memories.length > 0) {
|
|
35
60
|
const formatted = formatForInjection(memories);
|
|
@@ -39,5 +64,5 @@ runHook(async () => {
|
|
|
39
64
|
process.stdout.write(formatted);
|
|
40
65
|
}
|
|
41
66
|
|
|
42
|
-
await
|
|
67
|
+
await store.close();
|
|
43
68
|
});
|
package/src/index.ts
CHANGED
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
import { existsSync, readFileSync, writeFileSync, mkdirSync, chmodSync, rmSync } from "node:fs";
|
|
14
14
|
import { join, resolve } from "node:path";
|
|
15
15
|
|
|
16
|
-
const VERSION = "0.
|
|
16
|
+
const VERSION = "0.4.0";
|
|
17
17
|
const HOME = process.env["HOME"] ?? process.env["USERPROFILE"] ?? "";
|
|
18
18
|
const BETTERDB_DIR = join(HOME, ".betterdb");
|
|
19
19
|
const BIN_DIR = join(BETTERDB_DIR, "bin");
|
|
@@ -36,11 +36,19 @@ Usage:
|
|
|
36
36
|
betterdb-memory <command>
|
|
37
37
|
|
|
38
38
|
Commands:
|
|
39
|
-
install
|
|
40
|
-
uninstall
|
|
41
|
-
status
|
|
42
|
-
maintain
|
|
43
|
-
|
|
39
|
+
install Compile binaries, register hooks + MCP server
|
|
40
|
+
uninstall Remove hooks, MCP server, and compiled binaries
|
|
41
|
+
status Check health of Valkey and model providers
|
|
42
|
+
maintain Run aging/consolidation pipeline manually
|
|
43
|
+
forget Bulk-delete memories by scope (dry run; pass --apply)
|
|
44
|
+
Flags: --project <name> (default: cwd) | --all-projects
|
|
45
|
+
--branch <name> --tags <a,b> --apply
|
|
46
|
+
migrate Move legacy betterdb:memory:* memories into the MemoryStore
|
|
47
|
+
(dry run; pass --apply to perform)
|
|
48
|
+
ingest-claude-md Ingest a CLAUDE.md / MEMORY.md file into the store [path]
|
|
49
|
+
setup-index Create the episodic vector index (recovery after install)
|
|
50
|
+
docker-valkey Manage Docker Valkey container [start|stop|status|remove]
|
|
51
|
+
version Print version
|
|
44
52
|
|
|
45
53
|
Environment:
|
|
46
54
|
BETTERDB_VALKEY_URL Valkey connection (default: redis://localhost:6379)
|
|
@@ -63,6 +71,28 @@ switch (command) {
|
|
|
63
71
|
case "maintain":
|
|
64
72
|
await runMaintain();
|
|
65
73
|
break;
|
|
74
|
+
case "forget":
|
|
75
|
+
await runForget(process.argv.slice(3));
|
|
76
|
+
break;
|
|
77
|
+
case "migrate":
|
|
78
|
+
await runMigrate(process.argv.includes("--apply"));
|
|
79
|
+
break;
|
|
80
|
+
case "ingest-claude-md":
|
|
81
|
+
await runIngestClaudeMd(process.argv[3]);
|
|
82
|
+
break;
|
|
83
|
+
case "setup-index":
|
|
84
|
+
await runSetupIndex();
|
|
85
|
+
break;
|
|
86
|
+
case "docker-valkey": {
|
|
87
|
+
const action = process.argv[3] ?? "start";
|
|
88
|
+
const port = process.argv[4] ?? "6379";
|
|
89
|
+
const script = join(PKG_ROOT, "scripts", "docker-valkey.sh");
|
|
90
|
+
const result = Bun.spawnSync(["bash", script, port, action]);
|
|
91
|
+
process.stdout.write(result.stdout);
|
|
92
|
+
process.stderr.write(result.stderr);
|
|
93
|
+
process.exit(result.exitCode);
|
|
94
|
+
break;
|
|
95
|
+
}
|
|
66
96
|
case "version":
|
|
67
97
|
case "--version":
|
|
68
98
|
case "-v":
|
|
@@ -175,12 +205,14 @@ async function runInstall() {
|
|
|
175
205
|
}
|
|
176
206
|
}
|
|
177
207
|
|
|
178
|
-
settings["hooks"]
|
|
208
|
+
const existingHooks = (settings["hooks"] ?? {}) as Record<string, unknown[]>;
|
|
209
|
+
const betterdbHooks: Record<string, unknown[]> = {
|
|
179
210
|
SessionStart: [{ hooks: [{ type: "command", command: join(BIN_DIR, "session-start") }] }],
|
|
180
211
|
PreToolUse: [{ matcher: "", hooks: [{ type: "command", command: join(BIN_DIR, "pre-tool") }] }],
|
|
181
212
|
PostToolUse: [{ matcher: "", hooks: [{ type: "command", command: join(BIN_DIR, "post-tool") }] }],
|
|
182
213
|
Stop: [{ hooks: [{ type: "command", command: join(BIN_DIR, "session-end") }] }],
|
|
183
214
|
};
|
|
215
|
+
settings["hooks"] = mergeHooks(existingHooks, betterdbHooks);
|
|
184
216
|
|
|
185
217
|
writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + "\n");
|
|
186
218
|
console.log(" Registered 4 hooks in ~/.claude/settings.json");
|
|
@@ -202,10 +234,16 @@ async function runInstall() {
|
|
|
202
234
|
console.log("\nSetting up Valkey index...");
|
|
203
235
|
try {
|
|
204
236
|
const { getValkeyClient } = await import("./client/valkey.js");
|
|
205
|
-
const
|
|
237
|
+
const { getPluginMemoryStore } = await import("./client/memory-store.js");
|
|
238
|
+
const { createModelClient } = await import("./client/model.js");
|
|
206
239
|
const client = await getValkeyClient();
|
|
207
|
-
await
|
|
240
|
+
const modelClient = await createModelClient();
|
|
241
|
+
// Record the active provider/dimension so a later provider swap is caught.
|
|
242
|
+
await client.assertEmbedDim(modelClient.embedDim, modelClient.preset.embedModel);
|
|
243
|
+
const store = await getPluginMemoryStore((t) => modelClient.embed(t));
|
|
244
|
+
await store.ensureIndex();
|
|
208
245
|
console.log(" Valkey index ready");
|
|
246
|
+
await store.close();
|
|
209
247
|
await client.quit();
|
|
210
248
|
} catch (err) {
|
|
211
249
|
console.log(` WARNING: Index setup failed (${err instanceof Error ? err.message : String(err)})`);
|
|
@@ -318,9 +356,19 @@ async function runStatus() {
|
|
|
318
356
|
try {
|
|
319
357
|
const { config } = await import("./config.js");
|
|
320
358
|
const { getValkeyClient } = await import("./client/valkey.js");
|
|
359
|
+
const { getPluginMemoryStore } = await import("./client/memory-store.js");
|
|
321
360
|
const client = await getValkeyClient();
|
|
322
|
-
const
|
|
323
|
-
|
|
361
|
+
const store = await getPluginMemoryStore();
|
|
362
|
+
const stats = await store.stats();
|
|
363
|
+
console.log(`OK (${stats.itemCount} memories, ${config.valkey.url})`);
|
|
364
|
+
const w = stats.config.weights;
|
|
365
|
+
const halfLifeDays = Math.round(stats.config.halfLifeSeconds / 86400);
|
|
366
|
+
console.log(
|
|
367
|
+
` Recall scoring: half-life ${halfLifeDays}d · ` +
|
|
368
|
+
`weights sim/rec/imp ${w.similarity}/${w.recency}/${w.importance}` +
|
|
369
|
+
(stats.evictions > 0 ? ` · ${stats.evictions} evictions` : ""),
|
|
370
|
+
);
|
|
371
|
+
await store.close();
|
|
324
372
|
await client.quit();
|
|
325
373
|
} catch (err) {
|
|
326
374
|
console.log(`FAILED (${err instanceof Error ? err.message : String(err)})`);
|
|
@@ -364,6 +412,30 @@ async function runStatus() {
|
|
|
364
412
|
console.log("FAILED (could not read settings)");
|
|
365
413
|
}
|
|
366
414
|
|
|
415
|
+
// Check Docker container (only if config has "docker": true)
|
|
416
|
+
const dockerFlag = readConfigValue("docker");
|
|
417
|
+
if (dockerFlag === "true") {
|
|
418
|
+
process.stdout.write("Docker container... ");
|
|
419
|
+
const script = join(PKG_ROOT, "scripts", "docker-valkey.sh");
|
|
420
|
+
if (existsSync(script)) {
|
|
421
|
+
const result = Bun.spawnSync(["bash", script, "6379", "status"]);
|
|
422
|
+
const output = result.stdout.toString().trim();
|
|
423
|
+
if (output.includes("is running")) {
|
|
424
|
+
const portMatch = output.match(/port (\d+)/);
|
|
425
|
+
console.log(`OK (betterdb-valkey, running, port ${portMatch?.[1] ?? "unknown"})`);
|
|
426
|
+
} else if (output.includes("stopped")) {
|
|
427
|
+
console.log(`STOPPED (run: bunx @betterdb/memory docker-valkey)`);
|
|
428
|
+
} else {
|
|
429
|
+
console.log(`NOT FOUND (run: bunx @betterdb/memory docker-valkey)`);
|
|
430
|
+
}
|
|
431
|
+
} else {
|
|
432
|
+
console.log("SCRIPT MISSING (docker-valkey.sh not found)");
|
|
433
|
+
}
|
|
434
|
+
} else {
|
|
435
|
+
process.stdout.write("Docker container... ");
|
|
436
|
+
console.log("NOT USED (Valkey managed externally)");
|
|
437
|
+
}
|
|
438
|
+
|
|
367
439
|
// Check config file
|
|
368
440
|
process.stdout.write("Config file... ");
|
|
369
441
|
if (existsSync(CONFIG_PATH)) {
|
|
@@ -381,30 +453,292 @@ async function runMaintain() {
|
|
|
381
453
|
console.log("BetterDB Memory for Claude Code — Maintenance\n");
|
|
382
454
|
|
|
383
455
|
const { getValkeyClient } = await import("./client/valkey.js");
|
|
456
|
+
const { getPluginMemoryStore } = await import("./client/memory-store.js");
|
|
384
457
|
const { createModelClient } = await import("./client/model.js");
|
|
385
458
|
const { AgingPipeline } = await import("./memory/aging.js");
|
|
386
459
|
|
|
387
460
|
const valkeyClient = await getValkeyClient();
|
|
388
461
|
const modelClient = await createModelClient();
|
|
389
|
-
const
|
|
462
|
+
const store = await getPluginMemoryStore((t) => modelClient.embed(t));
|
|
463
|
+
const pipeline = new AgingPipeline(valkeyClient, store, modelClient);
|
|
464
|
+
|
|
465
|
+
const memories = await store.listMemories();
|
|
466
|
+
console.log(`Total memories: ${memories.length}`);
|
|
467
|
+
|
|
468
|
+
await pipeline.runFullPipeline();
|
|
469
|
+
|
|
470
|
+
console.log("\nAging pipeline complete.");
|
|
471
|
+
await store.close();
|
|
472
|
+
await valkeyClient.quit();
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
// ---------------------------------------------------------------------------
|
|
476
|
+
// forget (bulk delete by scope: project / branch / tags)
|
|
477
|
+
// ---------------------------------------------------------------------------
|
|
478
|
+
|
|
479
|
+
async function runForget(argv: string[]) {
|
|
480
|
+
console.log("BetterDB Memory for Claude Code — Forget by scope\n");
|
|
481
|
+
|
|
482
|
+
const flag = (name: string): string | undefined => {
|
|
483
|
+
const i = argv.indexOf(`--${name}`);
|
|
484
|
+
return i >= 0 ? argv[i + 1] : undefined;
|
|
485
|
+
};
|
|
486
|
+
const apply = argv.includes("--apply");
|
|
487
|
+
const allProjects = argv.includes("--all-projects");
|
|
488
|
+
const branch = flag("branch");
|
|
489
|
+
const tags = flag("tags")?.split(",").map((t) => t.trim()).filter(Boolean);
|
|
490
|
+
|
|
491
|
+
const { getValkeyClient } = await import("./client/valkey.js");
|
|
492
|
+
const { getPluginMemoryStore } = await import("./client/memory-store.js");
|
|
493
|
+
const { getCwdProject } = await import("./memory/capture.js");
|
|
494
|
+
|
|
495
|
+
const project = allProjects ? undefined : (flag("project") ?? getCwdProject());
|
|
496
|
+
|
|
497
|
+
// Refuse an unbounded delete: --all-projects must be narrowed by branch/tags.
|
|
498
|
+
if (project === undefined && branch === undefined && (!tags || tags.length === 0)) {
|
|
499
|
+
console.error("Refusing to delete every memory. Narrow --all-projects with --branch or --tags.");
|
|
500
|
+
process.exit(1);
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
const scopeDesc = [
|
|
504
|
+
project !== undefined ? `project=${project}` : "all projects",
|
|
505
|
+
branch !== undefined ? `branch=${branch}` : null,
|
|
506
|
+
tags && tags.length > 0 ? `tags=${tags.join(",")}` : null,
|
|
507
|
+
].filter(Boolean).join(", ");
|
|
508
|
+
console.log(`Scope: ${scopeDesc}`);
|
|
509
|
+
|
|
510
|
+
const valkeyClient = await getValkeyClient();
|
|
511
|
+
const store = await getPluginMemoryStore();
|
|
512
|
+
|
|
513
|
+
const scope = {
|
|
514
|
+
...(project !== undefined ? { project } : {}),
|
|
515
|
+
...(branch !== undefined ? { branch } : {}),
|
|
516
|
+
...(tags && tags.length > 0 ? { tags } : {}),
|
|
517
|
+
};
|
|
518
|
+
|
|
519
|
+
// Preview through the SAME native scope filter forgetByScope deletes with, so
|
|
520
|
+
// the dry-run count is exactly what --apply will remove (older memories
|
|
521
|
+
// without native tags are matched identically by both paths).
|
|
522
|
+
const candidates = await store.listByScope(scope);
|
|
523
|
+
|
|
524
|
+
console.log(`Matched ${candidates.length} memories.`);
|
|
525
|
+
for (const m of candidates.slice(0, 5)) {
|
|
526
|
+
console.log(` - [${m.branch}] ${m.summary.oneLineSummary.slice(0, 70)}`);
|
|
527
|
+
}
|
|
528
|
+
if (candidates.length > 5) console.log(` ... and ${candidates.length - 5} more`);
|
|
529
|
+
|
|
530
|
+
if (!apply) {
|
|
531
|
+
console.log("\nDry run — re-run with --apply to delete.");
|
|
532
|
+
await store.close();
|
|
533
|
+
await valkeyClient.quit();
|
|
534
|
+
return;
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
const deleted = await store.forgetByScope(scope);
|
|
538
|
+
console.log(`\nDeleted ${deleted} memories.`);
|
|
539
|
+
|
|
540
|
+
await store.close();
|
|
541
|
+
await valkeyClient.quit();
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
// ---------------------------------------------------------------------------
|
|
545
|
+
// setup-index (recovery path: build the MemoryStore episodic vector index)
|
|
546
|
+
// ---------------------------------------------------------------------------
|
|
547
|
+
|
|
548
|
+
async function runSetupIndex() {
|
|
549
|
+
const { getValkeyClient } = await import("./client/valkey.js");
|
|
550
|
+
const { getPluginMemoryStore } = await import("./client/memory-store.js");
|
|
551
|
+
const { createModelClient } = await import("./client/model.js");
|
|
552
|
+
|
|
553
|
+
const client = await getValkeyClient();
|
|
554
|
+
const modelClient = await createModelClient();
|
|
555
|
+
// Record the active provider/dimension so a later provider swap is caught.
|
|
556
|
+
await client.assertEmbedDim(modelClient.embedDim, modelClient.preset.embedModel);
|
|
557
|
+
const store = await getPluginMemoryStore((t) => modelClient.embed(t));
|
|
558
|
+
await store.ensureIndex();
|
|
559
|
+
console.log("Index ready: betterdb:mem:idx");
|
|
560
|
+
|
|
561
|
+
await store.close();
|
|
562
|
+
await client.quit();
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
// ---------------------------------------------------------------------------
|
|
566
|
+
// migrate (legacy betterdb:memory:* -> MemoryStore betterdb:mem:*)
|
|
567
|
+
// ---------------------------------------------------------------------------
|
|
568
|
+
|
|
569
|
+
async function runMigrate(apply: boolean) {
|
|
570
|
+
console.log("BetterDB Memory for Claude Code — Migrate legacy memories\n");
|
|
390
571
|
|
|
391
|
-
const
|
|
392
|
-
|
|
572
|
+
const { getValkeyClient } = await import("./client/valkey.js");
|
|
573
|
+
const { getPluginMemoryStore } = await import("./client/memory-store.js");
|
|
574
|
+
const { createModelClient } = await import("./client/model.js");
|
|
393
575
|
|
|
394
|
-
|
|
576
|
+
const valkeyClient = await getValkeyClient();
|
|
577
|
+
const legacyIds = await valkeyClient.listMemoryIds();
|
|
578
|
+
console.log(`Found ${legacyIds.length} legacy memories under betterdb:memory:*`);
|
|
579
|
+
|
|
580
|
+
if (legacyIds.length === 0) {
|
|
581
|
+
console.log("Nothing to migrate.");
|
|
582
|
+
await valkeyClient.quit();
|
|
583
|
+
return;
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
if (!apply) {
|
|
587
|
+
console.log("\nDry run — re-run with --apply to migrate.");
|
|
588
|
+
console.log("Each legacy memory is re-embedded and written to betterdb:mem:*,");
|
|
589
|
+
console.log("and knowledge entries are re-pointed to the new memory ids.");
|
|
590
|
+
console.log("The legacy index is dropped only after the new count is verified;");
|
|
591
|
+
console.log("legacy hashes are left in place for you to delete once satisfied.");
|
|
592
|
+
await valkeyClient.quit();
|
|
593
|
+
return;
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
const modelClient = await createModelClient();
|
|
597
|
+
const store = await getPluginMemoryStore((t) => modelClient.embed(t));
|
|
598
|
+
await store.ensureIndex();
|
|
599
|
+
|
|
600
|
+
// Baseline so we can verify the store actually grew by the migrated count,
|
|
601
|
+
// not just that its total happens to exceed it (pre-existing memories).
|
|
602
|
+
const beforeCount = (await store.listMemories()).length;
|
|
603
|
+
|
|
604
|
+
let migrated = 0;
|
|
605
|
+
let failed = 0;
|
|
606
|
+
// MemoryStore.remember mints a fresh id, so track legacy -> new so we can
|
|
607
|
+
// re-point knowledge entries that reference the old episodic ids.
|
|
608
|
+
const idMap = new Map<string, string>();
|
|
395
609
|
const projects = new Set<string>();
|
|
396
|
-
for (const id of
|
|
610
|
+
for (const id of legacyIds) {
|
|
397
611
|
const memory = await valkeyClient.getMemory(id);
|
|
398
|
-
if (memory)
|
|
612
|
+
if (!memory) {
|
|
613
|
+
failed++;
|
|
614
|
+
continue;
|
|
615
|
+
}
|
|
616
|
+
try {
|
|
617
|
+
const newId = await store.storeMemory(memory);
|
|
618
|
+
idMap.set(id, newId);
|
|
619
|
+
projects.add(memory.project);
|
|
620
|
+
migrated++;
|
|
621
|
+
if (migrated % 10 === 0) {
|
|
622
|
+
console.log(` Migrated ${migrated}/${legacyIds.length}...`);
|
|
623
|
+
}
|
|
624
|
+
} catch (err) {
|
|
625
|
+
console.error(` Failed to migrate ${id}:`, err instanceof Error ? err.message : String(err));
|
|
626
|
+
failed++;
|
|
627
|
+
}
|
|
399
628
|
}
|
|
400
629
|
|
|
630
|
+
// Re-point distilled knowledge so sourceMemoryIds keep referencing real
|
|
631
|
+
// episodic memories under the new ids. storeKnowledge upserts by
|
|
632
|
+
// project:topic, so re-storing overwrites in place.
|
|
633
|
+
let remappedKnowledge = 0;
|
|
401
634
|
for (const project of projects) {
|
|
402
|
-
|
|
403
|
-
|
|
635
|
+
for (const entry of await valkeyClient.listKnowledge(project)) {
|
|
636
|
+
const remapped = entry.sourceMemoryIds.map((sid) => idMap.get(sid) ?? sid);
|
|
637
|
+
if (remapped.some((sid, i) => sid !== entry.sourceMemoryIds[i])) {
|
|
638
|
+
await valkeyClient.storeKnowledge({ ...entry, sourceMemoryIds: remapped });
|
|
639
|
+
remappedKnowledge++;
|
|
640
|
+
}
|
|
641
|
+
}
|
|
642
|
+
}
|
|
643
|
+
if (remappedKnowledge > 0) {
|
|
644
|
+
console.log(`Re-pointed ${remappedKnowledge} knowledge entries to new memory ids.`);
|
|
404
645
|
}
|
|
405
646
|
|
|
406
|
-
|
|
407
|
-
|
|
647
|
+
// Verify before dropping the legacy index: the store must have grown by the
|
|
648
|
+
// number we successfully migrated (not merely exceed it, which pre-existing
|
|
649
|
+
// memories would satisfy even if rows failed to copy).
|
|
650
|
+
const afterCount = (await store.listMemories()).length;
|
|
651
|
+
const grew = afterCount - beforeCount;
|
|
652
|
+
console.log(`\nMigrated: ${migrated}, failed: ${failed}, store grew by ${grew} (now ${afterCount}).`);
|
|
653
|
+
|
|
654
|
+
if (migrated > 0 && grew >= migrated) {
|
|
655
|
+
await valkeyClient.dropIndex();
|
|
656
|
+
console.log("Verified — dropped the legacy index (betterdb-memory-index).");
|
|
657
|
+
console.log("Legacy hashes (betterdb:memory:*) remain; delete them manually when ready.");
|
|
658
|
+
} else {
|
|
659
|
+
console.log("Count mismatch — left the legacy index in place. Re-run after investigating.");
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
await store.close();
|
|
663
|
+
await valkeyClient.quit();
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
// ---------------------------------------------------------------------------
|
|
667
|
+
// ingest-claude-md (ingest a CLAUDE.md / MEMORY.md file into the store)
|
|
668
|
+
// ---------------------------------------------------------------------------
|
|
669
|
+
|
|
670
|
+
async function runIngestClaudeMd(pathArg?: string) {
|
|
671
|
+
console.log("BetterDB Memory for Claude Code — Ingest markdown memory file\n");
|
|
672
|
+
|
|
673
|
+
const candidates = pathArg
|
|
674
|
+
? [pathArg]
|
|
675
|
+
: [
|
|
676
|
+
join(process.cwd(), "CLAUDE.md"),
|
|
677
|
+
join(process.cwd(), "MEMORY.md"),
|
|
678
|
+
join(HOME, ".claude", "CLAUDE.md"),
|
|
679
|
+
];
|
|
680
|
+
|
|
681
|
+
const filePath = candidates.find((p) => existsSync(p));
|
|
682
|
+
if (!filePath) {
|
|
683
|
+
console.error(`No memory file found. Looked in:\n ${candidates.join("\n ")}`);
|
|
684
|
+
process.exit(1);
|
|
685
|
+
}
|
|
686
|
+
console.log(`Reading ${filePath}`);
|
|
687
|
+
|
|
688
|
+
const content = readFileSync(filePath, "utf-8");
|
|
689
|
+
// Split into paragraph-sized chunks on blank lines so each becomes an
|
|
690
|
+
// independently recallable memory; cap length to keep embeddings sane.
|
|
691
|
+
const MAX_CHUNK = 480;
|
|
692
|
+
const chunks = content
|
|
693
|
+
.split(/\n\s*\n/)
|
|
694
|
+
.map((c) => c.trim())
|
|
695
|
+
.filter((c) => c.length > 0)
|
|
696
|
+
.map((c) => (c.length > MAX_CHUNK ? c.slice(0, MAX_CHUNK) : c));
|
|
697
|
+
|
|
698
|
+
if (chunks.length === 0) {
|
|
699
|
+
console.log("File is empty — nothing to ingest.");
|
|
700
|
+
process.exit(0);
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
const { getValkeyClient } = await import("./client/valkey.js");
|
|
704
|
+
const { getPluginMemoryStore } = await import("./client/memory-store.js");
|
|
705
|
+
const { createModelClient } = await import("./client/model.js");
|
|
706
|
+
const { getCwdProject } = await import("./memory/capture.js");
|
|
707
|
+
const { SessionSummarySchema } = await import("./memory/schema.js");
|
|
708
|
+
|
|
709
|
+
const valkeyClient = await getValkeyClient();
|
|
710
|
+
const modelClient = await createModelClient();
|
|
711
|
+
const store = await getPluginMemoryStore((t) => modelClient.embed(t));
|
|
712
|
+
await store.ensureIndex();
|
|
713
|
+
|
|
714
|
+
const project = getCwdProject();
|
|
715
|
+
const timestamp = new Date().toISOString();
|
|
716
|
+
let stored = 0;
|
|
717
|
+
|
|
718
|
+
for (const chunk of chunks) {
|
|
719
|
+
const summary = SessionSummarySchema.parse({
|
|
720
|
+
decisions: [],
|
|
721
|
+
patterns: [],
|
|
722
|
+
problemsSolved: [],
|
|
723
|
+
openThreads: [],
|
|
724
|
+
filesChanged: [],
|
|
725
|
+
oneLineSummary: chunk,
|
|
726
|
+
});
|
|
727
|
+
await store.storeMemory({
|
|
728
|
+
memoryId: crypto.randomUUID(),
|
|
729
|
+
project,
|
|
730
|
+
branch: "claude-md",
|
|
731
|
+
timestamp,
|
|
732
|
+
summary,
|
|
733
|
+
importanceScore: 0.6,
|
|
734
|
+
accessCount: 0,
|
|
735
|
+
lastAccessed: timestamp,
|
|
736
|
+
});
|
|
737
|
+
stored++;
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
console.log(`\nIngested ${stored} chunks from ${filePath} into project "${project}".`);
|
|
741
|
+
await store.close();
|
|
408
742
|
await valkeyClient.quit();
|
|
409
743
|
}
|
|
410
744
|
|
|
@@ -417,6 +751,29 @@ function commandExists(cmd: string): boolean {
|
|
|
417
751
|
return result.exitCode === 0;
|
|
418
752
|
}
|
|
419
753
|
|
|
754
|
+
/**
|
|
755
|
+
* Merge BetterDB hooks into existing settings hooks without clobbering
|
|
756
|
+
* entries from other plugins or user-defined hooks. For each event,
|
|
757
|
+
* removes any previous BetterDB entries (matched by BIN_DIR path)
|
|
758
|
+
* then appends the new ones.
|
|
759
|
+
*/
|
|
760
|
+
function mergeHooks(
|
|
761
|
+
existing: Record<string, unknown[]>,
|
|
762
|
+
ours: Record<string, unknown[]>,
|
|
763
|
+
): Record<string, unknown[]> {
|
|
764
|
+
const merged = { ...existing };
|
|
765
|
+
for (const [event, entries] of Object.entries(ours)) {
|
|
766
|
+
const prev = Array.isArray(merged[event]) ? merged[event] : [];
|
|
767
|
+
// Filter out previous BetterDB entries (contain our BIN_DIR or betterdb path)
|
|
768
|
+
const filtered = prev.filter((entry) => {
|
|
769
|
+
const json = JSON.stringify(entry);
|
|
770
|
+
return !json.includes(BIN_DIR) && !json.includes("betterdb");
|
|
771
|
+
});
|
|
772
|
+
merged[event] = [...filtered, ...entries];
|
|
773
|
+
}
|
|
774
|
+
return merged;
|
|
775
|
+
}
|
|
776
|
+
|
|
420
777
|
function readConfigValue(key: string): string | undefined {
|
|
421
778
|
if (!existsSync(CONFIG_PATH)) return undefined;
|
|
422
779
|
try {
|
|
@@ -425,6 +782,7 @@ function readConfigValue(key: string): string | undefined {
|
|
|
425
782
|
const val = (data as Record<string, unknown>)[key];
|
|
426
783
|
if (typeof val === "string") return val;
|
|
427
784
|
if (typeof val === "number") return String(val);
|
|
785
|
+
if (typeof val === "boolean") return String(val);
|
|
428
786
|
return undefined;
|
|
429
787
|
} catch {
|
|
430
788
|
return undefined;
|