@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.
@@ -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
- const valkeyClient = await getValkeyClient();
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 valkeyClient.storeMemory(memory, embedding);
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 { getValkeyClient } from "../client/valkey.js";
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 { MemoryRetriever, formatForInjection } from "../memory/retrieval.js";
6
- import { config } from "../config.js";
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 memories = await retriever.retrieve(queryContext, project);
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 valkeyClient.quit();
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.1.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 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/compression pipeline manually
43
- version Print version
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 embedDim = Number(Bun.env["BETTERDB_EMBED_DIM"] ?? readConfigValue("BETTERDB_EMBED_DIM") ?? "1024");
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 client.ensureIndex(embedDim);
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 memoryIds = await client.listMemoryIds();
323
- console.log(`OK (${memoryIds.length} memories, ${config.valkey.url})`);
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 pipeline = new AgingPipeline(valkeyClient, modelClient);
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 memoryIds = await valkeyClient.listMemoryIds();
392
- console.log(`Total memories: ${memoryIds.length}`);
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
- // Group by project
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 memoryIds) {
610
+ for (const id of legacyIds) {
397
611
  const memory = await valkeyClient.getMemory(id);
398
- if (memory) projects.add(memory.project);
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
- console.log(`\nRunning decay for project: ${project}`);
403
- await pipeline.runDecay(project);
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
- await valkeyClient.setLastAgingRun(new Date());
407
- console.log("\nAging pipeline complete.");
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;