@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 CHANGED
@@ -16,14 +16,31 @@ relevant history at the start of each new session.
16
16
  ### Install
17
17
 
18
18
  ```bash
19
+ # 1. Copy .env.example and fill in your settings
20
+ cp .env.example .env
21
+
22
+ # 2. Install
19
23
  bunx @betterdb/memory install
20
24
  ```
21
25
 
22
- This will:
26
+ The install will:
23
27
  1. Compile native hook binaries to `~/.betterdb/bin/`
24
28
  2. Register 4 lifecycle hooks with Claude Code
25
29
  3. Register the MCP server for mid-conversation tools
26
30
  4. Create the Valkey search index
31
+ 5. Save your `.env` values to `~/.betterdb/memory.json` for runtime use
32
+
33
+ ### Don't have Valkey?
34
+
35
+ The setup skill will offer to spin one up in Docker for you. Or run it manually:
36
+
37
+ ```bash
38
+ # Via CLI
39
+ bunx @betterdb/memory docker-valkey
40
+
41
+ # Or directly with Docker
42
+ docker run -d --name betterdb-valkey -p 6379:6379 -v betterdb-valkey-data:/data valkey/valkey-search:8 valkey-server --save 60 1
43
+ ```
27
44
 
28
45
  ### How It Works
29
46
 
@@ -37,7 +54,7 @@ This will:
37
54
  ### MCP Tools
38
55
 
39
56
  Claude can use these mid-conversation:
40
- - `search_context` — Semantic search over past sessions
57
+ - `search_context` — Semantic search over past sessions. Escalates project+branch → project → cross-project, and takes an optional `tags` filter (`decision`, `pattern`, `problem`, `open-thread`)
41
58
  - `store_insight` — Save a decision, pattern, or warning
42
59
  - `list_open_threads` — Show unresolved items
43
60
  - `forget` — Delete a specific memory
@@ -45,24 +62,98 @@ Claude can use these mid-conversation:
45
62
  ### CLI Commands
46
63
 
47
64
  ```bash
48
- bunx @betterdb/memory install # Set up hooks + MCP server
49
- bunx @betterdb/memory status # Check health
50
- bunx @betterdb/memory uninstall # Remove everything
51
- bunx @betterdb/memory maintain # Run aging/compression manually
65
+ bunx @betterdb/memory install # Set up hooks + MCP server
66
+ bunx @betterdb/memory status # Check health + recall scoring config
67
+ bunx @betterdb/memory uninstall # Remove everything
68
+ bunx @betterdb/memory maintain # Run aging/compression manually
69
+ bunx @betterdb/memory forget # Bulk-delete by scope (dry run; --apply to delete)
70
+ # --project <name> | --all-projects --branch <b> --tags <a,b>
71
+ bunx @betterdb/memory docker-valkey # Manage Docker Valkey container
52
72
  ```
53
73
 
54
74
  ### Configuration
55
75
 
56
- Via environment variables or `~/.betterdb/memory.json`:
76
+ Copy `.env.example` to `.env` and fill in your values before running `bunx @betterdb/memory install`. They get saved to `~/.betterdb/memory.json` and used by the compiled binaries at runtime.
77
+
78
+ #### Core
57
79
 
58
80
  | Variable | Default | Description |
59
81
  |----------|---------|-------------|
60
82
  | `BETTERDB_VALKEY_URL` | `redis://localhost:6379` | Valkey connection URL |
61
- | `BETTERDB_EMBED_MODEL` | auto-detect | Embedding provider |
62
- | `BETTERDB_SUMMARIZE_MODEL` | auto-detect | Summarization provider |
83
+ | `BETTERDB_VALKEY_INDEX_NAME` | `betterdb-memory-index` | Valkey search index name |
63
84
  | `BETTERDB_EMBED_DIM` | `1024` | Embedding dimensions |
64
- | `BETTERDB_MAX_CONTEXT_MEMORIES` | `5` | Memories injected per session |
85
+ | `BETTERDB_MAX_CONTEXT_MEMORIES` | `5` | Max memories injected per session (after gating) |
65
86
  | `BETTERDB_CONTEXT_FILE` | `.betterdb_context.md` | Context injection file |
