@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 CHANGED
@@ -161,9 +161,9 @@ function setupHooks() {
161
161
  ok(' Hooks configured (5 hooks).');
162
162
  }
163
163
 
164
- // ── [4/5] .gitignore ────────────────────────────────────────────────────────
164
+ // ── [4/6] .gitignore ────────────────────────────────────────────────────────
165
165
  function setupGitignore() {
166
- log('[4/5] Updating .gitignore ...');
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/5] Services ──────────────────────────────────────────────────────────
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('[5/5] Setting up background services ...');
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) => {
@@ -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 response = await sendRequest({ action: 'search', query, limit, options });
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 response = await sendRequest({ action: 'quickSearch', query, limit, options });
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 response = await sendRequest({ action: 'stats' });
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(LOG_DIR);
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(LOG_FILE, line); } catch (e) {}
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
- let db = null;
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
- if (db) return db;
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
- db = new Database(DB_PATH);
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(db);
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
- return db;
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
- if (db) {
102
- db.close();
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
- db.exec(`
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
- db.exec(`ALTER TABLE memories ADD COLUMN structured_content TEXT`);
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
- db.exec(`
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
- db.exec("DELETE FROM memories_fts WHERE rowid = -1");
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
- db.exec('DROP TABLE IF EXISTS memories_fts');
158
- db.exec(`
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 = db.prepare('SELECT id, content, structured_content, summary, tags, keywords FROM memories').all();
165
- const insertFts = db.prepare('INSERT INTO memories_fts(rowid, content, structured_content, summary, tags, keywords) VALUES (?, ?, ?, ?, ?, ?)');
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
- db.exec(`
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
- db.exec(`
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
- db.exec(`
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
- db.exec(`DROP TRIGGER IF EXISTS memories_ai`);
212
- db.exec(`DROP TRIGGER IF EXISTS memories_ad`);
213
- db.exec(`DROP TRIGGER IF EXISTS memories_au`);
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.7",
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