@alex900530/claude-persistent-memory 1.0.7 → 1.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/bin/setup.js +39 -4
- package/hooks/post-tool-memory-hook.js +1 -1
- package/hooks/pre-tool-memory-hook.js +1 -1
- package/hooks/user-prompt-hook.js +1 -1
- package/lib/embedding-client.js +13 -7
- package/lib/memory-db.js +39 -30
- package/package.json +1 -1
- package/services/embedding-server.js +8 -2
- package/services/memory-mcp-server.js +2 -1
package/bin/setup.js
CHANGED
|
@@ -161,9 +161,9 @@ function setupHooks() {
|
|
|
161
161
|
ok(' Hooks configured (5 hooks).');
|
|
162
162
|
}
|
|
163
163
|
|
|
164
|
-
// ── [4/
|
|
164
|
+
// ── [4/6] .gitignore ────────────────────────────────────────────────────────
|
|
165
165
|
function setupGitignore() {
|
|
166
|
-
log('[4/
|
|
166
|
+
log('[4/6] Updating .gitignore ...');
|
|
167
167
|
|
|
168
168
|
const entries = ['.claude-memory/', '.claude-memory.config.js', '.claude/', '.mcp.json'];
|
|
169
169
|
let content = '';
|
|
@@ -187,9 +187,43 @@ function setupGitignore() {
|
|
|
187
187
|
}
|
|
188
188
|
}
|
|
189
189
|
|
|
190
|
-
// ── [5/
|
|
190
|
+
// ── [5/6] Pre-download embedding model ───────────────────────────────────────
|
|
191
|
+
async function setupModel() {
|
|
192
|
+
log('[5/6] Preparing embedding model (bge-m3) ...');
|
|
193
|
+
|
|
194
|
+
const os = require('os');
|
|
195
|
+
const cacheDir = path.join(os.homedir(), '.cache', 'huggingface', 'transformers-js');
|
|
196
|
+
const modelDir = path.join(cacheDir, 'Xenova', 'bge-m3', 'onnx');
|
|
197
|
+
|
|
198
|
+
// Check if model is already cached
|
|
199
|
+
if (fs.existsSync(path.join(modelDir, 'model.onnx_data'))) {
|
|
200
|
+
const stat = fs.statSync(path.join(modelDir, 'model.onnx_data'));
|
|
201
|
+
if (stat.size > 300 * 1024 * 1024) { // > 300MB = likely complete
|
|
202
|
+
ok(` Model already cached (${(stat.size / 1024 / 1024 / 1024).toFixed(1)}GB).`);
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
205
|
+
warn(' Incomplete model cache detected, re-downloading ...');
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
log(' Downloading model (~2GB, this may take a few minutes) ...');
|
|
209
|
+
try {
|
|
210
|
+
// Resolve from package's own dependencies
|
|
211
|
+
const transformersPath = require.resolve('@huggingface/transformers', { paths: [PKG_DIR] });
|
|
212
|
+
const { pipeline, env } = await import(transformersPath);
|
|
213
|
+
env.cacheDir = cacheDir;
|
|
214
|
+
const extractor = await pipeline('feature-extraction', 'Xenova/bge-m3', { device: 'cpu' });
|
|
215
|
+
// Quick test to verify model works
|
|
216
|
+
await extractor('test', { pooling: 'cls', normalize: true });
|
|
217
|
+
ok(' Model downloaded and verified.');
|
|
218
|
+
} catch (e) {
|
|
219
|
+
warn(` Model download failed: ${e.message}`);
|
|
220
|
+
warn(' The embedding server will retry on first start.');
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// ── [6/6] Services ──────────────────────────────────────────────────────────
|
|
191
225
|
function setupServices() {
|
|
192
|
-
log('[
|
|
226
|
+
log('[6/6] Setting up background services ...');
|
|
193
227
|
|
|
194
228
|
// Check if Azure credentials are configured
|
|
195
229
|
let hasCredentials = false;
|
|
@@ -411,6 +445,7 @@ async function main() {
|
|
|
411
445
|
setupMCP();
|
|
412
446
|
setupHooks();
|
|
413
447
|
setupGitignore();
|
|
448
|
+
await setupModel();
|
|
414
449
|
setupServices();
|
|
415
450
|
}
|
|
416
451
|
|
|
@@ -65,7 +65,7 @@ function searchViaEmbedding(query, limit) {
|
|
|
65
65
|
}, TIMEOUT_MS - 50);
|
|
66
66
|
|
|
67
67
|
socket.connect(EMBEDDING_PORT, '127.0.0.1', () => {
|
|
68
|
-
socket.write(JSON.stringify({ action: 'search', query, limit }) + '\n');
|
|
68
|
+
socket.write(JSON.stringify({ action: 'search', query, limit, dataDir: config.dataDir }) + '\n');
|
|
69
69
|
});
|
|
70
70
|
|
|
71
71
|
socket.on('data', (data) => {
|
|
@@ -63,7 +63,7 @@ function searchViaEmbedding(query, limit) {
|
|
|
63
63
|
}, TIMEOUT_MS - 50);
|
|
64
64
|
|
|
65
65
|
socket.connect(EMBEDDING_PORT, '127.0.0.1', () => {
|
|
66
|
-
socket.write(JSON.stringify({ action: 'search', query, limit }) + '\n');
|
|
66
|
+
socket.write(JSON.stringify({ action: 'search', query, limit, dataDir: config.dataDir }) + '\n');
|
|
67
67
|
});
|
|
68
68
|
|
|
69
69
|
socket.on('data', (data) => {
|
|
@@ -53,7 +53,7 @@ function searchViaEmbedding(query, limit) {
|
|
|
53
53
|
}, SEARCH_TIMEOUT_MS);
|
|
54
54
|
|
|
55
55
|
socket.connect(EMBEDDING_PORT, '127.0.0.1', () => {
|
|
56
|
-
socket.write(JSON.stringify({ action: 'search', query, limit }) + '\n');
|
|
56
|
+
socket.write(JSON.stringify({ action: 'search', query, limit, dataDir: config.dataDir }) + '\n');
|
|
57
57
|
});
|
|
58
58
|
|
|
59
59
|
socket.on('data', (data) => {
|
package/lib/embedding-client.js
CHANGED
|
@@ -20,7 +20,7 @@ function log(msg) {
|
|
|
20
20
|
|
|
21
21
|
async function sendRequest(req, timeout = TIMEOUT_MS) {
|
|
22
22
|
const startTime = Date.now();
|
|
23
|
-
log(`[REQ] action=${req.action} query=${str(req.query)} limit=${req.limit || '-'}`);
|
|
23
|
+
log(`[REQ] action=${req.action} query=${str(req.query)} limit=${req.limit || '-'} dataDir=${req.dataDir || 'default'}`);
|
|
24
24
|
|
|
25
25
|
return new Promise((resolve, reject) => {
|
|
26
26
|
const socket = new net.Socket();
|
|
@@ -82,13 +82,17 @@ async function sendRequest(req, timeout = TIMEOUT_MS) {
|
|
|
82
82
|
});
|
|
83
83
|
}
|
|
84
84
|
|
|
85
|
-
async function search(query, limit = 3, options = {}) {
|
|
86
|
-
const
|
|
85
|
+
async function search(query, limit = 3, options = {}, dataDir = null) {
|
|
86
|
+
const req = { action: 'search', query, limit, options };
|
|
87
|
+
if (dataDir) req.dataDir = dataDir;
|
|
88
|
+
const response = await sendRequest(req);
|
|
87
89
|
return response.results;
|
|
88
90
|
}
|
|
89
91
|
|
|
90
|
-
async function quickSearch(query, limit = 3, options = {}) {
|
|
91
|
-
const
|
|
92
|
+
async function quickSearch(query, limit = 3, options = {}, dataDir = null) {
|
|
93
|
+
const req = { action: 'quickSearch', query, limit, options };
|
|
94
|
+
if (dataDir) req.dataDir = dataDir;
|
|
95
|
+
const response = await sendRequest(req);
|
|
92
96
|
return response.results;
|
|
93
97
|
}
|
|
94
98
|
|
|
@@ -101,8 +105,10 @@ async function ping() {
|
|
|
101
105
|
}
|
|
102
106
|
}
|
|
103
107
|
|
|
104
|
-
async function getStats() {
|
|
105
|
-
const
|
|
108
|
+
async function getStats(dataDir = null) {
|
|
109
|
+
const req = { action: 'stats' };
|
|
110
|
+
if (dataDir) req.dataDir = dataDir;
|
|
111
|
+
const response = await sendRequest(req);
|
|
106
112
|
return response.stats;
|
|
107
113
|
}
|
|
108
114
|
|
package/lib/memory-db.js
CHANGED
|
@@ -36,19 +36,17 @@ function tokenize(text) {
|
|
|
36
36
|
}
|
|
37
37
|
|
|
38
38
|
const DATA_DIR = config.dataDir;
|
|
39
|
-
const LOG_DIR = config.logDir;
|
|
40
|
-
const DB_PATH = path.join(DATA_DIR, 'memory.db');
|
|
41
|
-
const LOG_FILE = path.join(LOG_DIR, 'memory-db-calls.log');
|
|
42
39
|
|
|
43
|
-
// Ensure directories exist
|
|
40
|
+
// Ensure default directories exist
|
|
44
41
|
ensureDir(DATA_DIR);
|
|
45
|
-
ensureDir(
|
|
42
|
+
ensureDir(path.join(DATA_DIR, 'logs'));
|
|
46
43
|
|
|
47
44
|
function _str(v) { return v == null ? '' : typeof v === 'string' ? v : JSON.stringify(v); }
|
|
48
45
|
|
|
49
46
|
function _log(msg) {
|
|
47
|
+
const logFile = path.join(activeDataDir, 'logs', 'memory-db-calls.log');
|
|
50
48
|
const line = `[${new Date().toISOString()}] ${msg}\n`;
|
|
51
|
-
try { fs.appendFileSync(
|
|
49
|
+
try { fs.appendFileSync(logFile, line); } catch (e) {}
|
|
52
50
|
}
|
|
53
51
|
|
|
54
52
|
// Stopwords
|
|
@@ -71,26 +69,36 @@ const STRUCTURIZE_CONFIG = {
|
|
|
71
69
|
|
|
72
70
|
// ============== Database Management ==============
|
|
73
71
|
|
|
74
|
-
|
|
72
|
+
const dbPool = new Map(); // dataDir -> Database
|
|
73
|
+
let activeDataDir = DATA_DIR; // default to config's dataDir
|
|
75
74
|
let embeddingModel = null;
|
|
76
75
|
|
|
76
|
+
function setActiveDataDir(dataDir) {
|
|
77
|
+
activeDataDir = dataDir;
|
|
78
|
+
}
|
|
79
|
+
|
|
77
80
|
function getDb() {
|
|
78
|
-
|
|
81
|
+
const dir = activeDataDir;
|
|
82
|
+
if (dbPool.has(dir)) return dbPool.get(dir);
|
|
79
83
|
|
|
80
84
|
try {
|
|
85
|
+
ensureDir(dir);
|
|
86
|
+
ensureDir(path.join(dir, 'logs'));
|
|
87
|
+
|
|
81
88
|
const Database = require('better-sqlite3');
|
|
82
|
-
|
|
89
|
+
const database = new Database(path.join(dir, 'memory.db'));
|
|
83
90
|
|
|
84
91
|
// Load sqlite-vec extension
|
|
85
92
|
try {
|
|
86
93
|
const sqliteVec = require('sqlite-vec');
|
|
87
|
-
sqliteVec.load(
|
|
94
|
+
sqliteVec.load(database);
|
|
88
95
|
} catch (e) {
|
|
89
96
|
console.error('[memory-db] Warning: sqlite-vec not loaded:', e.message);
|
|
90
97
|
}
|
|
91
98
|
|
|
92
|
-
initTables()
|
|
93
|
-
|
|
99
|
+
dbPool.set(dir, database); // put in pool before initTables (in case it calls getDb)
|
|
100
|
+
initTables(database);
|
|
101
|
+
return database;
|
|
94
102
|
} catch (e) {
|
|
95
103
|
console.error('[memory-db] Failed to initialize database:', e.message);
|
|
96
104
|
throw e;
|
|
@@ -98,15 +106,15 @@ function getDb() {
|
|
|
98
106
|
}
|
|
99
107
|
|
|
100
108
|
function closeDb() {
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
db = null;
|
|
109
|
+
for (const [dir, database] of dbPool) {
|
|
110
|
+
database.close();
|
|
104
111
|
}
|
|
112
|
+
dbPool.clear();
|
|
105
113
|
}
|
|
106
114
|
|
|
107
|
-
function initTables() {
|
|
115
|
+
function initTables(database) {
|
|
108
116
|
// Main memories table
|
|
109
|
-
|
|
117
|
+
database.exec(`
|
|
110
118
|
CREATE TABLE IF NOT EXISTS memories (
|
|
111
119
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
112
120
|
content TEXT NOT NULL,
|
|
@@ -132,7 +140,7 @@ function initTables() {
|
|
|
132
140
|
|
|
133
141
|
// [v4.5] Add structured_content column (if it doesn't exist)
|
|
134
142
|
try {
|
|
135
|
-
|
|
143
|
+
database.exec(`ALTER TABLE memories ADD COLUMN structured_content TEXT`);
|
|
136
144
|
} catch (e) {
|
|
137
145
|
// Column already exists, ignore
|
|
138
146
|
}
|
|
@@ -140,7 +148,7 @@ function initTables() {
|
|
|
140
148
|
// FTS5 full-text search table (standalone, not external content)
|
|
141
149
|
// Managed manually via ftsInsert/ftsDelete for jieba tokenization support
|
|
142
150
|
try {
|
|
143
|
-
|
|
151
|
+
database.exec(`
|
|
144
152
|
CREATE VIRTUAL TABLE IF NOT EXISTS memories_fts USING fts5(
|
|
145
153
|
content, structured_content, summary, tags, keywords
|
|
146
154
|
)
|
|
@@ -151,18 +159,18 @@ function initTables() {
|
|
|
151
159
|
|
|
152
160
|
// Migrate from external content FTS to standalone FTS if needed
|
|
153
161
|
try {
|
|
154
|
-
|
|
162
|
+
database.exec("DELETE FROM memories_fts WHERE rowid = -1");
|
|
155
163
|
} catch (e) {
|
|
156
164
|
// External content FTS table detected, rebuild as standalone
|
|
157
|
-
|
|
158
|
-
|
|
165
|
+
database.exec('DROP TABLE IF EXISTS memories_fts');
|
|
166
|
+
database.exec(`
|
|
159
167
|
CREATE VIRTUAL TABLE memories_fts USING fts5(
|
|
160
168
|
content, structured_content, summary, tags, keywords
|
|
161
169
|
)
|
|
162
170
|
`);
|
|
163
171
|
// Repopulate FTS with tokenized content
|
|
164
|
-
const rows =
|
|
165
|
-
const insertFts =
|
|
172
|
+
const rows = database.prepare('SELECT id, content, structured_content, summary, tags, keywords FROM memories').all();
|
|
173
|
+
const insertFts = database.prepare('INSERT INTO memories_fts(rowid, content, structured_content, summary, tags, keywords) VALUES (?, ?, ?, ?, ?, ?)');
|
|
166
174
|
for (const r of rows) {
|
|
167
175
|
try {
|
|
168
176
|
insertFts.run(r.id, tokenize(r.content || ''), tokenize(r.structured_content || ''), tokenize(r.summary || ''), tokenize(r.tags || ''), tokenize(r.keywords || ''));
|
|
@@ -172,7 +180,7 @@ function initTables() {
|
|
|
172
180
|
|
|
173
181
|
// Vector table (if sqlite-vec is available, use cosine distance)
|
|
174
182
|
try {
|
|
175
|
-
|
|
183
|
+
database.exec(`
|
|
176
184
|
CREATE VIRTUAL TABLE IF NOT EXISTS memories_vec USING vec0(
|
|
177
185
|
embedding float[${config.embedding.dimensions}] distance_metric=cosine
|
|
178
186
|
)
|
|
@@ -182,7 +190,7 @@ function initTables() {
|
|
|
182
190
|
}
|
|
183
191
|
|
|
184
192
|
// Clusters table
|
|
185
|
-
|
|
193
|
+
database.exec(`
|
|
186
194
|
CREATE TABLE IF NOT EXISTS clusters (
|
|
187
195
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
188
196
|
theme TEXT NOT NULL,
|
|
@@ -199,7 +207,7 @@ function initTables() {
|
|
|
199
207
|
`);
|
|
200
208
|
|
|
201
209
|
// Create indexes
|
|
202
|
-
|
|
210
|
+
database.exec(`
|
|
203
211
|
CREATE INDEX IF NOT EXISTS idx_memories_type ON memories(type);
|
|
204
212
|
CREATE INDEX IF NOT EXISTS idx_memories_domain ON memories(domain);
|
|
205
213
|
CREATE INDEX IF NOT EXISTS idx_memories_confidence ON memories(confidence);
|
|
@@ -208,9 +216,9 @@ function initTables() {
|
|
|
208
216
|
`);
|
|
209
217
|
|
|
210
218
|
// Drop old FTS triggers (no longer needed with standalone FTS + manual management)
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
219
|
+
database.exec(`DROP TRIGGER IF EXISTS memories_ai`);
|
|
220
|
+
database.exec(`DROP TRIGGER IF EXISTS memories_ad`);
|
|
221
|
+
database.exec(`DROP TRIGGER IF EXISTS memories_au`);
|
|
214
222
|
}
|
|
215
223
|
|
|
216
224
|
// ============== FTS Management ==============
|
|
@@ -1272,6 +1280,7 @@ module.exports = {
|
|
|
1272
1280
|
// Database
|
|
1273
1281
|
getDb,
|
|
1274
1282
|
closeDb,
|
|
1283
|
+
setActiveDataDir,
|
|
1275
1284
|
|
|
1276
1285
|
// Core functions
|
|
1277
1286
|
save,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@alex900530/claude-persistent-memory",
|
|
3
|
-
"version": "1.0
|
|
3
|
+
"version": "1.1.0",
|
|
4
4
|
"description": "Persistent memory system for Claude Code — hybrid BM25 + vector search, LLM-driven structuring, automatic clustering",
|
|
5
5
|
"main": "lib/memory-db.js",
|
|
6
6
|
"bin": {
|
|
@@ -39,16 +39,22 @@ async function handleRequest(data) {
|
|
|
39
39
|
try {
|
|
40
40
|
const request = JSON.parse(data);
|
|
41
41
|
const startTime = Date.now();
|
|
42
|
+
|
|
43
|
+
// Switch to request-specified project database
|
|
44
|
+
if (request.dataDir) {
|
|
45
|
+
memoryDb.setActiveDataDir(request.dataDir);
|
|
46
|
+
}
|
|
47
|
+
|
|
42
48
|
switch (request.action) {
|
|
43
49
|
case 'search': {
|
|
44
|
-
log(`[REQ] action=search query=${str(request.query)} limit=${request.limit || 3}`);
|
|
50
|
+
log(`[REQ] action=search query=${str(request.query)} limit=${request.limit || 3} dataDir=${request.dataDir || 'default'}`);
|
|
45
51
|
const results = await memoryDb.search(request.query, request.limit || 3, request.options || {});
|
|
46
52
|
const duration = Date.now() - startTime;
|
|
47
53
|
log(`[RES] action=search duration=${duration}ms results=${results.length} matches=${results.map(r => '#' + r.id + '(' + (r.vectorSimilarity != null ? r.vectorSimilarity.toFixed(3) : '?') + ')').join(',')}`);
|
|
48
54
|
return { success: true, results };
|
|
49
55
|
}
|
|
50
56
|
case 'quickSearch': {
|
|
51
|
-
log(`[REQ] action=quickSearch query=${str(request.query)} limit=${request.limit || 3}`);
|
|
57
|
+
log(`[REQ] action=quickSearch query=${str(request.query)} limit=${request.limit || 3} dataDir=${request.dataDir || 'default'}`);
|
|
52
58
|
const quickResults = memoryDb.quickSearch(request.query, request.limit || 3, request.options || {});
|
|
53
59
|
const duration = Date.now() - startTime;
|
|
54
60
|
log(`[RES] action=quickSearch duration=${duration}ms results=${quickResults.length} matches=${quickResults.map(r => '#' + r.id + '(' + (r.vectorSimilarity != null ? r.vectorSimilarity.toFixed(3) : '?') + ')').join(',')}`);
|
|
@@ -30,6 +30,7 @@ const z = require('zod');
|
|
|
30
30
|
|
|
31
31
|
// ============ Memory modules ============
|
|
32
32
|
|
|
33
|
+
const config = require('../config');
|
|
33
34
|
const memoryDb = require('../lib/memory-db');
|
|
34
35
|
|
|
35
36
|
// Embedding client for hybrid search via embedding server
|
|
@@ -55,7 +56,7 @@ async function hybridSearch(query, limit, options = {}) {
|
|
|
55
56
|
}
|
|
56
57
|
|
|
57
58
|
if (useEmbeddingService) {
|
|
58
|
-
return embeddingClient.search(query, limit);
|
|
59
|
+
return embeddingClient.search(query, limit, options, config.dataDir);
|
|
59
60
|
}
|
|
60
61
|
|
|
61
62
|
// Fallback: inline hybrid search
|