superlocalmemory 3.0.3 → 3.0.7

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 CHANGED
@@ -85,13 +85,35 @@ Query ──► Strategy Classifier ──► 4 Parallel Channels:
85
85
 
86
86
  ---
87
87
 
88
+ ## Prerequisites
89
+
90
+ | Requirement | Version | Why |
91
+ |:-----------|:--------|:----|
92
+ | **Node.js** | 14+ | npm package manager |
93
+ | **Python** | 3.11+ | V3 engine runtime |
94
+ | **pip** | Latest | Python dependency installer |
95
+
96
+ > All Python dependencies are installed automatically during `npm install`. You don't need to run pip manually. If any dependency fails, the installer shows clear instructions.
97
+
98
+ ---
99
+
88
100
  ## Quick Start
89
101
 
90
- ### Install via npm (recommended)
102
+ ### Install via npm (recommended — one command, everything included)
91
103
 
92
104
  ```bash
93
105
  npm install -g superlocalmemory
94
- slm setup
106
+ ```
107
+
108
+ This single command:
109
+ - Installs the V3 engine and CLI
110
+ - Auto-installs all Python dependencies (numpy, scipy, networkx, sentence-transformers, etc.)
111
+ - Creates the data directory at `~/.superlocalmemory/`
112
+ - Detects and guides V2 migration if applicable
113
+
114
+ Then configure:
115
+ ```bash
116
+ slm setup # Choose mode, configure provider
95
117
  ```
96
118
 
97
119
  ### Install via pip
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "superlocalmemory",
3
- "version": "3.0.3",
3
+ "version": "3.0.7",
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",
@@ -2,26 +2,26 @@
2
2
  /**
3
3
  * SuperLocalMemory V3 - NPM Postinstall Script
4
4
  *
5
+ * ONE COMMAND INSTALL. Everything the user needs.
6
+ * Python deps auto-installed. Embeddings auto-downloaded.
7
+ *
5
8
  * Copyright (c) 2026 Varun Pratap Bhardwaj / Qualixar
6
9
  * Licensed under MIT License
7
- * Repository: https://github.com/qualixar/superlocalmemory
8
10
  */
9
11
 
12
+ const { spawnSync } = require('child_process');
10
13
  const path = require('path');
11
14
  const os = require('os');
12
15
  const fs = require('fs');
13
16
 
14
17
  console.log('\n════════════════════════════════════════════════════════════');
15
- console.log(' SuperLocalMemory V3 - Post-Installation');
18
+ console.log(' SuperLocalMemory V3 Post-Installation');
16
19
  console.log(' by Varun Pratap Bhardwaj / Qualixar');
17
20
  console.log(' https://github.com/qualixar/superlocalmemory');
18
21
  console.log('════════════════════════════════════════════════════════════\n');
19
22
 
20
- // V3 data directory
23
+ // --- Step 1: Create data directory ---
21
24
  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