87
+ | `BETTERDB_ALLOW_REMOTE_FALLBACK` | `true` | Fall back to remote APIs if local models unavailable |
88
+
89
+ #### Recall Gating
90
+
91
+ Recall over-fetches a candidate pool, gates it by relevance, and escalates on a
92
+ miss (project+branch → project → cross-project). Memories are stored with their
93
+ git branch as a native thread scope and content-type tags, so recall can narrow
94
+ to the current branch first and filter by type. `search_context` returns nothing
95
+ only when nothing clears the bar — so a miss is honest, not a silent drop.
96
+
97
+ The gate is **relative**, not an absolute similarity threshold: embed models
98
+ compress cosine similarity into different, narrow bands (mxbai-embed-large packs
99
+ everything into ~0.7–0.88), so a fixed threshold doesn't transfer across models.
100
+ Instead, `floor` drops genuine noise, and hits within `margin` of the top match
101
+ are kept; confidence comes from the scale-independent top-vs-next gap.
102
+
103
+ | Variable | Default | Description |
104
+ |----------|---------|-------------|
105
+ | `BETTERDB_RECALL_FLOOR` | `0.5` | Similarity floor — drops noise and loosens the store's own distance gate |
106
+ | `BETTERDB_RECALL_MARGIN` | `0.05` | Keep hits within this similarity of the top match |
107
+ | `BETTERDB_RECALL_SEPARATION` | `0.04` | Top-vs-next gap above which a match is "high" confidence |
108
+ | `BETTERDB_RECALL_POOL_K` | `10` | Rung-1 over-fetch pool (project) |
109
+ | `BETTERDB_RECALL_POOL_K_WIDE` | `20` | Rung-2/3 over-fetch pool (wider / cross-project) |
110
+ | `BETTERDB_ALLOW_CROSS_PROJECT` | `true` | Allow escalation / `scope="all"` to search across projects |
111
+
112
+ Ranking within the gated pool uses a composite score (similarity + recency +
113
+ importance), owned by `@betterdb/agent-memory`. Recency is the system's single
114
+ time-decay — a half-life applied at query time, not a stored per-memory aging
115
+ pass. These knobs tune it; defaults match the store's.
116
+
117
+ | Variable | Default | Description |
118
+ |----------|---------|-------------|
119
+ | `BETTERDB_RECALL_HALF_LIFE_DAYS` | `7` | Age at which a memory's recency term halves |
120
+ | `BETTERDB_RECALL_WEIGHT_SIMILARITY` | `0.6` | Weight of semantic similarity in the composite score |
121
+ | `BETTERDB_RECALL_WEIGHT_RECENCY` | `0.25` | Weight of recency |
122
+ | `BETTERDB_RECALL_WEIGHT_IMPORTANCE` | `0.15` | Weight of stored importance |
123
+
124
+ #### Model Providers
125
+
126
+ | Variable | Default | Description |
127
+ |----------|---------|-------------|
128
+ | `BETTERDB_EMBED_PROVIDER` | auto-detect | Force embed provider: `local`, `ollama`, `voyage`, `openai`, `groq`, `together` |
129
+ | `BETTERDB_SUMMARIZE_PROVIDER` | auto-detect | Force summarize provider: `ollama`, `anthropic`, `openai`, `groq`, `together` |
130
+ | `BETTERDB_EMBED_MODEL` | `mxbai-embed-large` | Ollama embedding model name |
131
+ | `BETTERDB_SUMMARIZE_MODEL` | `mistral:7b` | Ollama summarization model name |
132
+ | `BETTERDB_OLLAMA_URL` | `http://localhost:11434` | Ollama API URL |
133
+
134
+ #### Embeddings work with zero config
135
+
136
+ If no embedding provider is detected (no Ollama models, no API keys), BetterDB falls back to **on-device embeddings** via `@xenova/transformers` (`all-MiniLM-L6-v2`, 384-dim, Apache-2.0). No API key, no running service — the model weights download once on first use and are cached thereafter. Auto-detected providers (Ollama, then API keys) take priority when available.
137
+
138
+ #### API Keys
139
+
140
+ Embeddings always work (on-device fallback above). A summarization provider is still required — Ollama is free and local; the others require API keys.
141
+
142
+ | Variable | Provider | Used for |
143
+ |----------|----------|----------|
144
+ | `ANTHROPIC_API_KEY` | [Anthropic](https://console.anthropic.com/) | Summarization only (no embeddings) |
145
+ | `VOYAGE_API_KEY` | [Voyage AI](https://www.voyageai.com/) | Embeddings only |
146
+ | `OPENAI_API_KEY` | [OpenAI](https://platform.openai.com/) | Embeddings + summarization |
147
+ | `GROQ_API_KEY` | [Groq](https://console.groq.com/) | Embeddings + summarization |
148
+ | `TOGETHER_API_KEY` | [Together AI](https://www.together.ai/) | Embeddings + summarization |
149
+
150
+ #### Aging Pipeline
151
+
152
+ | Variable | Default | Description |
153
+ |----------|---------|-------------|
154
+ | `BETTERDB_COMPRESS_THRESHOLD` | `0.3` | Importance threshold for compression |
155
+ | `BETTERDB_DISTILL_MIN_SESSIONS` | `5` | Min sessions before knowledge distillation |
156
+ | `BETTERDB_AGING_INTERVAL_HOURS` | `6` | Hours between automatic aging runs |
66
157
 
67
158
  ## License
68
159
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@betterdb/memory",
3
- "version": "0.1.2",
3
+ "version": "0.4.0",
4
4
  "description": "BetterDB Memory for Claude Code — Valkey-powered persistent memory across sessions",
5
5
  "license": "MIT",
6
6
  "author": "BetterDB Inc. <hello@betterdb.com>",
@@ -46,9 +46,11 @@
46
46
  "typecheck": "tsc --noEmit"
47
47
  },
48
48
  "dependencies": {
49
+ "@betterdb/agent-memory": "^0.2.1",
49
50
  "iovalkey": "^0.2.1",
50
51
  "ollama": "^0.5.14",
51
52
  "@modelcontextprotocol/sdk": "^1.12.1",
53
+ "@xenova/transformers": "^2.17.2",
52
54
  "zod": "^3.24.4",
53
55
  "zod-to-json-schema": "^3.24.5",
54
56
  "@anthropic-ai/sdk": "latest"
@@ -7,16 +7,19 @@
7
7
  * bun run scripts/aging-worker.ts
8
8
  */
9
9
  import { getValkeyClient } from "../src/client/valkey.js";
10
+ import { getPluginMemoryStore } from "../src/client/memory-store.js";
10
11
  import { createModelClient } from "../src/client/model.js";
11
12
  import { AgingPipeline } from "../src/memory/aging.js";
12
13
 
13
14
  try {
14
15
  const valkeyClient = await getValkeyClient();
15
16
  const modelClient = await createModelClient();
17
+ const store = await getPluginMemoryStore((t) => modelClient.embed(t));
16
18
 
17
- const pipeline = new AgingPipeline(valkeyClient, modelClient);
19
+ const pipeline = new AgingPipeline(valkeyClient, store, modelClient);
18
20
  await pipeline.runFullPipeline();
19
21
 
22
+ await store.close();
20
23
  await valkeyClient.quit();
21
24
  } catch (err) {
22
25
  console.error("[betterdb] Aging worker failed:", err);
@@ -0,0 +1,101 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+
4
+ CONTAINER_NAME="betterdb-valkey"
5
+ VOLUME_NAME="betterdb-valkey-data"
6
+ PORT="${1:-6379}"
7
+ ACTION="${2:-start}"
8
+
9
+ # Check if Docker is available
10
+ if ! command -v docker &>/dev/null; then
11
+ echo "ERROR: Docker is not installed."
12
+ echo "Install Docker: https://docs.docker.com/get-docker/"
13
+ exit 1
14
+ fi
15
+
16
+ case "$ACTION" in
17
+ start)
18
+ # Check if container already exists
19
+ if docker ps -a --format '{{.Names}}' | grep -q "^${CONTAINER_NAME}$"; then
20
+ if docker ps --format '{{.Names}}' | grep -q "^${CONTAINER_NAME}$"; then
21
+ echo "Valkey container '${CONTAINER_NAME}' is already running on port ${PORT}."
22
+ exit 0
23
+ else
24
+ echo "Starting existing Valkey container '${CONTAINER_NAME}'..."
25
+ docker start "${CONTAINER_NAME}"
26
+ exit 0
27
+ fi
28
+ fi
29
+
30
+ # Try valkey-search image first (includes Search module)
31
+ echo "Pulling valkey/valkey-search:8..."
32
+ if docker pull valkey/valkey-search:8 2>/dev/null; then
33
+ IMAGE="valkey/valkey-search:8"
34
+ else
35
+ echo "valkey-search image not available, falling back to valkey/valkey:8-alpine..."
36
+ docker pull valkey/valkey:8-alpine
37
+ IMAGE="valkey/valkey:8-alpine"
38
+ fi
39
+
40
+ echo "Starting Valkey container on port ${PORT}..."
41
+ if ! docker run -d \
42
+ --name "${CONTAINER_NAME}" \
43
+ -p "${PORT}:6379" \
44
+ -v "${VOLUME_NAME}:/data" \
45
+ "${IMAGE}" \
46
+ valkey-server --save 60 1 2>/dev/null; then
47
+ # Port likely in use — retry on 16379
48
+ PORT=16379
49
+ echo "Port conflict. Retrying on port ${PORT}..."
50
+ docker run -d \
51
+ --name "${CONTAINER_NAME}" \
52
+ -p "${PORT}:6379" \
53
+ -v "${VOLUME_NAME}:/data" \
54
+ "${IMAGE}" \
55
+ valkey-server --save 60 1
56
+ fi
57
+
58
+ # Wait for startup
59
+ sleep 2
60
+
61
+ # Verify
62
+ if docker exec "${CONTAINER_NAME}" valkey-cli ping | grep -q PONG; then
63
+ echo "Valkey is running on redis://localhost:${PORT}"
64
+ else
65
+ echo "WARNING: Container started but ping failed. Check: docker logs ${CONTAINER_NAME}"
66
+ exit 1
67
+ fi
68
+ ;;
69
+
70
+ stop)
71
+ if docker ps --format '{{.Names}}' | grep -q "^${CONTAINER_NAME}$"; then
72
+ docker stop "${CONTAINER_NAME}"
73
+ echo "Valkey container stopped."
74
+ else
75
+ echo "Valkey container is not running."
76
+ fi
77
+ ;;
78
+
79
+ status)
80
+ if docker ps --format '{{.Names}}' | grep -q "^${CONTAINER_NAME}$"; then
81
+ RUNNING_PORT=$(docker port "${CONTAINER_NAME}" 6379 2>/dev/null | head -1 | cut -d: -f2)
82
+ echo "Valkey container '${CONTAINER_NAME}' is running on port ${RUNNING_PORT:-unknown}."
83
+ elif docker ps -a --format '{{.Names}}' | grep -q "^${CONTAINER_NAME}$"; then
84
+ echo "Valkey container '${CONTAINER_NAME}' exists but is stopped."
85
+ else
86
+ echo "No Valkey container found."
87
+ fi
88
+ ;;
89
+
90
+ remove)
91
+ docker stop "${CONTAINER_NAME}" 2>/dev/null || true
92
+ docker rm "${CONTAINER_NAME}" 2>/dev/null || true
93
+ echo "Valkey container removed. Volume '${VOLUME_NAME}' preserved."
94
+ echo "To remove data: docker volume rm ${VOLUME_NAME}"
95
+ ;;
96
+
97
+ *)
98
+ echo "Usage: docker-valkey.sh [port] [start|stop|status|remove]"
99
+ exit 1
100
+ ;;
101
+ esac
@@ -0,0 +1,94 @@
1
+ #!/usr/bin/env bun
2
+
3
+ /**
4
+ * Register BetterDB Memory lifecycle hooks in ~/.claude/settings.json.
5
+ *
6
+ * Usage:
7
+ * bun run scripts/register-hooks.ts <plugin-root>
8
+ *
9
+ * <plugin-root> is the absolute path to the plugin directory (where src/hooks/ lives).
10
+ * Hook commands are written with resolved absolute paths — no env vars.
11
+ */
12
+
13
+ import { existsSync, readFileSync, writeFileSync, mkdirSync } from "node:fs";
14
+ import { join, resolve } from "node:path";
15
+
16
+ const pluginRoot = process.argv[2];
17
+ if (!pluginRoot) {
18
+ console.error("Usage: bun run register-hooks.ts <plugin-root>");
19
+ process.exit(1);
20
+ }
21
+
22
+ const resolvedRoot = resolve(pluginRoot);
23
+ const hooksDir = join(resolvedRoot, "src", "hooks");
24
+
25
+ // Verify hook source files exist
26
+ const hookFiles = ["session-start.ts", "pre-tool.ts", "post-tool.ts", "session-end.ts"];
27
+ for (const file of hookFiles) {
28
+ if (!existsSync(join(hooksDir, file))) {
29
+ console.error(`ERROR: Hook source not found: ${join(hooksDir, file)}`);
30
+ process.exit(1);
31
+ }
32
+ }
33
+
34
+ const HOME = process.env["HOME"] ?? process.env["USERPROFILE"] ?? "";
35
+ const claudeDir = join(HOME, ".claude");
36
+ const settingsPath = join(claudeDir, "settings.json");
37
+
38
+ // Ensure ~/.claude/ exists
39
+ mkdirSync(claudeDir, { recursive: true });
40
+
41
+ // Read existing settings (or start fresh)
42
+ let settings: Record<string, unknown> = {};
43
+ if (existsSync(settingsPath)) {
44
+ try {
45
+ settings = JSON.parse(readFileSync(settingsPath, "utf-8"));
46
+ } catch {
47
+ // Corrupted file — start fresh but warn
48
+ console.warn("WARNING: Could not parse ~/.claude/settings.json — existing content will be preserved as backup.");
49
+ const backupPath = settingsPath + ".bak";
50
+ writeFileSync(backupPath, readFileSync(settingsPath));
51
+ console.warn(` Backup saved to ${backupPath}`);
52
+ }
53
+ }
54
+
55
+ function cmd(hookFile: string): string {
56
+ return `bash -c 'bun run "${join(hooksDir, hookFile)}"'`;
57
+ }
58
+
59
+ // Merge hooks — replaces BetterDB entries per event, preserves all others
60
+ const existingHooks = (settings["hooks"] ?? {}) as Record<string, unknown[]>;
61
+ const betterdbHooks: Record<string, unknown[]> = {
62
+ SessionStart: [
63
+ { hooks: [{ type: "command", command: cmd("session-start.ts") }] },
64
+ ],
65
+ PreToolUse: [
66
+ { matcher: "", hooks: [{ type: "command", command: cmd("pre-tool.ts") }] },
67
+ ],
68
+ PostToolUse: [
69
+ { matcher: "", hooks: [{ type: "command", command: cmd("post-tool.ts") }] },
70
+ ],
71
+ Stop: [
72
+ { hooks: [{ type: "command", command: cmd("session-end.ts") }] },
73
+ ],
74
+ };
75
+
76
+ for (const [event, entries] of Object.entries(betterdbHooks)) {
77
+ const prev = Array.isArray(existingHooks[event]) ? existingHooks[event] : [];
78
+ const filtered = prev.filter((entry) => {
79
+ const json = JSON.stringify(entry);
80
+ return !json.includes("betterdb") && !json.includes(hooksDir);
81
+ });
82
+ existingHooks[event] = [...filtered, ...entries];
83
+ }
84
+ settings["hooks"] = existingHooks;
85
+
86
+ writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + "\n");
87
+
88
+ console.log("BetterDB Memory — Hooks registered in ~/.claude/settings.json\n");
89
+ console.log(" SessionStart → session-start.ts");
90
+ console.log(" PreToolUse → pre-tool.ts");
91
+ console.log(" PostToolUse → post-tool.ts");
92
+ console.log(" Stop → session-end.ts");
93
+ console.log(`\n Plugin root: ${resolvedRoot}`);
94
+ console.log("\n Restart Claude Code for hooks to take effect.");
@@ -1,14 +1,21 @@
1
1
  #!/usr/bin/env bun
