@betterdb/memory 0.2.0 → 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.
@@ -31,6 +31,15 @@ export class ValkeyClient {
31
31
  this.client = client;
32
32
  }
33
33
 
34
+ /**
35
+ * The underlying iovalkey connection. Exposed so the episodic-vector path
36
+ * (PluginMemoryStore) can share this single connection instead of opening a
37
+ * second one — its `.call()` satisfies MemoryStoreClient.
38
+ */
39
+ get redis(): Redis {
40
+ return this.client;
41
+ }
42
+
34
43
  // --- Index Management ---
35
44
 
36
45
  async assertEmbedDim(expectedDim: number, providerLabel?: string): Promise<void> {
package/src/config.ts CHANGED
@@ -46,16 +46,39 @@ export const config = {
46
46
  },
47
47
  memory: {
48
48
  maxContextMemories: Number(env("BETTERDB_MAX_CONTEXT_MEMORIES") ?? 5),
49
- decayRate: Number(env("BETTERDB_DECAY_RATE") ?? 0.95),
50
49
  compressThreshold: Number(env("BETTERDB_COMPRESS_THRESHOLD") ?? 0.3),
51
50
  distillMinSessions: Number(env("BETTERDB_DISTILL_MIN_SESSIONS") ?? 5),
52
51
  contextFile: env("BETTERDB_CONTEXT_FILE") ?? ".betterdb_context.md",
53
52
  agingIntervalHours: Number(env("BETTERDB_AGING_INTERVAL_HOURS") ?? 6),
54
53
  },
54
+ recall: {
55
+ // Relative gate — model-agnostic (embed models compress cosine similarity
56
+ // into different bands, so absolute thresholds don't transfer). `floor`
57
+ // drops genuine noise and loosens the store's own distance gate; `margin`
58
+ // keeps hits within that similarity of the top match; `separation` is the
59
+ // top-vs-next gap above which a result is "high" confidence.
60
+ floor: Number(env("BETTERDB_RECALL_FLOOR") ?? 0.5),
61
+ margin: Number(env("BETTERDB_RECALL_MARGIN") ?? 0.05),
62
+ separation: Number(env("BETTERDB_RECALL_SEPARATION") ?? 0.04),
63
+ // Over-fetch pool sizes: rung-1 (project) and rung-2/3 (wider / cross).
64
+ poolK: Number(env("BETTERDB_RECALL_POOL_K") ?? 10),
65
+ poolKWide: Number(env("BETTERDB_RECALL_POOL_K_WIDE") ?? 20),
66
+ // Allow the ladder / search_context to fall back to cross-project scope.
67
+ allowCrossProject: env("BETTERDB_ALLOW_CROSS_PROJECT") !== "false",
68
+ // Composite recall scoring, owned by @betterdb/agent-memory: a weighted
69
+ // blend of semantic similarity, recency (half-life decay), and importance.
70
+ // Recency is the ONE time-decay in the system — it replaces the old, unused
71
+ // per-day `decayRate`. `halfLifeDays` is the age at which a memory's recency
72
+ // term halves; weights (defaults match the store's) blend the three terms.
73
+ halfLifeDays: Number(env("BETTERDB_RECALL_HALF_LIFE_DAYS") ?? 7),
74
+ weightSimilarity: Number(env("BETTERDB_RECALL_WEIGHT_SIMILARITY") ?? 0.6),
75
+ weightRecency: Number(env("BETTERDB_RECALL_WEIGHT_RECENCY") ?? 0.25),
76
+ weightImportance: Number(env("BETTERDB_RECALL_WEIGHT_IMPORTANCE") ?? 0.15),
77
+ },
55
78
  allowRemoteFallback: env("BETTERDB_ALLOW_REMOTE_FALLBACK") !== "false",
56
79
  providers: {
57
80
  embedProvider: env("BETTERDB_EMBED_PROVIDER") as
58
- | "ollama" | "openai" | "voyage" | "groq" | "together"
81
+ | "local" | "ollama" | "openai" | "voyage" | "groq" | "together"
59
82
  | undefined,
60
83
  summarizeProvider: env("BETTERDB_SUMMARIZE_PROVIDER") as
61
84
  | "ollama" | "openai" | "anthropic" | "groq" | "together"
@@ -1,5 +1,6 @@
1
1
  import { readRawPayload, runHook } from "./_utils.js";
2
- import { getValkeyClient } from "../client/valkey.js";
2
+ import { getPluginMemoryStore } from "../client/memory-store.js";
3
+ import { getCwdProject } from "../memory/capture.js";
3
4
  import { config, isConfigured } from "../config.js";
4
5
 
5
6
  /**
@@ -23,21 +24,20 @@ runHook(async () => {
23
24
 
24
25
  if (!filePath) return;
25
26
 
26
- let valkeyClient;
27
+ let store;
27
28
  try {
28
- valkeyClient = await getValkeyClient();
29
+ store = await getPluginMemoryStore();
29
30
  } catch {
30
31
  return; // Valkey unavailable — skip silently
31
32
  }
32
33
 
33
- // Scan for memories that reference this file
34
- const memoryIds = await valkeyClient.listMemoryIds();
34
+ // Scan the current project's recent memories for ones that reference this
35
+ // file. Scope to the project and cap at 50 so this stays cheap on every tool
36
+ // call instead of materializing the whole store.
37
+ const memories = await store.listMemories(getCwdProject(), undefined, 50);
35
38
  const relevantNotes: string[] = [];
36
39
 
37
- for (const id of memoryIds.slice(0, 50)) {
38
- const memory = await valkeyClient.getMemory(id);
39
- if (!memory) continue;
40
-
40
+ for (const memory of memories) {
41
41
  if (memory.summary.filesChanged.some((f) => f.includes(filePath) || filePath.includes(f))) {
42
42
  relevantNotes.push(
43
43
  `- ${memory.summary.oneLineSummary} (${memory.timestamp.split("T")[0]})`,
@@ -56,5 +56,5 @@ runHook(async () => {
56
56
  await Bun.write(config.memory.contextFile, existing + note);
57
57
  }
58
58
 
59
- await valkeyClient.quit();
59
+ await store.close();
60
60
  });
@@ -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,
@@ -110,7 +111,6 @@ runHook(async () => {
110
111
 
111
112
  const summary = await modelClient.summarize(transcript);
112
113
  const importance = computeInitialImportance(summary);
113
- const embedding = await modelClient.embed(summary.oneLineSummary);
114
114
 
115
115
  const memory: EpisodicMemory = {
116
116
  memoryId: crypto.randomUUID(),
@@ -123,7 +123,9 @@ runHook(async () => {
123
123
  lastAccessed: new Date().toISOString(),
124
124
  };
125
125
 
126
- await valkeyClient.storeMemory(memory, embedding);
126
+ const store = await getPluginMemoryStore((t) => modelClient.embed(t));
127
+ await store.storeMemory(memory);
128
+ await store.close();
127
129
  await valkeyClient.quit();
128
130
  await cleanup(eventFilePath);
129
131
  });
@@ -1,8 +1,9 @@
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";
4
+ import { SessionCapture, getGitBranch } from "../memory/capture.js";
5
+ import { formatForInjection } from "../memory/retrieval.js";
6
+ import { escalatingRecall } from "../memory/recall.js";
6
7
  import { config, isConfigured } from "../config.js";
7
8
 
8
9
  /**
@@ -28,21 +29,32 @@ runHook(async () => {
28
29
  process.chdir(cwd);
29
30
  }
30
31
 
31
- let valkeyClient;
32
+ const modelClient = await createModelClient();
33
+
34
+ let store;
32
35
  try {
33
- valkeyClient = await getValkeyClient();
36
+ store = await getPluginMemoryStore((t) => modelClient.embed(t));
34
37
  } catch {
35
38
  return; // Valkey unreachable — skip silently
36
39
  }
37
40
 
38
- const modelClient = await createModelClient();
39
-
40
41
  const capture = new SessionCapture();
41
42
  const queryContext = await capture.getQueryContext();
42
43
 
43
- const retriever = new MemoryRetriever(valkeyClient, modelClient);
44
44
  const project = queryContext.split("\n")[0]?.replace("Project: ", "") ?? "unknown";
45
- 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);
46
58
 
47
59
  if (memories.length > 0) {
48
60
  const formatted = formatForInjection(memories);
@@ -52,5 +64,5 @@ runHook(async () => {
52
64
  process.stdout.write(formatted);
53
65
  }
54
66
 
55
- await valkeyClient.quit();
67
+ await store.close();
56
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,12 +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
- docker-valkey Manage Docker Valkey container [start|stop|status|remove]
44
- 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
45
52
 
46
53
  Environment:
47
54
  BETTERDB_VALKEY_URL Valkey connection (default: redis://localhost:6379)
@@ -64,6 +71,18 @@ switch (command) {
64
71
  case "maintain":
65
72
  await runMaintain();
66
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;
67
86
  case "docker-valkey": {
68
87
  const action = process.argv[3] ?? "start";
69
88
  const port = process.argv[4] ?? "6379";
@@ -215,10 +234,16 @@ async function runInstall() {
215
234
  console.log("\nSetting up Valkey index...");
216
235
  try {
217
236
  const { getValkeyClient } = await import("./client/valkey.js");
218
- 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");
219
239
  const client = await getValkeyClient();
220
- 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();
221
245
  console.log(" Valkey index ready");
246
+ await store.close();
222
247
  await client.quit();
223
248
  } catch (err) {
224
249
  console.log(` WARNING: Index setup failed (${err instanceof Error ? err.message : String(err)})`);
@@ -331,9 +356,19 @@ async function runStatus() {
331
356
  try {
332
357
  const { config } = await import("./config.js");
333
358
  const { getValkeyClient } = await import("./client/valkey.js");
359
+ const { getPluginMemoryStore } = await import("./client/memory-store.js");
334
360
  const client = await getValkeyClient();
335
- const memoryIds = await client.listMemoryIds();
336
- 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();
337
372
  await client.quit();
338
373
  } catch (err) {
339
374
  console.log(`FAILED (${err instanceof Error ? err.message : String(err)})`);
@@ -418,30 +453,292 @@ async function runMaintain() {
418
453
  console.log("BetterDB Memory for Claude Code — Maintenance\n");
419
454
 
420
455
  const { getValkeyClient } = await import("./client/valkey.js");
456
+ const { getPluginMemoryStore } = await import("./client/memory-store.js");
421
457
  const { createModelClient } = await import("./client/model.js");
422
458
  const { AgingPipeline } = await import("./memory/aging.js");
423
459
 
424
460
  const valkeyClient = await getValkeyClient();
425
461
  const modelClient = await createModelClient();
426
- 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");
571
+
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");
575
+
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
+ }
427
585
 
428
- const memoryIds = await valkeyClient.listMemoryIds();
429
- console.log(`Total memories: ${memoryIds.length}`);
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
+ }
430
595
 
431
- // Group by project
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>();
432
609
  const projects = new Set<string>();
433
- for (const id of memoryIds) {
610
+ for (const id of legacyIds) {
434
611
  const memory = await valkeyClient.getMemory(id);
435
- 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
+ }
436
628
  }
437
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;
438
634
  for (const project of projects) {
439
- console.log(`\nRunning decay for project: ${project}`);
440
- 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.`);
441
645
  }
442
646
 
443
- await valkeyClient.setLastAgingRun(new Date());
444
- 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();
445
742
  await valkeyClient.quit();
446
743
  }
447
744