25
  if (!fs.existsSync(SLM_HOME)) {
26
26
  fs.mkdirSync(SLM_HOME, { recursive: true });
27
27
  console.log('✓ Created data directory: ' + SLM_HOME);
@@ -29,7 +29,102 @@ if (!fs.existsSync(SLM_HOME)) {
29
29
  console.log('✓ Data directory exists: ' + SLM_HOME);
30
30
  }
31
31
 
32
- // Detect V2 installation
32
+ // --- Step 2: Find Python 3 ---
33
+ function findPython() {
34
+ const candidates = [
35
+ 'python3', 'python',
36
+ '/opt/homebrew/bin/python3', '/usr/local/bin/python3', '/usr/bin/python3',
37
+ ];
38
+ if (os.platform() === 'win32') candidates.push('py -3');
39
+ for (const cmd of candidates) {
40
+ try {
41
+ const parts = cmd.split(' ');
42
+ const r = spawnSync(parts[0], [...parts.slice(1), '--version'], {
43
+ stdio: 'pipe', timeout: 5000,
44
+ env: { ...process.env, PATH: '/opt/homebrew/bin:/usr/local/bin:/usr/bin:' + (process.env.PATH || '') },
45
+ });
46
+ if (r.status === 0 && (r.stdout || '').toString().includes('3.')) return parts;
47
+ } catch (e) { /* next */ }
48
+ }
49
+ return null;
50
+ }
51
+
52
+ const pythonParts = findPython();
53
+ if (!pythonParts) {
54
+ console.log('');
55
+ console.log('╔══════════════════════════════════════════════════════════╗');
56
+ console.log('║ ⚠ Python 3.11+ Required ║');
57
+ console.log('╚══════════════════════════════════════════════════════════╝');
58
+ console.log('');
59
+ console.log(' SuperLocalMemory V3 requires Python 3.11+');
60
+ console.log(' Install from: https://python.org/downloads/');
61
+ console.log(' After installing Python, run: slm setup');
62
+ console.log('');
63
+ process.exit(0); // Don't fail npm install
64
+ }
65
+ console.log('✓ Found Python: ' + pythonParts.join(' '));
66
+
67
+ // --- Step 3: Install ALL Python dependencies ---
68
+ console.log('\nInstalling Python dependencies (this may take 1-2 minutes)...\n');
69
+
70
+ // Detect if --user or --break-system-packages is needed
71
+ function pipInstall(packages, label) {
72
+ // Try normal install first
73
+ let result = spawnSync(pythonParts[0], [
74
+ ...pythonParts.slice(1), '-m', 'pip', 'install', '--quiet', '--disable-pip-version-check',
75
+ ...packages,
76
+ ], { stdio: 'pipe', timeout: 300000, env: { ...process.env, PATH: '/opt/homebrew/bin:/usr/local/bin:/usr/bin:' + (process.env.PATH || '') } });
77
+
78
+ if (result.status === 0) return true;
79
+
80
+ // If PEP 668 blocks it, try --user
81
+ const stderr = (result.stderr || '').toString();
82
+ if (stderr.includes('externally-managed') || stderr.includes('PEP 668')) {
83
+ result = spawnSync(pythonParts[0], [
84
+ ...pythonParts.slice(1), '-m', 'pip', 'install', '--quiet', '--disable-pip-version-check',
85
+ '--user', ...packages,
86
+ ], { stdio: 'pipe', timeout: 300000, env: { ...process.env, PATH: '/opt/homebrew/bin:/usr/local/bin:/usr/bin:' + (process.env.PATH || '') } });
87
+ if (result.status === 0) return true;
88
+
89
+ // Last resort: --break-system-packages
90
+ result = spawnSync(pythonParts[0], [
91
+ ...pythonParts.slice(1), '-m', 'pip', 'install', '--quiet', '--disable-pip-version-check',
92
+ '--break-system-packages', ...packages,
93
+ ], { stdio: 'pipe', timeout: 300000, env: { ...process.env, PATH: '/opt/homebrew/bin:/usr/local/bin:/usr/bin:' + (process.env.PATH || '') } });
94
+ return result.status === 0;
95
+ }
96
+
97
+ return false;
98
+ }
99
+
100
+ // Core dependencies (REQUIRED — product won't work without these)
101
+ const coreDeps = [
102
+ 'numpy>=1.26.0', 'scipy>=1.12.0', 'networkx>=3.0',
103
+ 'httpx>=0.24.0', 'python-dateutil>=2.9.0',
104
+ 'rank-bm25>=0.2.2', 'vaderSentiment>=3.3.2',
105
+ ];
106
+
107
+ if (pipInstall(coreDeps, 'core')) {
108
+ console.log('✓ Core dependencies installed (math, search, NLP)');
109
+ } else {
110
+ console.log('⚠ Core dependency installation failed.');
111
+ console.log(' Run manually: pip install ' + coreDeps.join(' '));
112
+ }
113
+
114
+ // Search dependencies (IMPORTANT — enables semantic search, 4-channel retrieval)
115
+ const searchDeps = ['sentence-transformers>=2.5.0', 'geoopt>=0.5.0'];
116
+
117
+ console.log('\nInstalling semantic search engine (downloads ~500MB on first use)...');
118
+ if (pipInstall(searchDeps, 'search')) {
119
+ console.log('✓ Semantic search engine installed (sentence-transformers + Fisher-Rao)');
120
+ } else {
121
+ console.log('⚠ Semantic search installation failed (BM25 keyword search still works).');
122
+ console.log(' For full 4-channel retrieval, run:');
123
+ console.log(' pip install sentence-transformers geoopt');
124
+ }
125
+
126
+ // --- Step 4: Detect V2 installation ---
127
+ const V2_HOME = path.join(os.homedir(), '.claude-memory');
33
128
  if (fs.existsSync(V2_HOME) && fs.existsSync(path.join(V2_HOME, 'memory.db'))) {
34
129
  console.log('');
35
130
  console.log('╔══════════════════════════════════════════════════════════╗');
@@ -42,11 +137,9 @@ if (fs.existsSync(V2_HOME) && fs.existsSync(path.join(V2_HOME, 'memory.db'))) {
42
137
  console.log(' To migrate V2 data to V3, run:');
43
138
  console.log(' slm migrate');
44
139
  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
140
  }
49
141
 
142
+ // --- Done ---
50
143
  console.log('════════════════════════════════════════════════════════════');
51
144
  console.log(' ✓ SuperLocalMemory V3 installed successfully!');
52
145
  console.log('');
@@ -56,5 +149,10 @@ console.log(' slm status # Check system status');
56
149
  console.log(' slm remember "..." # Store a memory');
57
150
  console.log(' slm recall "..." # Search memories');
58
151
  console.log('');
59
- console.log(' Documentation: https://github.com/qualixar/superlocalmemory/wiki');
152
+ console.log(' Prerequisites satisfied:');
153
+ console.log(' ✓ Python 3.11+');
154
+ console.log(' ✓ Core math & search libraries');
155
+ console.log(' ✓ Data directory (~/.superlocalmemory/)');
156
+ console.log('');
157
+ console.log(' Docs: https://github.com/qualixar/superlocalmemory/wiki');
60
158
  console.log('════════════════════════════════════════════════════════════\n');
@@ -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,12 +189,16 @@ 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
- self._config.model_name, trust_remote_code=False,
201
+ self._config.model_name, trust_remote_code=True,
184
202
  )
185
203
  actual_dim = model.get_sentence_embedding_dimension()
186
204
  if actual_dim != self._config.dimension:
@@ -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
 
@@ -260,24 +260,77 @@ class V2Migrator:
260
260
  # Backfill V3 columns from V2 data
261
261
  conn.execute('UPDATE memories SET profile_id = COALESCE(profile, "default") WHERE profile_id IS NULL')
262
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
+
263
286
  conn.commit()
264
287
 
265
- for sql in V3_TABLES_SQL:
266
- conn.execute(sql)
267
- for sql in V3_INDEXES_SQL:
268
- conn.execute(sql)
269
- # Mark migration
270
- conn.execute(
271
- "INSERT OR REPLACE INTO v3_config (key, value, updated_at) VALUES (?, ?, ?)",
272
- ("migration_date", datetime.now(UTC).isoformat(), datetime.now(UTC).isoformat()),
273
- )
274
- conn.execute(
275
- "INSERT OR REPLACE INTO v3_config (key, value, updated_at) VALUES (?, ?, ?)",
276
- ("migration_version", "3.0.0", datetime.now(UTC).isoformat()),
277
- )
288
+ # Use the FULL V3 schema (not the partial V3_TABLES_SQL)
289
+ from superlocalmemory.storage import schema
290
+ schema.create_all_tables(conn)
278
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
305
+ # Step 4b: Convert V2 memories → V3 atomic_facts
306
+ try:
307
+ now = datetime.now(UTC).isoformat()
308
+ rows = conn.execute("SELECT memory_id, profile_id, content, created_at FROM memories").fetchall()
309
+ converted = 0
310
+ for row in rows:
311
+ mid, pid, content, created = row[0], row[1], row[2], row[3]
312
+ if not content or not content.strip():
313
+ continue
314
+ fid = f"v2_fact_{mid}"
315
+ conn.execute(
316
+ "INSERT OR IGNORE INTO atomic_facts "
317
+ "(fact_id, memory_id, profile_id, content, fact_type, "
318
+ " entities_json, canonical_entities_json, confidence, importance, "
319
+ " evidence_count, access_count, source_turn_ids_json, session_id, "
320
+ " lifecycle, emotional_valence, emotional_arousal, signal_type, created_at) "
321
+ "VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)",
322
+ (fid, mid, pid or "default", content, "factual",
323
+ "[]", "[]", 0.8, 0.5, 1, 0, "[]", "",
324
+ "active", 0.0, 0.0, "factual", created or now),
325
+ )
326
+ converted += 1
327
+ conn.commit()
328
+ stats["steps"].append(f"Converted {converted} V2 memories to V3 facts")
329
+ except Exception as exc:
330
+ stats["steps"].append(f"V2 conversion partial: {exc}")
331
+
279
332
  conn.close()
280
- stats["steps"].append(f"Extended schema ({len(V3_TABLES_SQL)} tables, {len(V3_INDEXES_SQL)} indexes)")
333
+ stats["steps"].append("Created V3 schema")
281
334
 
282
335
  # Step 5: Symlink (only if .claude-memory is not already a symlink)
283
336
  if not self._v2_base.is_symlink():