2
2
  import { getValkeyClient } from "../src/client/valkey.js";
3
+ import { getPluginMemoryStore } from "../src/client/memory-store.js";
3
4
  import { createModelClient } from "../src/client/model.js";
4
- import { config } from "../src/config.js";
5
5
 
6
6
  const client = await getValkeyClient();
7
7
  const modelClient = await createModelClient();
8
8
 
9
- await client.ensureIndex(modelClient.embedDim, modelClient.preset.embedModel);
10
- console.log("Index ready:", config.valkey.indexName);
9
+ // Create the episodic vector index that MemoryStore reads/writes
10
+ // (betterdb:mem:idx) — the same one `install` builds. Record the active
11
+ // provider/dimension first so a later provider swap is caught.
12
+ await client.assertEmbedDim(modelClient.embedDim, modelClient.preset.embedModel);
13
+ const store = await getPluginMemoryStore((t) => modelClient.embed(t));
14
+ await store.ensureIndex();
15
+
16
+ console.log("Index ready: betterdb:mem:idx");
11
17
  console.log("Embedding dimension:", modelClient.embedDim);
12
18
  console.log("Preset:", modelClient.preset.embedModel, "/", modelClient.preset.summarizeModel);
13
19
 
20
+ await store.close();
14
21
  await client.quit();
