@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 +101 -10
- package/package.json +3 -1
- package/scripts/aging-worker.ts +4 -1
- package/scripts/docker-valkey.sh +101 -0
- package/scripts/register-hooks.ts +94 -0
- package/scripts/setup-index.ts +10 -3
- package/scripts/unregister-hooks.ts +79 -0
- package/src/client/memory-store.ts +406 -0
- package/src/client/model.ts +10 -10
- package/src/client/providers/local.ts +58 -0
- package/src/client/valkey.ts +9 -0
- package/src/config.ts +38 -6
- package/src/hooks/post-tool.ts +2 -0
- package/src/hooks/pre-tool.ts +12 -11
- package/src/hooks/session-end.ts +14 -4
- package/src/hooks/session-start.ts +33 -8
- package/src/index.ts +379 -21
- package/src/mcp/server.ts +82 -42
- package/src/memory/aging.ts +78 -196
- package/src/memory/recall.ts +169 -0
- package/src/memory/retrieval.ts +73 -70
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
|
|
|
@@ -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
|
|
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 + 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
|
-
|
|
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
|
-
| `
|
|
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` |
|
|
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.
|
|
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"
|
package/scripts/aging-worker.ts
CHANGED
|
@@ -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.");
|
package/scripts/setup-index.ts
CHANGED
|
@@ -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
|
-
|
|
10
|
-
|
|
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
|
+
}
|