superlocalmemory 3.0.2 → 3.0.5

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "superlocalmemory",
3
- "version": "3.0.2",
3
+ "version": "3.0.5",
4
4
  "description": "Information-geometric agent memory with mathematical guarantees. 4-channel retrieval, Fisher-Rao similarity, zero-LLM mode, EU AI Act compliant. Works with Claude, Cursor, Windsurf, and 17+ AI tools.",
5
5
  "keywords": [
6
6
  "ai-memory",
@@ -7,6 +7,7 @@
7
7
  * Repository: https://github.com/qualixar/superlocalmemory
8
8
  */
9
9
 
10
+ const { spawnSync } = require('child_process');
10
11
  const path = require('path');
11
12
  const os = require('os');
12
13
  const fs = require('fs');
@@ -17,11 +18,8 @@ console.log(' by Varun Pratap Bhardwaj / Qualixar');
17
18
  console.log(' https://github.com/qualixar/superlocalmemory');
18
19
  console.log('════════════════════════════════════════════════════════════\n');
19
20
 
20
- // V3 data directory
21
+ // --- Step 1: Create data directory ---
21
22
  const SLM_HOME = path.join(os.homedir(), '.superlocalmemory');
22
- const V2_HOME = path.join(os.homedir(), '.claude-memory');
23
-
24
- // Ensure V3 data directory exists
25
23
  if (!fs.existsSync(SLM_HOME)) {
26
24
  fs.mkdirSync(SLM_HOME, { recursive: true });
27
25
  console.log('✓ Created data directory: ' + SLM_HOME);
@@ -29,7 +27,80 @@ if (!fs.existsSync(SLM_HOME)) {
29
27
  console.log('✓ Data directory exists: ' + SLM_HOME);
30
28
  }
31
29
 
32
- // Detect V2 installation
30
+ // --- Step 2: Find Python 3 ---
31
+ function findPython() {
32
+ const candidates = ['python3', 'python', '/opt/homebrew/bin/python3', '/usr/local/bin/python3', '/usr/bin/python3'];
33
+ if (os.platform() === 'win32') candidates.push('py -3');
34
+ for (const cmd of candidates) {
35
+ try {
36
+ const parts = cmd.split(' ');
37
+ const r = spawnSync(parts[0], [...parts.slice(1), '--version'], { stdio: 'pipe', timeout: 5000 });
38
+ if (r.status === 0 && (r.stdout || '').toString().includes('3.')) return cmd;
39
+ } catch (e) { /* next */ }
40
+ }
41
+ return null;
42
+ }
43
+
44
+ const python = findPython();
45
+ if (!python) {
46
+ console.log('⚠ Python 3.11+ not found. Install from https://python.org/downloads/');
47
+ console.log(' After installing Python, run: slm setup');
48
+ process.exit(0); // Don't fail npm install
49
+ }
50
+ console.log('✓ Found Python: ' + python);
51
+
52
+ // --- Step 3: Install Python dependencies ---
53
+ console.log('\nInstalling Python dependencies...');
54
+ const PKG_ROOT = path.join(__dirname, '..');
55
+ const requirementsFile = path.join(PKG_ROOT, 'src', 'superlocalmemory', 'requirements-install.txt');
56
+
57
+ // Create a minimal requirements file for core operation
58
+ const coreDeps = [
59
+ 'numpy>=1.26.0',
60
+ 'scipy>=1.12.0',
61
+ 'networkx>=3.0',
62
+ 'httpx>=0.24.0',
63
+ 'python-dateutil>=2.9.0',
64
+ 'rank-bm25>=0.2.2',
65
+ 'vaderSentiment>=3.3.2',
66
+ ].join('\n');
67
+
68
+ // Write temp requirements file
69
+ const tmpReq = path.join(SLM_HOME, '.install-requirements.txt');
70
+ fs.writeFileSync(tmpReq, coreDeps);
71
+
72
+ const pythonParts = python.split(' ');
73
+ const pipResult = spawnSync(pythonParts[0], [
74
+ ...pythonParts.slice(1), '-m', 'pip', 'install', '--quiet', '--disable-pip-version-check',
75
+ '-r', tmpReq,
76
+ ], { stdio: 'inherit', timeout: 120000 });
77
+
78
+ if (pipResult.status === 0) {
79
+ console.log('✓ Core Python dependencies installed');
80
+ } else {
81
+ console.log('⚠ Some Python dependencies failed to install.');
82
+ console.log(' Run manually: ' + python + ' -m pip install numpy scipy networkx httpx python-dateutil rank-bm25 vaderSentiment');
83
+ }
84
+
85
+ // Clean up temp file
86
+ try { fs.unlinkSync(tmpReq); } catch (e) { /* ok */ }
87
+
88
+ // --- Step 4: Optional search dependencies (sentence-transformers) ---
89
+ console.log('\nChecking optional search dependencies...');
90
+ const stCheck = spawnSync(pythonParts[0], [
91
+ ...pythonParts.slice(1), '-c', 'import sentence_transformers; print("ok")',
92
+ ], { stdio: 'pipe', timeout: 10000 });
93
+
94
+ if (stCheck.status === 0) {
95
+ console.log('✓ sentence-transformers already installed (semantic search enabled)');
96
+ } else {
97
+ console.log('ℹ sentence-transformers not installed (BM25-only search mode)');
98
+ console.log(' For semantic search, run: ' + python + ' -m pip install sentence-transformers');
99
+ console.log(' This downloads ~1.5GB of ML models on first use.');
100
+ }
101
+
102
+ // --- Step 5: Detect V2 installation ---
103
+ const V2_HOME = path.join(os.homedir(), '.claude-memory');
33
104
  if (fs.existsSync(V2_HOME) && fs.existsSync(path.join(V2_HOME, 'memory.db'))) {
34
105
  console.log('');
35
106
  console.log('╔══════════════════════════════════════════════════════════╗');
@@ -42,11 +113,9 @@ if (fs.existsSync(V2_HOME) && fs.existsSync(path.join(V2_HOME, 'memory.db'))) {
42
113
  console.log(' To migrate V2 data to V3, run:');
43
114
  console.log(' slm migrate');
44
115
  console.log('');
45
- console.log(' Read the migration guide:');
46
- console.log(' https://github.com/qualixar/superlocalmemory/wiki/Migration-from-V2');
47
- console.log('');
48
116
  }
49
117
 
118
+ // --- Done ---
50
119
  console.log('════════════════════════════════════════════════════════════');
51
120
  console.log(' ✓ SuperLocalMemory V3 installed successfully!');
52
121
  console.log('');
@@ -56,6 +56,14 @@ class EmbeddingService:
56
56
  self._model: object | None = None
57
57
  self._lock = threading.Lock()
58
58
  self._loaded = False
59
+ self._available = True # Set False if model can't load
60
+
61
+ @property
62
+ def is_available(self) -> bool:
63
+ """Check if embedding service has a usable model."""
64
+ if not self._loaded:
65
+ self._ensure_loaded()
66
+ return self._available and self._model is not None
59
67
 
60
68
  # ------------------------------------------------------------------
61
69
  # Public API
@@ -78,6 +86,9 @@ class EmbeddingService:
78
86
  """
79
87
  if not text or not text.strip():
80
88
  raise ValueError("Cannot embed empty text")
89
+ self._ensure_loaded()
90
+ if self._model is None:
91
+ return None
81
92
  vec = self._encode_single(text)
82
93
  self._validate_dimension(vec)
83
94
  return vec.tolist()
@@ -98,6 +109,9 @@ class EmbeddingService:
98
109
  if not t or not t.strip():
99
110
  raise ValueError(f"Text at index {i} is empty")
100
111
 
112
+ self._ensure_loaded()
113
+ if self._model is None:
114
+ return [None] * len(texts)
101
115
  vectors = self._encode_batch(texts)
102
116
  for vec in vectors:
103
117
  self._validate_dimension(vec)
@@ -175,10 +189,14 @@ class EmbeddingService:
175
189
  try:
176
190
  from sentence_transformers import SentenceTransformer
177
191
  except ImportError:
178
- raise ImportError(
179
- "sentence-transformers required: "
180
- "pip install sentence-transformers"
192
+ logger.warning(
193
+ "sentence-transformers not installed. Embeddings disabled. "
194
+ "Install with: pip install sentence-transformers"
181
195
  )
196
+ self._model = None
197
+ self._loaded = True
198
+ self._available = False
199
+ return
182
200
  model = SentenceTransformer(
183
201
  self._config.model_name, trust_remote_code=False,
184
202
  )
@@ -81,7 +81,12 @@ class MemoryEngine:
81
81
 
82
82
  self._db = DatabaseManager(self._config.db_path)
83
83
  self._db.initialize(schema)
84
- self._embedder = EmbeddingService(self._config.embedding)
84
+ try:
85
+ emb = EmbeddingService(self._config.embedding)
86
+ self._embedder = emb if emb.is_available else None
87
+ except Exception as exc:
88
+ logger.warning("Embeddings unavailable (%s). BM25-only mode.", exc)
89
+ self._embedder = None
85
90
 
86
91
  if self._caps.llm_fact_extraction:
87
92
  self._llm = LLMBackbone(self._config.llm)
@@ -68,24 +68,21 @@ class EntropyGate:
68
68
  logger.debug("Entropy gate: blocked (low-info pattern: '%s')", normalized)
69
69
  return False
70
70
 
71
- # Stage 2: Similarity-based deduplication
72
- if self._embedder is not None and self._recent_embeddings:
71
+ # Stage 2: Similarity-based deduplication (requires embeddings)
72
+ if self._embedder is not None:
73
73
  emb = self._embedder.embed(content)
74
- for recent in self._recent_embeddings:
75
- sim = _cosine(emb, recent)
76
- if sim > self._threshold:
77
- logger.debug(
78
- "Entropy gate: blocked (near-duplicate, sim=%.3f)", sim
79
- )
80
- return False
81
- # Add to window
82
- self._recent_embeddings.append(emb)
83
- if len(self._recent_embeddings) > self._window_size:
84
- self._recent_embeddings.pop(0)
85
- elif self._embedder is not None:
86
- # First content — add to window, always pass
87
- emb = self._embedder.embed(content)
88
- self._recent_embeddings.append(emb)
74
+ if emb is not None:
75
+ if self._recent_embeddings:
76
+ for recent in self._recent_embeddings:
77
+ sim = _cosine(emb, recent)
78
+ if sim > self._threshold:
79
+ logger.debug(
80
+ "Entropy gate: blocked (near-duplicate, sim=%.3f)", sim
81
+ )
82
+ return False
83
+ self._recent_embeddings.append(emb)
84
+ if len(self._recent_embeddings) > self._window_size:
85
+ self._recent_embeddings.pop(0)
89
86
 
90
87
  return True
91
88
 
@@ -240,24 +240,70 @@ class V2Migrator:
240
240
  shutil.copy2(str(self._v2_db), str(self._v3_db))
241
241
  stats["steps"].append("Copied database to V3 location")
242
242
 
243
- # Step 4: Extend schema
243
+ # Step 4: Extend schema + alter V2 tables for V3 compatibility
244
244
  conn = sqlite3.connect(str(self._v3_db))
245
- for sql in V3_TABLES_SQL:
246
- conn.execute(sql)
247
- for sql in V3_INDEXES_SQL:
248
- conn.execute(sql)
249
- # Mark migration
250
- conn.execute(
251
- "INSERT OR REPLACE INTO v3_config (key, value, updated_at) VALUES (?, ?, ?)",
252
- ("migration_date", datetime.now(UTC).isoformat(), datetime.now(UTC).isoformat()),
253
- )
254
- conn.execute(
255
- "INSERT OR REPLACE INTO v3_config (key, value, updated_at) VALUES (?, ?, ?)",
256
- ("migration_version", "3.0.0", datetime.now(UTC).isoformat()),
257
- )
245
+
246
+ # Add missing V3 columns to V2 memories table
247
+ existing_cols = {r[1] for r in conn.execute("PRAGMA table_info(memories)").fetchall()}
248
+ v3_columns = [
249
+ ("profile_id", 'TEXT DEFAULT "default"'),
250
+ ("memory_id", "TEXT"),
251
+ ("session_id", 'TEXT DEFAULT ""'),
252
+ ("speaker", 'TEXT DEFAULT ""'),
253
+ ("role", 'TEXT DEFAULT "user"'),
254
+ ("session_date", "TEXT"),
255
+ ("metadata_json", 'TEXT DEFAULT "{}"'),
256
+ ]
257
+ for col, coltype in v3_columns:
258
+ if col not in existing_cols:
259
+ conn.execute(f"ALTER TABLE memories ADD COLUMN {col} {coltype}")
260
+ # Backfill V3 columns from V2 data
261
+ conn.execute('UPDATE memories SET profile_id = COALESCE(profile, "default") WHERE profile_id IS NULL')
262
+ conn.execute("UPDATE memories SET memory_id = 'v2_' || CAST(id AS TEXT) WHERE memory_id IS NULL")
263
+ # Create unique index on memory_id so V3 FKs work
264
+ try:
265
+ conn.execute("CREATE UNIQUE INDEX IF NOT EXISTS idx_memories_memory_id ON memories (memory_id)")
266
+ except Exception:
267
+ pass
268
+ # Disable FK enforcement for migrated DBs (V2 schema is incompatible)
269
+ conn.execute("PRAGMA foreign_keys=OFF")
270
+
271
+ # Rename ALL tables with incompatible schemas (V2 + old alpha)
272
+ # User data is in 'memories' table (already upgraded above)
273
+ # Everything else is computed/derived and will be recreated by V3
274
+ all_existing = {r[0] for r in conn.execute(
275
+ "SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE '_v2_bak_%'"
276
+ ).fetchall()}
277
+ # Keep only: memories (upgraded), profiles, schema_version, v3_config, sqlite_sequence
278
+ keep_tables = {"memories", "profiles", "schema_version", "v3_config", "sqlite_sequence"}
279
+ v2_conflicting = [t for t in all_existing if t not in keep_tables and not t.startswith("_")]
280
+ for table in v2_conflicting:
281
+ try:
282
+ conn.execute(f'ALTER TABLE "{table}" RENAME TO "_v2_bak_{table}"')
283
+ except Exception:
284
+ pass # Table may not exist
285
+
258
286
  conn.commit()
287
+
288
+ # Use the FULL V3 schema (not the partial V3_TABLES_SQL)
289
+ from superlocalmemory.storage import schema
290
+ schema.create_all_tables(conn)
291
+ conn.commit()
292
+ # Mark migration in config table
293
+ try:
294
+ conn.execute(
295
+ "INSERT OR REPLACE INTO config (key, value, updated_at) VALUES (?, ?, ?)",
296
+ ("migration_date", datetime.now(UTC).isoformat(), datetime.now(UTC).isoformat()),
297
+ )
298
+ conn.execute(
299
+ "INSERT OR REPLACE INTO config (key, value, updated_at) VALUES (?, ?, ?)",
300
+ ("migration_version", "3.0.0", datetime.now(UTC).isoformat()),
301
+ )
302
+ conn.commit()
303
+ except Exception:
304
+ pass # Schema handles this on engine init
259
305
  conn.close()
260
- stats["steps"].append(f"Extended schema ({len(V3_TABLES_SQL)} tables, {len(V3_INDEXES_SQL)} indexes)")
306
+ stats["steps"].append("Created V3 schema")
261
307
 
262
308
  # Step 5: Symlink (only if .claude-memory is not already a symlink)
263
309
  if not self._v2_base.is_symlink():