superlocalmemory 3.0.3 → 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.
|
|
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",
|
package/scripts/postinstall.js
CHANGED
|
@@ -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
|
-
//
|
|
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
|
-
//
|
|
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
|
-
|
|
179
|
-
"sentence-transformers
|
|
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
|
-
|
|
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
|
|
71
|
+
# Stage 2: Similarity-based deduplication (requires embeddings)
|
|
72
|
+
if self._embedder is not None:
|
|
73
73
|
emb = self._embedder.embed(content)
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
self._recent_embeddings.
|
|
85
|
-
|
|
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,50 @@ 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
|
-
|
|
266
|
-
|
|
267
|
-
|
|
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
|
|
279
305
|
conn.close()
|
|
280
|
-
stats["steps"].append(
|
|
306
|
+
stats["steps"].append("Created V3 schema")
|
|
281
307
|
|
|
282
308
|
# Step 5: Symlink (only if .claude-memory is not already a symlink)
|
|
283
309
|
if not self._v2_base.is_symlink():
|