@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 +59 -8
- package/package.json +1 -1
- package/scripts/docker-valkey.sh +101 -0
- package/scripts/register-hooks.ts +94 -0
- package/scripts/unregister-hooks.ts +79 -0
- package/src/config.ts +13 -4
- package/src/hooks/post-tool.ts +2 -0
- package/src/hooks/pre-tool.ts +2 -1
- package/src/hooks/session-end.ts +10 -2
- package/src/hooks/session-start.ts +15 -2
- package/src/index.ts +67 -6
- package/src/mcp/server.ts +21 -1
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
|
-
|
|
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
|
|
49
|
-
bunx @betterdb/memory status
|
|
50
|
-
bunx @betterdb/memory uninstall
|
|
51
|
-
bunx @betterdb/memory maintain
|
|
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
|
-
|
|
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
|
-
| `
|
|
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
|
@@ -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
|
-
|
|
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(
|
|
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
|
+
}
|
package/src/hooks/post-tool.ts
CHANGED
|
@@ -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";
|
package/src/hooks/pre-tool.ts
CHANGED
|
@@ -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
|
|
package/src/hooks/session-end.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
|
40
|
-
uninstall
|
|
41
|
-
status
|
|
42
|
-
maintain
|
|
43
|
-
|
|
39
|
+
install Compile binaries, register hooks + MCP server
|
|
40
|
+
uninstall Remove hooks, MCP server, and compiled binaries
|
|
41
|
+
status Check health of Valkey and model providers
|
|
42
|
+
maintain Run aging/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.
|
|
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) {
|