@@ -0,0 +1,79 @@
1
+ #!/usr/bin/env bun
2
+
3
+ /**
4
+ * Remove BetterDB Memory lifecycle hooks from ~/.claude/settings.json.
5
+ *
6
+ * Only removes hook entries whose commands contain "betterdb-memory" or
7
+ * "betterdb" in the path. Other hooks from other plugins are preserved.
8
+ *
9
+ * Usage:
10
+ * bun run scripts/unregister-hooks.ts
11
+ */
12
+
13
+ import { existsSync, readFileSync, writeFileSync } from "node:fs";
14
+ import { join } from "node:path";
15
+
16
+ const HOME = process.env["HOME"] ?? process.env["USERPROFILE"] ?? "";
17
+ const settingsPath = join(HOME, ".claude", "settings.json");
18
+
19
+ if (!existsSync(settingsPath)) {
20
+ console.log("No ~/.claude/settings.json found — nothing to remove.");
21
+ process.exit(0);
22
+ }
23
+
24
+ let settings: Record<string, unknown>;
25
+ try {
26
+ settings = JSON.parse(readFileSync(settingsPath, "utf-8"));
27
+ } catch {
28
+ console.error("ERROR: Could not parse ~/.claude/settings.json");
29
+ process.exit(1);
30
+ }
31
+
32
+ const hooks = settings["hooks"];
33
+ if (!hooks || typeof hooks !== "object") {
34
+ console.log("No hooks found in ~/.claude/settings.json — nothing to remove.");
35
+ process.exit(0);
36
+ }
37
+
38
+ const hooksObj = hooks as Record<string, unknown[]>;
39
+ const BETTERDB_PATTERN = /betterdb/i;
40
+ let removedCount = 0;
41
+
42
+ for (const [event, entries] of Object.entries(hooksObj)) {
43
+ if (!Array.isArray(entries)) continue;
44
+
45
+ const filtered = entries.filter((entry) => {
46
+ if (typeof entry !== "object" || entry === null) return true;
47
+ const hooksList = (entry as Record<string, unknown>)["hooks"];
48
+ if (!Array.isArray(hooksList)) return true;
49
+
50
+ // Keep this entry if ANY of its hooks are NOT betterdb-related
51
+ const hasBetterdb = hooksList.some((h) => {
52
+ if (typeof h !== "object" || h === null) return false;
53
+ const command = (h as Record<string, unknown>)["command"];
54
+ return typeof command === "string" && BETTERDB_PATTERN.test(command);
55
+ });
56
+
57
+ if (hasBetterdb) removedCount++;
58
+ return !hasBetterdb;
59
+ });
60
+
61
+ if (filtered.length === 0) {
62
+ delete hooksObj[event];
63
+ } else {
64
+ hooksObj[event] = filtered;
65
+ }
66
+ }
67
+
68
+ // If hooks object is now empty, remove it entirely
69
+ if (Object.keys(hooksObj).length === 0) {
70
+ delete settings["hooks"];
71
+ }
72
+
73
+ writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + "\n");
74
+
75
+ if (removedCount > 0) {
76
+ console.log(`BetterDB Memory — Removed ${removedCount} hook(s) from ~/.claude/settings.json`);
77
+ } else {
78
+ console.log("No BetterDB Memory hooks found in ~/.claude/settings.json.");
79
+ }