@betterdb/memory 0.1.2 → 0.2.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
 
@@ -45,24 +62,58 @@ 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
67
+ bunx @betterdb/memory uninstall # Remove everything
68
+ bunx @betterdb/memory maintain # Run aging/compression manually
69
+ bunx @betterdb/memory docker-valkey # Manage Docker Valkey container
52
70
  ```
53
71
 
54
72
  ### Configuration
55
73
 
56
- Via environment variables or `~/.betterdb/memory.json`:
74
+ 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.
75
+
76
+ #### Core
57
77
 
58
78
  | Variable | Default | Description |
59
79
  |----------|---------|-------------|
60
80
  | `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 |
81
+ | `BETTERDB_VALKEY_INDEX_NAME` | `betterdb-memory-index` | Valkey search index name |
63
82
  | `BETTERDB_EMBED_DIM` | `1024` | Embedding dimensions |
64
83
  | `BETTERDB_MAX_CONTEXT_MEMORIES` | `5` | Memories injected per session |
65
84
  | `BETTERDB_CONTEXT_FILE` | `.betterdb_context.md` | Context injection file |
85
+ | `BETTERDB_ALLOW_REMOTE_FALLBACK` | `true` | Fall back to remote APIs if local models unavailable |
86
+
87
+ #### Model Providers
88
+
89
+ | Variable | Default | Description |
90
+ |----------|---------|-------------|
91
+ | `BETTERDB_EMBED_PROVIDER` | auto-detect | Force embed provider: `ollama`, `voyage`, `openai`, `groq`, `together` |
92
+ | `BETTERDB_SUMMARIZE_PROVIDER` | auto-detect | Force summarize provider: `ollama`, `anthropic`, `openai`, `groq`, `together` |
93
+ | `BETTERDB_EMBED_MODEL` | `mxbai-embed-large` | Ollama embedding model name |
94
+ | `BETTERDB_SUMMARIZE_MODEL` | `mistral:7b` | Ollama summarization model name |
95
+ | `BETTERDB_OLLAMA_URL` | `http://localhost:11434` | Ollama API URL |
96
+
97
+ #### API Keys
98
+
99
+ At least one embedding provider and one summarization provider must be available. Ollama is free and local; the others require API keys.
100
+
101
+ | Variable | Provider | Used for |
102
+ |----------|----------|----------|
103
+ | `ANTHROPIC_API_KEY` | [Anthropic](https://console.anthropic.com/) | Summarization only (no embeddings) |
104
+ | `VOYAGE_API_KEY` | [Voyage AI](https://www.voyageai.com/) | Embeddings only |
105
+ | `OPENAI_API_KEY` | [OpenAI](https://platform.openai.com/) | Embeddings + summarization |
106
+ | `GROQ_API_KEY` | [Groq](https://console.groq.com/) | Embeddings + summarization |
107
+ | `TOGETHER_API_KEY` | [Together AI](https://www.together.ai/) | Embeddings + summarization |
108
+
109
+ #### Aging Pipeline
110
+
111
+ | Variable | Default | Description |
112
+ |----------|---------|-------------|
113
+ | `BETTERDB_DECAY_RATE` | `0.95` | Memory importance decay per day |
114
+ | `BETTERDB_COMPRESS_THRESHOLD` | `0.3` | Importance threshold for compression |
115
+ | `BETTERDB_DISTILL_MIN_SESSIONS` | `5` | Min sessions before knowledge distillation |
116
+ | `BETTERDB_AGING_INTERVAL_HOURS` | `6` | Hours between automatic aging runs |
66
117
 
67
118
  ## License
68
119
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@betterdb/memory",
3
- "version": "0.1.2",
3
+ "version": "0.2.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>",
@@ -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.");
@@ -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
+ }
package/src/config.ts CHANGED
@@ -1,17 +1,21 @@
1
1
  import { readFileSync, existsSync } from "node:fs";
2
2
  import { join } from "node:path";
3
3
 
4
+ const CONFIG_PATH = join(
5
+ process.env["HOME"] ?? process.env["USERPROFILE"] ?? "",
6
+ ".betterdb",
7
+ "memory.json",
8
+ );
9
+
4
10
  /**
5
11
  * Load saved config from ~/.betterdb/memory.json as fallback for env vars.
6
12
  * This allows compiled binaries (hooks, MCP server) to work without
7
13
  * requiring env vars to be set — config is saved during `install`.
8
14
  */
9
15
  const _fileConfig: Record<string, string> = (() => {
10
- const home = process.env["HOME"] ?? process.env["USERPROFILE"] ?? "";
11
- const p = join(home, ".betterdb", "memory.json");
12
- if (!existsSync(p)) return {};
16
+ if (!existsSync(CONFIG_PATH)) return {};
13
17
  try {
14
- const data: unknown = JSON.parse(readFileSync(p, "utf-8"));
18
+ const data: unknown = JSON.parse(readFileSync(CONFIG_PATH, "utf-8"));
15
19
  if (typeof data !== "object" || data === null) return {};
16
20
  const result: Record<string, string> = {};
17
21
  for (const [k, v] of Object.entries(data as Record<string, unknown>)) {
@@ -65,3 +69,8 @@ export const config = {
65
69
  } as const;
66
70
 
67
71
  export type Config = typeof config;
72
+
73
+ /** Returns true if ~/.betterdb/memory.json exists (i.e. setup has been run). */
74
+ export function isConfigured(): boolean {
75
+ return existsSync(CONFIG_PATH);
76
+ }
@@ -1,6 +1,7 @@
1
1
  import { readRawPayload, runHook } from "./_utils.js";
2
2
  import { appendFile } from "node:fs/promises";
3
3
  import type { SessionEvent } from "../memory/schema.js";
4
+ import { isConfigured } from "../config.js";
4
5
 
5
6
  /**
6
7
  * PostToolUse hook: Records tool call results to a temp JSONL file.
@@ -13,6 +14,7 @@ import type { SessionEvent } from "../memory/schema.js";
13
14
  * The JSONL file is read by session-end.ts to build the session transcript.
14
15
  */
15
16
  runHook(async () => {
17
+ if (!isConfigured()) return;
16
18
  const payload = await readRawPayload();
17
19
  const sessionId = payload["session_id"] as string;
18
20
  const toolName = (payload["tool_name"] as string) ?? "unknown";
@@ -1,6 +1,6 @@
1
1
  import { readRawPayload, runHook } from "./_utils.js";
2
2
  import { getValkeyClient } from "../client/valkey.js";
3
- import { config } from "../config.js";
3
+ import { config, isConfigured } from "../config.js";
4
4
 
5
5
  /**
6
6
  * PreToolUse hook: Checks for file history and appends notes to context.
@@ -11,6 +11,7 @@ import { config } from "../config.js";
11
11
  * - Exit 0 to allow, exit 2 to block
12
12
  */
13
13
  runHook(async () => {
14
+ if (!isConfigured()) return;
14
15
  const payload = await readRawPayload();
15
16
  const toolInput = payload["tool_input"] as Record<string, unknown> | undefined;
16
17
 
@@ -8,7 +8,7 @@ import {
8
8
  getCwdProject,
9
9
  } from "../memory/capture.js";
10
10
  import { SessionEventSchema, type EpisodicMemory } from "../memory/schema.js";
11
- import { config } from "../config.js";
11
+ import { config, isConfigured } from "../config.js";
12
12
  import { unlink } from "node:fs/promises";
13
13
 
14
14
  /**
@@ -25,6 +25,7 @@ import { unlink } from "node:fs/promises";
25
25
  * 3. If model client is unavailable, queue for later processing
26
26
  */
27
27
  runHook(async () => {
28
+ if (!isConfigured()) return;
28
29
  const payload = await readRawPayload();
29
30
  const sessionId = payload["session_id"] as string;
30
31
  const cwd = (payload["cwd"] as string) ?? process.cwd();
@@ -77,7 +78,14 @@ runHook(async () => {
77
78
  transcript.slice(-half);
78
79
  }
79
80
 
80
- const valkeyClient = await getValkeyClient();
81
+ let valkeyClient;
82
+ try {
83
+ valkeyClient = await getValkeyClient();
84
+ } catch {
85
+ await cleanup(eventFilePath);
86
+ return; // Valkey unreachable — skip silently
87
+ }
88
+
81
89
  const project = getCwdProject();
82
90
  const branch = getGitBranch();
83
91
 
@@ -3,7 +3,7 @@ import { getValkeyClient } from "../client/valkey.js";
3
3
  import { createModelClient } from "../client/model.js";
4
4
  import { SessionCapture } from "../memory/capture.js";
5
5
  import { MemoryRetriever, formatForInjection } from "../memory/retrieval.js";
6
- import { config } from "../config.js";
6
+ import { config, isConfigured } from "../config.js";
7
7
 
8
8
  /**
9
9
  * SessionStart hook: Retrieves relevant memories and injects context.
@@ -14,6 +14,13 @@ import { config } from "../config.js";
14
14
  * - Exit 0 for success
15
15
  */
16
16
  runHook(async () => {
17
+ if (!isConfigured()) {
18
+ process.stdout.write(
19
+ "[BetterDB Memory] Not configured yet. Run /betterdb-memory:setup to connect to Valkey.\n",
20
+ );
21
+ return;
22
+ }
23
+
17
24
  const payload = await readRawPayload();
18
25
  const cwd = (payload["cwd"] as string) ?? process.cwd();
19
26
 
@@ -21,7 +28,13 @@ runHook(async () => {
21
28
  process.chdir(cwd);
22
29
  }
23
30
 
24
- const valkeyClient = await getValkeyClient();
31
+ let valkeyClient;
32
+ try {
33
+ valkeyClient = await getValkeyClient();
34
+ } catch {
35
+ return; // Valkey unreachable — skip silently
36
+ }
37
+
25
38
  const modelClient = await createModelClient();
26
39
 
27
40
  const capture = new SessionCapture();
package/src/index.ts CHANGED
@@ -36,11 +36,12 @@ 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/compression pipeline manually
43
+ docker-valkey Manage Docker Valkey container [start|stop|status|remove]
44
+ version Print version
44
45
 
45
46
  Environment:
46
47
  BETTERDB_VALKEY_URL Valkey connection (default: redis://localhost:6379)
@@ -63,6 +64,16 @@ switch (command) {
63
64
  case "maintain":
64
65
  await runMaintain();
65
66
  break;
67
+ case "docker-valkey": {
68
+ const action = process.argv[3] ?? "start";
69
+ const port = process.argv[4] ?? "6379";
70
+ const script = join(PKG_ROOT, "scripts", "docker-valkey.sh");
71
+ const result = Bun.spawnSync(["bash", script, port, action]);
72
+ process.stdout.write(result.stdout);
73
+ process.stderr.write(result.stderr);
74
+ process.exit(result.exitCode);
75
+ break;
76
+ }
66
77
  case "version":
67
78
  case "--version":
68
79
  case "-v":
@@ -175,12 +186,14 @@ async function runInstall() {
175
186
  }
176
187
  }
177
188
 
178
- settings["hooks"] = {
189
+ const existingHooks = (settings["hooks"] ?? {}) as Record<string, unknown[]>;
190
+ const betterdbHooks: Record<string, unknown[]> = {
179
191
  SessionStart: [{ hooks: [{ type: "command", command: join(BIN_DIR, "session-start") }] }],
180
192
  PreToolUse: [{ matcher: "", hooks: [{ type: "command", command: join(BIN_DIR, "pre-tool") }] }],
181
193
  PostToolUse: [{ matcher: "", hooks: [{ type: "command", command: join(BIN_DIR, "post-tool") }] }],
182
194
  Stop: [{ hooks: [{ type: "command", command: join(BIN_DIR, "session-end") }] }],
183
195
  };
196
+ settings["hooks"] = mergeHooks(existingHooks, betterdbHooks);
184
197
 
185
198
  writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + "\n");
186
199
  console.log(" Registered 4 hooks in ~/.claude/settings.json");
@@ -364,6 +377,30 @@ async function runStatus() {
364
377
  console.log("FAILED (could not read settings)");
365
378
  }
366
379
 
380
+ // Check Docker container (only if config has "docker": true)
381
+ const dockerFlag = readConfigValue("docker");
382
+ if (dockerFlag === "true") {
383
+ process.stdout.write("Docker container... ");
384
+ const script = join(PKG_ROOT, "scripts", "docker-valkey.sh");
385
+ if (existsSync(script)) {
386
+ const result = Bun.spawnSync(["bash", script, "6379", "status"]);
387
+ const output = result.stdout.toString().trim();
388
+ if (output.includes("is running")) {
389
+ const portMatch = output.match(/port (\d+)/);
390
+ console.log(`OK (betterdb-valkey, running, port ${portMatch?.[1] ?? "unknown"})`);
391
+ } else if (output.includes("stopped")) {
392
+ console.log(`STOPPED (run: bunx @betterdb/memory docker-valkey)`);
393
+ } else {
394
+ console.log(`NOT FOUND (run: bunx @betterdb/memory docker-valkey)`);
395
+ }
396
+ } else {
397
+ console.log("SCRIPT MISSING (docker-valkey.sh not found)");
398
+ }
399
+ } else {
400
+ process.stdout.write("Docker container... ");
401
+ console.log("NOT USED (Valkey managed externally)");
402
+ }
403
+
367
404
  // Check config file
368
405
  process.stdout.write("Config file... ");
369
406
  if (existsSync(CONFIG_PATH)) {
@@ -417,6 +454,29 @@ function commandExists(cmd: string): boolean {
417
454
  return result.exitCode === 0;
418
455
  }
419
456
 
457
+ /**
458
+ * Merge BetterDB hooks into existing settings hooks without clobbering
459
+ * entries from other plugins or user-defined hooks. For each event,
460
+ * removes any previous BetterDB entries (matched by BIN_DIR path)
461
+ * then appends the new ones.
462
+ */
463
+ function mergeHooks(
464
+ existing: Record<string, unknown[]>,
465
+ ours: Record<string, unknown[]>,
466
+ ): Record<string, unknown[]> {
467
+ const merged = { ...existing };
468
+ for (const [event, entries] of Object.entries(ours)) {
469
+ const prev = Array.isArray(merged[event]) ? merged[event] : [];
470
+ // Filter out previous BetterDB entries (contain our BIN_DIR or betterdb path)
471
+ const filtered = prev.filter((entry) => {
472
+ const json = JSON.stringify(entry);
473
+ return !json.includes(BIN_DIR) && !json.includes("betterdb");
474
+ });
475
+ merged[event] = [...filtered, ...entries];
476
+ }
477
+ return merged;
478
+ }
479
+
420
480
  function readConfigValue(key: string): string | undefined {
421
481
  if (!existsSync(CONFIG_PATH)) return undefined;
422
482
  try {
@@ -425,6 +485,7 @@ function readConfigValue(key: string): string | undefined {
425
485
  const val = (data as Record<string, unknown>)[key];
426
486
  if (typeof val === "string") return val;
427
487
  if (typeof val === "number") return String(val);
488
+ if (typeof val === "boolean") return String(val);
428
489
  return undefined;
429
490
  } catch {
430
491
  return undefined;
package/src/mcp/server.ts CHANGED
@@ -5,11 +5,15 @@ import { getValkeyClient } from "../client/valkey.js";
5
5
  import { createModelClient } from "../client/model.js";
6
6
  import { formatForInjection } from "../memory/retrieval.js";
7
7
  import { getCwdProject } from "../memory/capture.js";
8
+ import { isConfigured } from "../config.js";
8
9
  import type { EpisodicMemory, KnowledgeEntry } from "../memory/schema.js";
9
10
 
11
+ const SETUP_MESSAGE =
12
+ "BetterDB Memory is not configured yet. Run /betterdb-memory:setup to connect to Valkey and create the search index.";
13
+
10
14
  const server = new McpServer({
11
15
  name: "betterdb-memory",
12
- version: "0.1.0",
16
+ version: "0.2.0",
13
17
  });
14
18
 
15
19
  // --- Tool: search_context ---
@@ -22,6 +26,10 @@ server.tool(
22
26
  top_k: z.number().int().min(1).max(20).optional().describe("Max results (default: 5)"),
23
27
  },
24
28
  async ({ query, top_k }) => {
29
+ if (!isConfigured()) {
30
+ return { content: [{ type: "text" as const, text: SETUP_MESSAGE }] };
31
+ }
32
+
25
33
  const valkeyClient = await getValkeyClient();
26
34
  const modelClient = await createModelClient();
27
35
 
@@ -56,6 +64,10 @@ server.tool(
56
64
  project: z.string().optional().describe("Project name (auto-detected if omitted)"),
57
65
  },
58
66
  async ({ content, category, project: projectInput }) => {
67
+ if (!isConfigured()) {
68
+ return { content: [{ type: "text" as const, text: SETUP_MESSAGE }] };
69
+ }
70
+
59
71
  const valkeyClient = await getValkeyClient();
60
72
  const modelClient = await createModelClient();
61
73
  const project = projectInput ?? getCwdProject();
@@ -114,6 +126,10 @@ server.tool(
114
126
  project: z.string().optional().describe("Project name (auto-detected if omitted)"),
115
127
  },
116
128
  async ({ project: projectInput }) => {
129
+ if (!isConfigured()) {
130
+ return { content: [{ type: "text" as const, text: SETUP_MESSAGE }] };
131
+ }
132
+
117
133
  const valkeyClient = await getValkeyClient();
118
134
  const project = projectInput ?? getCwdProject();
119
135
 
@@ -154,6 +170,10 @@ server.tool(
154
170
  confirmed: z.boolean().optional().describe("Set to true to confirm deletion"),
155
171
  },
156
172
  async ({ memory_id, confirmed }) => {
173
+ if (!isConfigured()) {
174
+ return { content: [{ type: "text" as const, text: SETUP_MESSAGE }] };
175
+ }
176
+
157
177
  const valkeyClient = await getValkeyClient();
158
178
 
159
179
  if (!confirmed) {