@betterdb/memory 0.1.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/LICENSE +21 -0
- package/README.md +70 -0
- package/package.json +60 -0
- package/scripts/aging-worker.ts +24 -0
- package/scripts/check-providers.ts +103 -0
- package/scripts/install-hooks.sh +103 -0
- package/scripts/migrate-embeddings.ts +69 -0
- package/scripts/setup-index.ts +14 -0
- package/scripts/validate-pack.sh +67 -0
- package/src/client/model.ts +281 -0
- package/src/client/providers/_prompt.ts +35 -0
- package/src/client/providers/anthropic.ts +70 -0
- package/src/client/providers/groq.ts +102 -0
- package/src/client/providers/ollama.ts +53 -0
- package/src/client/providers/openai.ts +125 -0
- package/src/client/providers/together.ts +94 -0
- package/src/client/providers/voyage.ts +46 -0
- package/src/client/valkey.ts +448 -0
- package/src/config.ts +67 -0
- package/src/hooks/_utils.ts +53 -0
- package/src/hooks/post-tool.ts +46 -0
- package/src/hooks/pre-tool.ts +59 -0
- package/src/hooks/session-end.ts +194 -0
- package/src/hooks/session-start.ts +43 -0
- package/src/index.ts +435 -0
- package/src/mcp/server.ts +201 -0
- package/src/memory/aging.ts +321 -0
- package/src/memory/capture.ts +122 -0
- package/src/memory/retrieval.ts +114 -0
- package/src/memory/schema.ts +111 -0
- package/tsconfig.json +21 -0
package/src/index.ts
ADDED
|
@@ -0,0 +1,435 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* BetterDB Memory for Claude Code — CLI entry point.
|
|
5
|
+
*
|
|
6
|
+
* Usage:
|
|
7
|
+
* betterdb-memory install — Compile binaries, register hooks + MCP server
|
|
8
|
+
* betterdb-memory status — Check health of Valkey + model providers
|
|
9
|
+
* betterdb-memory uninstall — Remove hooks, MCP, and compiled binaries
|
|
10
|
+
* betterdb-memory maintain — Run aging/compression manually
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { existsSync, readFileSync, writeFileSync, mkdirSync, chmodSync, rmSync } from "node:fs";
|
|
14
|
+
import { join, resolve } from "node:path";
|
|
15
|
+
|
|
16
|
+
const VERSION = "0.1.0";
|
|
17
|
+
const HOME = process.env["HOME"] ?? process.env["USERPROFILE"] ?? "";
|
|
18
|
+
const BETTERDB_DIR = join(HOME, ".betterdb");
|
|
19
|
+
const BIN_DIR = join(BETTERDB_DIR, "bin");
|
|
20
|
+
const CONFIG_PATH = join(BETTERDB_DIR, "memory.json");
|
|
21
|
+
const MANIFEST_PATH = join(BETTERDB_DIR, "install-manifest.json");
|
|
22
|
+
const PKG_ROOT = resolve(import.meta.dir, "..");
|
|
23
|
+
|
|
24
|
+
const USAGE = `
|
|
25
|
+
BetterDB Memory for Claude Code v${VERSION}
|
|
26
|
+
|
|
27
|
+
Usage:
|
|
28
|
+
betterdb-memory <command>
|
|
29
|
+
|
|
30
|
+
Commands:
|
|
31
|
+
install Compile binaries, register hooks + MCP server
|
|
32
|
+
uninstall Remove hooks, MCP server, and compiled binaries
|
|
33
|
+
status Check health of Valkey and model providers
|
|
34
|
+
maintain Run aging/compression pipeline manually
|
|
35
|
+
version Print version
|
|
36
|
+
|
|
37
|
+
Environment:
|
|
38
|
+
BETTERDB_VALKEY_URL Valkey connection (default: redis://localhost:6379)
|
|
39
|
+
BETTERDB_EMBED_MODEL Embedding model (auto-detected)
|
|
40
|
+
BETTERDB_EMBED_DIM Embedding dimensions (default: 1024)
|
|
41
|
+
`.trim();
|
|
42
|
+
|
|
43
|
+
const command = process.argv[2];
|
|
44
|
+
|
|
45
|
+
switch (command) {
|
|
46
|
+
case "install":
|
|
47
|
+
await runInstall();
|
|
48
|
+
break;
|
|
49
|
+
case "uninstall":
|
|
50
|
+
await runUninstall();
|
|
51
|
+
break;
|
|
52
|
+
case "status":
|
|
53
|
+
await runStatus();
|
|
54
|
+
break;
|
|
55
|
+
case "maintain":
|
|
56
|
+
await runMaintain();
|
|
57
|
+
break;
|
|
58
|
+
case "version":
|
|
59
|
+
case "--version":
|
|
60
|
+
case "-v":
|
|
61
|
+
console.log(VERSION);
|
|
62
|
+
break;
|
|
63
|
+
case "help":
|
|
64
|
+
case "--help":
|
|
65
|
+
case "-h":
|
|
66
|
+
case undefined:
|
|
67
|
+
console.log(USAGE);
|
|
68
|
+
break;
|
|
69
|
+
default:
|
|
70
|
+
console.error(`Unknown command: ${command}\n`);
|
|
71
|
+
console.log(USAGE);
|
|
72
|
+
process.exit(1);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// ---------------------------------------------------------------------------
|
|
76
|
+
// install
|
|
77
|
+
// ---------------------------------------------------------------------------
|
|
78
|
+
|
|
79
|
+
const BINARIES = [
|
|
80
|
+
{ src: "src/hooks/session-start.ts", out: "session-start" },
|
|
81
|
+
{ src: "src/hooks/session-end.ts", out: "session-end" },
|
|
82
|
+
{ src: "src/hooks/pre-tool.ts", out: "pre-tool" },
|
|
83
|
+
{ src: "src/hooks/post-tool.ts", out: "post-tool" },
|
|
84
|
+
{ src: "src/mcp/server.ts", out: "mcp-server" },
|
|
85
|
+
] as const;
|
|
86
|
+
|
|
87
|
+
async function runInstall() {
|
|
88
|
+
console.log("BetterDB Memory for Claude Code — Install\n");
|
|
89
|
+
|
|
90
|
+
// 1. PREFLIGHT
|
|
91
|
+
if (!commandExists("bun")) {
|
|
92
|
+
console.error("ERROR: 'bun' not found on PATH.");
|
|
93
|
+
console.error("Install Bun: https://bun.sh");
|
|
94
|
+
process.exit(1);
|
|
95
|
+
}
|
|
96
|
+
if (!commandExists("claude")) {
|
|
97
|
+
console.error("ERROR: 'claude' not found on PATH.");
|
|
98
|
+
console.error("Install Claude Code first: https://docs.anthropic.com/en/docs/claude-code");
|
|
99
|
+
process.exit(1);
|
|
100
|
+
}
|
|
101
|
+
console.log("Preflight checks passed.\n");
|
|
102
|
+
|
|
103
|
+
// 2. VALKEY CONNECTION
|
|
104
|
+
const valkeyUrl =
|
|
105
|
+
Bun.env["BETTERDB_VALKEY_URL"] ??
|
|
106
|
+
readConfigValue("BETTERDB_VALKEY_URL") ??
|
|
107
|
+
"redis://localhost:6379";
|
|
108
|
+
|
|
109
|
+
process.stdout.write(`Connecting to Valkey at ${valkeyUrl}... `);
|
|
110
|
+
try {
|
|
111
|
+
const Redis = (await import("iovalkey")).default;
|
|
112
|
+
const client = new Redis(valkeyUrl, { maxRetriesPerRequest: 1, lazyConnect: true });
|
|
113
|
+
await client.connect();
|
|
114
|
+
await client.ping();
|
|
115
|
+
console.log("OK");
|
|
116
|
+
await client.quit();
|
|
117
|
+
} catch (err) {
|
|
118
|
+
console.log("FAILED");
|
|
119
|
+
console.error(`\nCould not connect to Valkey at ${valkeyUrl}`);
|
|
120
|
+
console.error("Make sure Valkey 8+ is running with the Search module loaded.");
|
|
121
|
+
console.error("Quick start: docker run -d -p 6379:6379 valkey/valkey-bundle:8");
|
|
122
|
+
process.exit(1);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// 3. COMPILE NATIVE BINARIES
|
|
126
|
+
mkdirSync(BIN_DIR, { recursive: true });
|
|
127
|
+
|
|
128
|
+
console.log(`\nCompiling ${BINARIES.length} binaries to ${BIN_DIR}/`);
|
|
129
|
+
for (const bin of BINARIES) {
|
|
130
|
+
const srcPath = join(PKG_ROOT, bin.src);
|
|
131
|
+
const outPath = join(BIN_DIR, bin.out);
|
|
132
|
+
process.stdout.write(` ${bin.out}... `);
|
|
133
|
+
|
|
134
|
+
if (!existsSync(srcPath)) {
|
|
135
|
+
console.log(`FAILED (source not found: ${srcPath})`);
|
|
136
|
+
process.exit(1);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const result = Bun.spawnSync([
|
|
140
|
+
"bun", "build", "--compile", "--external", "openai",
|
|
141
|
+
srcPath, "--outfile", outPath,
|
|
142
|
+
]);
|
|
143
|
+
|
|
144
|
+
if (result.exitCode !== 0) {
|
|
145
|
+
console.log("FAILED");
|
|
146
|
+
console.error(result.stderr.toString());
|
|
147
|
+
process.exit(1);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
chmodSync(outPath, 0o755);
|
|
151
|
+
console.log("OK");
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// Verify all binaries exist
|
|
155
|
+
const missing = BINARIES.filter((b) => !existsSync(join(BIN_DIR, b.out)));
|
|
156
|
+
if (missing.length > 0) {
|
|
157
|
+
console.error(`\nERROR: Missing binaries: ${missing.map((b) => b.out).join(", ")}`);
|
|
158
|
+
process.exit(1);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// 4. REGISTER WITH CLAUDE CODE
|
|
162
|
+
console.log("\nRegistering with Claude Code...");
|
|
163
|
+
|
|
164
|
+
// Write hooks to ~/.claude/settings.json
|
|
165
|
+
const claudeDir = join(HOME, ".claude");
|
|
166
|
+
mkdirSync(claudeDir, { recursive: true });
|
|
167
|
+
const settingsPath = join(claudeDir, "settings.json");
|
|
168
|
+
|
|
169
|
+
let settings: Record<string, unknown> = {};
|
|
170
|
+
if (existsSync(settingsPath)) {
|
|
171
|
+
try {
|
|
172
|
+
settings = JSON.parse(readFileSync(settingsPath, "utf-8"));
|
|
173
|
+
} catch {
|
|
174
|
+
// corrupted settings — start fresh
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
settings["hooks"] = {
|
|
179
|
+
SessionStart: [{ hooks: [{ type: "command", command: join(BIN_DIR, "session-start") }] }],
|
|
180
|
+
PreToolUse: [{ matcher: "", hooks: [{ type: "command", command: join(BIN_DIR, "pre-tool") }] }],
|
|
181
|
+
PostToolUse: [{ matcher: "", hooks: [{ type: "command", command: join(BIN_DIR, "post-tool") }] }],
|
|
182
|
+
Stop: [{ hooks: [{ type: "command", command: join(BIN_DIR, "session-end") }] }],
|
|
183
|
+
};
|
|
184
|
+
|
|
185
|
+
writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + "\n");
|
|
186
|
+
console.log(" Registered 4 hooks in ~/.claude/settings.json");
|
|
187
|
+
|
|
188
|
+
// Register MCP server — compiled binary, no env vars needed (reads ~/.betterdb/memory.json)
|
|
189
|
+
const mcpConfig = JSON.stringify({ type: "stdio", command: join(BIN_DIR, "mcp-server") });
|
|
190
|
+
const mcpResult = Bun.spawnSync(["claude", "mcp", "add-json", "betterdb-memory", mcpConfig]);
|
|
191
|
+
if (mcpResult.exitCode === 0) {
|
|
192
|
+
console.log(" Registered MCP server: betterdb-memory");
|
|
193
|
+
} else {
|
|
194
|
+
// Try removing first then re-adding (in case it already exists)
|
|
195
|
+
Bun.spawnSync(["claude", "mcp", "remove", "betterdb-memory"]);
|
|
196
|
+
const retry = Bun.spawnSync(["claude", "mcp", "add-json", "betterdb-memory", mcpConfig]);
|
|
197
|
+
if (retry.exitCode === 0) {
|
|
198
|
+
console.log(" Registered MCP server: betterdb-memory (replaced existing)");
|
|
199
|
+
} else {
|
|
200
|
+
console.log(" WARNING: MCP registration failed — register manually:");
|
|
201
|
+
console.log(` claude mcp add-json betterdb-memory '${mcpConfig}'`);
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// 5. SETUP VALKEY INDEX
|
|
206
|
+
console.log("\nSetting up Valkey index...");
|
|
207
|
+
try {
|
|
208
|
+
const { getValkeyClient } = await import("./client/valkey.js");
|
|
209
|
+
const embedDim = Number(Bun.env["BETTERDB_EMBED_DIM"] ?? readConfigValue("BETTERDB_EMBED_DIM") ?? "1024");
|
|
210
|
+
const client = await getValkeyClient();
|
|
211
|
+
await client.ensureIndex(embedDim);
|
|
212
|
+
console.log(" Valkey index ready");
|
|
213
|
+
await client.quit();
|
|
214
|
+
} catch (err) {
|
|
215
|
+
console.log(` WARNING: Index setup failed (${err instanceof Error ? err.message : String(err)})`);
|
|
216
|
+
console.log(" You can create it later: npx @betterdb/memory setup-index");
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// 6. SAVE CONFIG
|
|
220
|
+
mkdirSync(BETTERDB_DIR, { recursive: true });
|
|
221
|
+
|
|
222
|
+
const configData: Record<string, string | number> = {
|
|
223
|
+
BETTERDB_VALKEY_URL: valkeyUrl,
|
|
224
|
+
BETTERDB_VALKEY_INDEX_NAME: Bun.env["BETTERDB_VALKEY_INDEX_NAME"] ?? "betterdb-memory-index",
|
|
225
|
+
BETTERDB_EMBED_DIM: Number(Bun.env["BETTERDB_EMBED_DIM"] ?? 1024),
|
|
226
|
+
version: VERSION,
|
|
227
|
+
installedAt: new Date().toISOString(),
|
|
228
|
+
};
|
|
229
|
+
|
|
230
|
+
// Carry forward any extra env vars the user has set
|
|
231
|
+
const extraKeys = [
|
|
232
|
+
"BETTERDB_EMBED_MODEL", "BETTERDB_SUMMARIZE_MODEL",
|
|
233
|
+
"BETTERDB_OLLAMA_URL", "BETTERDB_EMBED_PROVIDER", "BETTERDB_SUMMARIZE_PROVIDER",
|
|
234
|
+
"BETTERDB_MAX_CONTEXT_MEMORIES", "BETTERDB_ALLOW_REMOTE_FALLBACK",
|
|
235
|
+
"ANTHROPIC_API_KEY", "VOYAGE_API_KEY", "OPENAI_API_KEY",
|
|
236
|
+
"GROQ_API_KEY", "TOGETHER_API_KEY",
|
|
237
|
+
];
|
|
238
|
+
for (const key of extraKeys) {
|
|
239
|
+
const val = Bun.env[key];
|
|
240
|
+
if (val) configData[key] = val;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
writeFileSync(CONFIG_PATH, JSON.stringify(configData, null, 2) + "\n");
|
|
244
|
+
|
|
245
|
+
const manifest = {
|
|
246
|
+
binaries: BINARIES.map((b) => ({ name: b.out, path: join(BIN_DIR, b.out) })),
|
|
247
|
+
configPath: CONFIG_PATH,
|
|
248
|
+
settingsPath,
|
|
249
|
+
installedAt: new Date().toISOString(),
|
|
250
|
+
version: VERSION,
|
|
251
|
+
};
|
|
252
|
+
writeFileSync(MANIFEST_PATH, JSON.stringify(manifest, null, 2) + "\n");
|
|
253
|
+
|
|
254
|
+
// 7. PRINT SUMMARY
|
|
255
|
+
console.log("\n=== Installation Complete ===\n");
|
|
256
|
+
console.log(` ✅ Compiled ${BINARIES.length} binaries to ${BIN_DIR}/`);
|
|
257
|
+
console.log(" ✅ Registered 4 hooks with Claude Code");
|
|
258
|
+
console.log(" ✅ Registered MCP server: betterdb-memory");
|
|
259
|
+
console.log(" ✅ Valkey index ready");
|
|
260
|
+
console.log(` ✅ Config saved to ${CONFIG_PATH}`);
|
|
261
|
+
console.log("\n 🎉 Start a new Claude Code session to try it.");
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
// ---------------------------------------------------------------------------
|
|
265
|
+
// uninstall
|
|
266
|
+
// ---------------------------------------------------------------------------
|
|
267
|
+
|
|
268
|
+
async function runUninstall() {
|
|
269
|
+
console.log("BetterDB Memory for Claude Code — Uninstall\n");
|
|
270
|
+
|
|
271
|
+
// Remove hooks from ~/.claude/settings.json
|
|
272
|
+
const settingsPath = join(HOME, ".claude", "settings.json");
|
|
273
|
+
if (existsSync(settingsPath)) {
|
|
274
|
+
try {
|
|
275
|
+
const settings = JSON.parse(readFileSync(settingsPath, "utf-8"));
|
|
276
|
+
if (settings.hooks) {
|
|
277
|
+
delete settings.hooks;
|
|
278
|
+
writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + "\n");
|
|
279
|
+
console.log(" Removed hooks from ~/.claude/settings.json");
|
|
280
|
+
} else {
|
|
281
|
+
console.log(" No hooks found in ~/.claude/settings.json");
|
|
282
|
+
}
|
|
283
|
+
} catch {
|
|
284
|
+
console.log(" WARNING: Could not parse ~/.claude/settings.json");
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
// Remove MCP server
|
|
289
|
+
const mcpResult = Bun.spawnSync(["claude", "mcp", "remove", "betterdb-memory"]);
|
|
290
|
+
if (mcpResult.exitCode === 0) {
|
|
291
|
+
console.log(" Removed MCP server: betterdb-memory");
|
|
292
|
+
} else {
|
|
293
|
+
console.log(" MCP server not found or already removed");
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
// Delete compiled binaries
|
|
297
|
+
if (existsSync(BIN_DIR)) {
|
|
298
|
+
rmSync(BIN_DIR, { recursive: true });
|
|
299
|
+
console.log(` Deleted ${BIN_DIR}/`);
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
// Delete manifest (keep config for potential reinstall)
|
|
303
|
+
if (existsSync(MANIFEST_PATH)) {
|
|
304
|
+
rmSync(MANIFEST_PATH);
|
|
305
|
+
console.log(" Deleted install manifest");
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
console.log("\n Uninstall complete.");
|
|
309
|
+
console.log(` Config preserved at ${CONFIG_PATH} — delete ~/.betterdb/ to remove entirely.`);
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
// ---------------------------------------------------------------------------
|
|
313
|
+
// status
|
|
314
|
+
// ---------------------------------------------------------------------------
|
|
315
|
+
|
|
316
|
+
async function runStatus() {
|
|
317
|
+
console.log(`BetterDB Memory for Claude Code v${VERSION}\n`);
|
|
318
|
+
|
|
319
|
+
// Check Valkey connection
|
|
320
|
+
process.stdout.write("Valkey connection... ");
|
|
321
|
+
try {
|
|
322
|
+
const { config } = await import("./config.js");
|
|
323
|
+
const { getValkeyClient } = await import("./client/valkey.js");
|
|
324
|
+
const client = await getValkeyClient();
|
|
325
|
+
const memoryIds = await client.listMemoryIds();
|
|
326
|
+
console.log(`OK (${memoryIds.length} memories, ${config.valkey.url})`);
|
|
327
|
+
await client.quit();
|
|
328
|
+
} catch (err) {
|
|
329
|
+
console.log(`FAILED (${err instanceof Error ? err.message : String(err)})`);
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
// Check model providers
|
|
333
|
+
process.stdout.write("Model providers... ");
|
|
334
|
+
try {
|
|
335
|
+
const { createModelClient } = await import("./client/model.js");
|
|
336
|
+
const modelClient = await createModelClient();
|
|
337
|
+
console.log(
|
|
338
|
+
`OK (embed=${modelClient.preset.embedModel}, summarize=${modelClient.preset.summarizeModel})`,
|
|
339
|
+
);
|
|
340
|
+
} catch (err) {
|
|
341
|
+
console.log(`FAILED (${err instanceof Error ? err.message : String(err)})`);
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
// Check compiled binaries
|
|
345
|
+
process.stdout.write("Compiled binaries... ");
|
|
346
|
+
const present = BINARIES.filter((b) => existsSync(join(BIN_DIR, b.out)));
|
|
347
|
+
if (present.length === BINARIES.length) {
|
|
348
|
+
console.log(`OK (${present.length}/${BINARIES.length} in ${BIN_DIR}/)`);
|
|
349
|
+
} else if (present.length > 0) {
|
|
350
|
+
console.log(`PARTIAL (${present.length}/${BINARIES.length} — reinstall recommended)`);
|
|
351
|
+
} else {
|
|
352
|
+
console.log("NOT INSTALLED (run: npx @betterdb/memory install)");
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
// Check hooks
|
|
356
|
+
process.stdout.write("Claude Code hooks... ");
|
|
357
|
+
try {
|
|
358
|
+
const settingsPath = join(HOME, ".claude", "settings.json");
|
|
359
|
+
if (existsSync(settingsPath)) {
|
|
360
|
+
const settings = JSON.parse(readFileSync(settingsPath, "utf-8"));
|
|
361
|
+
const hookCount = Object.keys(settings.hooks ?? {}).length;
|
|
362
|
+
console.log(hookCount > 0 ? `OK (${hookCount} lifecycle events)` : "NOT CONFIGURED");
|
|
363
|
+
} else {
|
|
364
|
+
console.log("NOT CONFIGURED (no ~/.claude/settings.json)");
|
|
365
|
+
}
|
|
366
|
+
} catch {
|
|
367
|
+
console.log("FAILED (could not read settings)");
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
// Check config file
|
|
371
|
+
process.stdout.write("Config file... ");
|
|
372
|
+
if (existsSync(CONFIG_PATH)) {
|
|
373
|
+
console.log(`OK (${CONFIG_PATH})`);
|
|
374
|
+
} else {
|
|
375
|
+
console.log("NOT FOUND (run: npx @betterdb/memory install)");
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
// ---------------------------------------------------------------------------
|
|
380
|
+
// maintain
|
|
381
|
+
// ---------------------------------------------------------------------------
|
|
382
|
+
|
|
383
|
+
async function runMaintain() {
|
|
384
|
+
console.log("BetterDB Memory for Claude Code — Maintenance\n");
|
|
385
|
+
|
|
386
|
+
const { getValkeyClient } = await import("./client/valkey.js");
|
|
387
|
+
const { createModelClient } = await import("./client/model.js");
|
|
388
|
+
const { AgingPipeline } = await import("./memory/aging.js");
|
|
389
|
+
|
|
390
|
+
const valkeyClient = await getValkeyClient();
|
|
391
|
+
const modelClient = await createModelClient();
|
|
392
|
+
const pipeline = new AgingPipeline(valkeyClient, modelClient);
|
|
393
|
+
|
|
394
|
+
const memoryIds = await valkeyClient.listMemoryIds();
|
|
395
|
+
console.log(`Total memories: ${memoryIds.length}`);
|
|
396
|
+
|
|
397
|
+
// Group by project
|
|
398
|
+
const projects = new Set<string>();
|
|
399
|
+
for (const id of memoryIds) {
|
|
400
|
+
const memory = await valkeyClient.getMemory(id);
|
|
401
|
+
if (memory) projects.add(memory.project);
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
for (const project of projects) {
|
|
405
|
+
console.log(`\nRunning decay for project: ${project}`);
|
|
406
|
+
await pipeline.runDecay(project);
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
await valkeyClient.setLastAgingRun(new Date());
|
|
410
|
+
console.log("\nAging pipeline complete.");
|
|
411
|
+
await valkeyClient.quit();
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
// ---------------------------------------------------------------------------
|
|
415
|
+
// helpers
|
|
416
|
+
// ---------------------------------------------------------------------------
|
|
417
|
+
|
|
418
|
+
function commandExists(cmd: string): boolean {
|
|
419
|
+
const result = Bun.spawnSync(["which", cmd]);
|
|
420
|
+
return result.exitCode === 0;
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
function readConfigValue(key: string): string | undefined {
|
|
424
|
+
if (!existsSync(CONFIG_PATH)) return undefined;
|
|
425
|
+
try {
|
|
426
|
+
const data: unknown = JSON.parse(readFileSync(CONFIG_PATH, "utf-8"));
|
|
427
|
+
if (typeof data !== "object" || data === null) return undefined;
|
|
428
|
+
const val = (data as Record<string, unknown>)[key];
|
|
429
|
+
if (typeof val === "string") return val;
|
|
430
|
+
if (typeof val === "number") return String(val);
|
|
431
|
+
return undefined;
|
|
432
|
+
} catch {
|
|
433
|
+
return undefined;
|
|
434
|
+
}
|
|
435
|
+
}
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
3
|
+
import { z } from "zod";
|
|
4
|
+
import { getValkeyClient } from "../client/valkey.js";
|
|
5
|
+
import { createModelClient } from "../client/model.js";
|
|
6
|
+
import { formatForInjection } from "../memory/retrieval.js";
|
|
7
|
+
import { getCwdProject } from "../memory/capture.js";
|
|
8
|
+
import type { EpisodicMemory, KnowledgeEntry } from "../memory/schema.js";
|
|
9
|
+
|
|
10
|
+
const server = new McpServer({
|
|
11
|
+
name: "betterdb-memory",
|
|
12
|
+
version: "0.1.0",
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
// --- Tool: search_context ---
|
|
16
|
+
|
|
17
|
+
server.tool(
|
|
18
|
+
"search_context",
|
|
19
|
+
"Search your past Claude Code sessions for relevant context, decisions, or patterns",
|
|
20
|
+
{
|
|
21
|
+
query: z.string().describe("The search query"),
|
|
22
|
+
top_k: z.number().int().min(1).max(20).optional().describe("Max results (default: 5)"),
|
|
23
|
+
},
|
|
24
|
+
async ({ query, top_k }) => {
|
|
25
|
+
const valkeyClient = await getValkeyClient();
|
|
26
|
+
const modelClient = await createModelClient();
|
|
27
|
+
|
|
28
|
+
const embedding = await modelClient.embed(query);
|
|
29
|
+
const project = getCwdProject();
|
|
30
|
+
const k = top_k ?? 5;
|
|
31
|
+
|
|
32
|
+
const memories = await valkeyClient.searchMemories(embedding, project, k);
|
|
33
|
+
const formatted = formatForInjection(memories);
|
|
34
|
+
|
|
35
|
+
return {
|
|
36
|
+
content: [
|
|
37
|
+
{
|
|
38
|
+
type: "text" as const,
|
|
39
|
+
text: formatted || "No matching memories found.",
|
|
40
|
+
},
|
|
41
|
+
],
|
|
42
|
+
};
|
|
43
|
+
},
|
|
44
|
+
);
|
|
45
|
+
|
|
46
|
+
// --- Tool: store_insight ---
|
|
47
|
+
|
|
48
|
+
server.tool(
|
|
49
|
+
"store_insight",
|
|
50
|
+
"Explicitly save an important insight, decision, or warning to persistent memory",
|
|
51
|
+
{
|
|
52
|
+
content: z.string().describe("The insight content"),
|
|
53
|
+
category: z
|
|
54
|
+
.enum(["decision", "pattern", "warning"])
|
|
55
|
+
.describe("Category of the insight"),
|
|
56
|
+
project: z.string().optional().describe("Project name (auto-detected if omitted)"),
|
|
57
|
+
},
|
|
58
|
+
async ({ content, category, project: projectInput }) => {
|
|
59
|
+
const valkeyClient = await getValkeyClient();
|
|
60
|
+
const modelClient = await createModelClient();
|
|
61
|
+
const project = projectInput ?? getCwdProject();
|
|
62
|
+
|
|
63
|
+
// Store as KnowledgeEntry
|
|
64
|
+
const entry: KnowledgeEntry = {
|
|
65
|
+
entryId: crypto.randomUUID(),
|
|
66
|
+
project,
|
|
67
|
+
topic: category,
|
|
68
|
+
fact: content,
|
|
69
|
+
confidence: 0.9,
|
|
70
|
+
sourceMemoryIds: [],
|
|
71
|
+
lastUpdated: new Date().toISOString(),
|
|
72
|
+
accessCount: 0,
|
|
73
|
+
};
|
|
74
|
+
await valkeyClient.storeKnowledge(entry);
|
|
75
|
+
|
|
76
|
+
// Also store as EpisodicMemory for vector searchability
|
|
77
|
+
const embedding = await modelClient.embed(content);
|
|
78
|
+
const memory: EpisodicMemory = {
|
|
79
|
+
memoryId: crypto.randomUUID(),
|
|
80
|
+
project,
|
|
81
|
+
branch: "manual",
|
|
82
|
+
timestamp: new Date().toISOString(),
|
|
83
|
+
summary: {
|
|
84
|
+
decisions: category === "decision" ? [content] : [],
|
|
85
|
+
patterns: category === "pattern" ? [content] : [],
|
|
86
|
+
problemsSolved: [],
|
|
87
|
+
openThreads: category === "warning" ? [content] : [],
|
|
88
|
+
filesChanged: [],
|
|
89
|
+
oneLineSummary: `[${category}] ${content}`,
|
|
90
|
+
},
|
|
91
|
+
importanceScore: 0.8,
|
|
92
|
+
accessCount: 0,
|
|
93
|
+
lastAccessed: new Date().toISOString(),
|
|
94
|
+
};
|
|
95
|
+
await valkeyClient.storeMemory(memory, embedding);
|
|
96
|
+
|
|
97
|
+
return {
|
|
98
|
+
content: [
|
|
99
|
+
{
|
|
100
|
+
type: "text" as const,
|
|
101
|
+
text: `Stored ${category}: "${content}" (memory: ${memory.memoryId})`,
|
|
102
|
+
},
|
|
103
|
+
],
|
|
104
|
+
};
|
|
105
|
+
},
|
|
106
|
+
);
|
|
107
|
+
|
|
108
|
+
// --- Tool: list_open_threads ---
|
|
109
|
+
|
|
110
|
+
server.tool(
|
|
111
|
+
"list_open_threads",
|
|
112
|
+
"List unresolved questions and TODO items from past sessions",
|
|
113
|
+
{
|
|
114
|
+
project: z.string().optional().describe("Project name (auto-detected if omitted)"),
|
|
115
|
+
},
|
|
116
|
+
async ({ project: projectInput }) => {
|
|
117
|
+
const valkeyClient = await getValkeyClient();
|
|
118
|
+
const project = projectInput ?? getCwdProject();
|
|
119
|
+
|
|
120
|
+
const memoryIds = await valkeyClient.listMemoryIds(project, 0.5);
|
|
121
|
+
const threads = new Set<string>();
|
|
122
|
+
|
|
123
|
+
for (const id of memoryIds) {
|
|
124
|
+
const memory = await valkeyClient.getMemory(id);
|
|
125
|
+
if (!memory) continue;
|
|
126
|
+
for (const thread of memory.summary.openThreads) {
|
|
127
|
+
threads.add(thread);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
if (threads.size === 0) {
|
|
132
|
+
return {
|
|
133
|
+
content: [
|
|
134
|
+
{ type: "text" as const, text: "No open threads found." },
|
|
135
|
+
],
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const formatted = `# Open Threads for ${project}\n\n${[...threads].map((t) => `- [ ] ${t}`).join("\n")}`;
|
|
140
|
+
|
|
141
|
+
return {
|
|
142
|
+
content: [{ type: "text" as const, text: formatted }],
|
|
143
|
+
};
|
|
144
|
+
},
|
|
145
|
+
);
|
|
146
|
+
|
|
147
|
+
// --- Tool: forget ---
|
|
148
|
+
|
|
149
|
+
server.tool(
|
|
150
|
+
"forget",
|
|
151
|
+
"Permanently delete a specific memory entry",
|
|
152
|
+
{
|
|
153
|
+
memory_id: z.string().describe("The memory ID to delete"),
|
|
154
|
+
confirmed: z.boolean().optional().describe("Set to true to confirm deletion"),
|
|
155
|
+
},
|
|
156
|
+
async ({ memory_id, confirmed }) => {
|
|
157
|
+
const valkeyClient = await getValkeyClient();
|
|
158
|
+
|
|
159
|
+
if (!confirmed) {
|
|
160
|
+
const memory = await valkeyClient.getMemory(memory_id);
|
|
161
|
+
if (!memory) {
|
|
162
|
+
return {
|
|
163
|
+
content: [
|
|
164
|
+
{
|
|
165
|
+
type: "text" as const,
|
|
166
|
+
text: `Memory ${memory_id} not found.`,
|
|
167
|
+
},
|
|
168
|
+
],
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
return {
|
|
172
|
+
content: [
|
|
173
|
+
{
|
|
174
|
+
type: "text" as const,
|
|
175
|
+
text: `Are you sure you want to delete this memory?\n\n` +
|
|
176
|
+
`**Summary:** ${memory.summary.oneLineSummary}\n` +
|
|
177
|
+
`**Project:** ${memory.project}\n` +
|
|
178
|
+
`**Date:** ${memory.timestamp.split("T")[0]}\n\n` +
|
|
179
|
+
`Call forget again with confirmed=true to proceed.`,
|
|
180
|
+
},
|
|
181
|
+
],
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
await valkeyClient.deleteMemory(memory_id);
|
|
186
|
+
|
|
187
|
+
return {
|
|
188
|
+
content: [
|
|
189
|
+
{
|
|
190
|
+
type: "text" as const,
|
|
191
|
+
text: `Memory ${memory_id} has been permanently deleted.`,
|
|
192
|
+
},
|
|
193
|
+
],
|
|
194
|
+
};
|
|
195
|
+
},
|
|
196
|
+
);
|
|
197
|
+
|
|
198
|
+
// --- Start Server ---
|
|
199
|
+
|
|
200
|
+
const transport = new StdioServerTransport();
|
|
201
|
+
await server.connect(transport);
|