superlocalmemory 2.8.2 → 2.8.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.
Files changed (73) hide show
  1. package/ATTRIBUTION.md +1 -1
  2. package/CHANGELOG.md +17 -0
  3. package/README.md +7 -5
  4. package/api_server.py +5 -0
  5. package/bin/slm +35 -0
  6. package/bin/slm.bat +3 -3
  7. package/docs/SECURITY-QUICK-REFERENCE.md +214 -0
  8. package/install.ps1 +11 -11
  9. package/mcp_server.py +78 -10
  10. package/package.json +2 -2
  11. package/requirements-core.txt +16 -18
  12. package/requirements-learning.txt +8 -8
  13. package/requirements.txt +9 -7
  14. package/scripts/prepack.js +33 -0
  15. package/scripts/verify-v27.ps1 +301 -0
  16. package/src/agent_registry.py +32 -28
  17. package/src/auto_backup.py +12 -6
  18. package/src/cache_manager.py +2 -2
  19. package/src/compression/__init__.py +25 -0
  20. package/src/compression/cli.py +150 -0
  21. package/src/compression/cold_storage.py +217 -0
  22. package/src/compression/config.py +72 -0
  23. package/src/compression/orchestrator.py +133 -0
  24. package/src/compression/tier2_compressor.py +228 -0
  25. package/src/compression/tier3_compressor.py +153 -0
  26. package/src/compression/tier_classifier.py +148 -0
  27. package/src/db_connection_manager.py +5 -5
  28. package/src/event_bus.py +24 -22
  29. package/src/hnsw_index.py +3 -3
  30. package/src/learning/__init__.py +5 -4
  31. package/src/learning/adaptive_ranker.py +14 -265
  32. package/src/learning/bootstrap/__init__.py +69 -0
  33. package/src/learning/bootstrap/constants.py +93 -0
  34. package/src/learning/bootstrap/db_queries.py +316 -0
  35. package/src/learning/bootstrap/sampling.py +82 -0
  36. package/src/learning/bootstrap/text_utils.py +71 -0
  37. package/src/learning/cross_project_aggregator.py +58 -57
  38. package/src/learning/db/__init__.py +40 -0
  39. package/src/learning/db/constants.py +44 -0
  40. package/src/learning/db/schema.py +279 -0
  41. package/src/learning/learning_db.py +15 -234
  42. package/src/learning/ranking/__init__.py +33 -0
  43. package/src/learning/ranking/constants.py +84 -0
  44. package/src/learning/ranking/helpers.py +278 -0
  45. package/src/learning/source_quality_scorer.py +66 -65
  46. package/src/learning/synthetic_bootstrap.py +28 -310
  47. package/src/memory/__init__.py +36 -0
  48. package/src/memory/cli.py +205 -0
  49. package/src/memory/constants.py +39 -0
  50. package/src/memory/helpers.py +28 -0
  51. package/src/memory/schema.py +166 -0
  52. package/src/memory-profiles.py +94 -86
  53. package/src/memory-reset.py +187 -185
  54. package/src/memory_compression.py +2 -2
  55. package/src/memory_store_v2.py +44 -354
  56. package/src/migrate_v1_to_v2.py +11 -10
  57. package/src/patterns/analyzers.py +104 -100
  58. package/src/patterns/learner.py +17 -13
  59. package/src/patterns/scoring.py +25 -21
  60. package/src/patterns/store.py +40 -38
  61. package/src/patterns/terminology.py +53 -51
  62. package/src/provenance_tracker.py +2 -2
  63. package/src/qualixar_attribution.py +1 -1
  64. package/src/search/engine.py +16 -14
  65. package/src/search/index_loader.py +13 -11
  66. package/src/setup_validator.py +160 -158
  67. package/src/subscription_manager.py +20 -18
  68. package/src/tree/builder.py +66 -64
  69. package/src/tree/nodes.py +103 -97
  70. package/src/tree/queries.py +142 -137
  71. package/src/tree/schema.py +46 -42
  72. package/src/webhook_dispatcher.py +3 -3
  73. package/ui_server.py +7 -4
@@ -0,0 +1,28 @@
1
+ #!/usr/bin/env python3
2
+ # SPDX-License-Identifier: MIT
3
+ # Copyright (c) 2026 SuperLocalMemory (superlocalmemory.com)
4
+ """
5
+ Helper utilities for MemoryStoreV2.
6
+
7
+ This module contains standalone utility functions extracted from memory_store_v2.py
8
+ to reduce file size and improve maintainability.
9
+ """
10
+
11
+
12
+ def format_content(content: str, full: bool = False, threshold: int = 5000, preview_len: int = 2000) -> str:
13
+ """
14
+ Smart content formatting with optional truncation.
15
+
16
+ Args:
17
+ content: Content to format
18
+ full: If True, always show full content
19
+ threshold: Max length before truncation (default 5000)
20
+ preview_len: Preview length when truncating (default 2000)
21
+
22
+ Returns:
23
+ Formatted content string
24
+ """
25
+ if full or len(content) < threshold:
26
+ return content
27
+ else:
28
+ return f"{content[:preview_len]}..."
@@ -0,0 +1,166 @@
1
+ #!/usr/bin/env python3
2
+ # SPDX-License-Identifier: MIT
3
+ # Copyright (c) 2026 SuperLocalMemory (superlocalmemory.com)
4
+ """
5
+ Database schema definitions for MemoryStoreV2.
6
+
7
+ This module contains SQL schema definitions and migration logic extracted from
8
+ memory_store_v2.py to reduce file size and improve maintainability.
9
+ """
10
+
11
+ from typing import Dict, List, Tuple
12
+
13
+
14
+ # V2 column definitions for migration support
15
+ V2_COLUMNS: Dict[str, str] = {
16
+ 'summary': 'TEXT',
17
+ 'project_path': 'TEXT',
18
+ 'project_name': 'TEXT',
19
+ 'category': 'TEXT',
20
+ 'parent_id': 'INTEGER',
21
+ 'tree_path': 'TEXT',
22
+ 'depth': 'INTEGER DEFAULT 0',
23
+ 'memory_type': 'TEXT DEFAULT "session"',
24
+ 'importance': 'INTEGER DEFAULT 5',
25
+ 'updated_at': 'TIMESTAMP DEFAULT CURRENT_TIMESTAMP',
26
+ 'last_accessed': 'TIMESTAMP',
27
+ 'access_count': 'INTEGER DEFAULT 0',
28
+ 'content_hash': 'TEXT',
29
+ 'cluster_id': 'INTEGER',
30
+ 'profile': 'TEXT DEFAULT "default"'
31
+ }
32
+
33
+ # v2.8.0 schema migrations - lifecycle + access control columns
34
+ V28_MIGRATIONS: List[Tuple[str, str]] = [
35
+ ("lifecycle_state", "TEXT DEFAULT 'active'"),
36
+ ("lifecycle_updated_at", "TIMESTAMP"),
37
+ ("lifecycle_history", "TEXT DEFAULT '[]'"),
38
+ ("access_level", "TEXT DEFAULT 'public'"),
39
+ ]
40
+
41
+ # Index definitions for V2 fields
42
+ V2_INDEXES: List[Tuple[str, str]] = [
43
+ ('idx_project', 'project_path'),
44
+ ('idx_tags', 'tags'),
45
+ ('idx_category', 'category'),
46
+ ('idx_tree_path', 'tree_path'),
47
+ ('idx_cluster', 'cluster_id'),
48
+ ('idx_last_accessed', 'last_accessed'),
49
+ ('idx_parent_id', 'parent_id'),
50
+ ('idx_profile', 'profile')
51
+ ]
52
+
53
+
54
+ def get_memories_table_sql() -> str:
55
+ """
56
+ Returns the CREATE TABLE SQL for the main memories table.
57
+ V1 compatible + V2 extensions.
58
+ """
59
+ return '''
60
+ CREATE TABLE IF NOT EXISTS memories (
61
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
62
+ content TEXT NOT NULL,
63
+ summary TEXT,
64
+
65
+ -- Organization
66
+ project_path TEXT,
67
+ project_name TEXT,
68
+ tags TEXT,
69
+ category TEXT,
70
+
71
+ -- Hierarchy (Layer 2 link)
72
+ parent_id INTEGER,
73
+ tree_path TEXT,
74
+ depth INTEGER DEFAULT 0,
75
+
76
+ -- Metadata
77
+ memory_type TEXT DEFAULT 'session',
78
+ importance INTEGER DEFAULT 5,
79
+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
80
+ updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
81
+ last_accessed TIMESTAMP,
82
+ access_count INTEGER DEFAULT 0,
83
+
84
+ -- Deduplication
85
+ content_hash TEXT UNIQUE,
86
+
87
+ -- Graph (Layer 3 link)
88
+ cluster_id INTEGER,
89
+
90
+ FOREIGN KEY (parent_id) REFERENCES memories(id) ON DELETE CASCADE
91
+ )
92
+ '''
93
+
94
+
95
+ def get_sessions_table_sql() -> str:
96
+ """
97
+ Returns the CREATE TABLE SQL for the sessions table.
98
+ V1 compatible.
99
+ """
100
+ return '''
101
+ CREATE TABLE IF NOT EXISTS sessions (
102
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
103
+ session_id TEXT UNIQUE,
104
+ project_path TEXT,
105
+ started_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
106
+ ended_at TIMESTAMP,
107
+ summary TEXT
108
+ )
109
+ '''
110
+
111
+
112
+ def get_fts_table_sql() -> str:
113
+ """
114
+ Returns the CREATE VIRTUAL TABLE SQL for full-text search.
115
+ V1 compatible.
116
+ """
117
+ return '''
118
+ CREATE VIRTUAL TABLE IF NOT EXISTS memories_fts
119
+ USING fts5(content, summary, tags, content='memories', content_rowid='id')
120
+ '''
121
+
122
+
123
+ def get_fts_trigger_insert_sql() -> str:
124
+ """Returns the FTS INSERT trigger SQL."""
125
+ return '''
126
+ CREATE TRIGGER IF NOT EXISTS memories_ai AFTER INSERT ON memories BEGIN
127
+ INSERT INTO memories_fts(rowid, content, summary, tags)
128
+ VALUES (new.id, new.content, new.summary, new.tags);
129
+ END
130
+ '''
131
+
132
+
133
+ def get_fts_trigger_delete_sql() -> str:
134
+ """Returns the FTS DELETE trigger SQL."""
135
+ return '''
136
+ CREATE TRIGGER IF NOT EXISTS memories_ad AFTER DELETE ON memories BEGIN
137
+ INSERT INTO memories_fts(memories_fts, rowid, content, summary, tags)
138
+ VALUES('delete', old.id, old.content, old.summary, old.tags);
139
+ END
140
+ '''
141
+
142
+
143
+ def get_fts_trigger_update_sql() -> str:
144
+ """Returns the FTS UPDATE trigger SQL."""
145
+ return '''
146
+ CREATE TRIGGER IF NOT EXISTS memories_au AFTER UPDATE ON memories BEGIN
147
+ INSERT INTO memories_fts(memories_fts, rowid, content, summary, tags)
148
+ VALUES('delete', old.id, old.content, old.summary, old.tags);
149
+ INSERT INTO memories_fts(rowid, content, summary, tags)
150
+ VALUES (new.id, new.content, new.summary, new.tags);
151
+ END
152
+ '''
153
+
154
+
155
+ def get_creator_metadata_table_sql() -> str:
156
+ """
157
+ Returns the CREATE TABLE SQL for creator attribution metadata.
158
+ REQUIRED by MIT License.
159
+ """
160
+ return '''
161
+ CREATE TABLE IF NOT EXISTS creator_metadata (
162
+ key TEXT PRIMARY KEY,
163
+ value TEXT NOT NULL,
164
+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
165
+ )
166
+ '''
@@ -128,74 +128,74 @@ class ProfileManager:
128
128
  main_cursor = main_conn.cursor()
129
129
 
130
130
  old_conn = sqlite3.connect(old_db_path)
131
- old_cursor = old_conn.cursor()
132
-
133
- # Get existing hashes
134
- main_cursor.execute("SELECT content_hash FROM memories WHERE content_hash IS NOT NULL")
135
- existing_hashes = {row[0] for row in main_cursor.fetchall()}
136
-
137
- # Get columns from old DB
138
- old_cursor.execute("PRAGMA table_info(memories)")
139
- old_columns = {row[1] for row in old_cursor.fetchall()}
140
-
141
- # Build SELECT based on available columns
142
- select_cols = ['content', 'summary', 'project_path', 'project_name', 'tags',
143
- 'category', 'memory_type', 'importance', 'created_at', 'updated_at',
144
- 'content_hash']
145
- available_cols = [c for c in select_cols if c in old_columns]
146
-
147
- if 'content' not in available_cols:
131
+ try:
132
+ old_cursor = old_conn.cursor()
133
+
134
+ # Get existing hashes
135
+ main_cursor.execute("SELECT content_hash FROM memories WHERE content_hash IS NOT NULL")
136
+ existing_hashes = {row[0] for row in main_cursor.fetchall()}
137
+
138
+ # Get columns from old DB
139
+ old_cursor.execute("PRAGMA table_info(memories)")
140
+ old_columns = {row[1] for row in old_cursor.fetchall()}
141
+
142
+ # Build SELECT based on available columns
143
+ select_cols = ['content', 'summary', 'project_path', 'project_name', 'tags',
144
+ 'category', 'memory_type', 'importance', 'created_at', 'updated_at',
145
+ 'content_hash']
146
+ available_cols = [c for c in select_cols if c in old_columns]
147
+
148
+ if 'content' not in available_cols:
149
+ return
150
+
151
+ old_cursor.execute(f"SELECT {', '.join(available_cols)} FROM memories")
152
+ rows = old_cursor.fetchall()
153
+
154
+ imported = 0
155
+ for row in rows:
156
+ row_dict = dict(zip(available_cols, row))
157
+ content = row_dict.get('content', '')
158
+ content_hash = row_dict.get('content_hash')
159
+
160
+ if not content:
161
+ continue
162
+
163
+ # Generate hash if missing
164
+ if not content_hash:
165
+ content_hash = hashlib.sha256(content.encode()).hexdigest()[:32]
166
+
167
+ if content_hash in existing_hashes:
168
+ continue
169
+
170
+ try:
171
+ main_cursor.execute('''
172
+ INSERT INTO memories (content, summary, project_path, project_name, tags,
173
+ category, memory_type, importance, created_at, updated_at,
174
+ content_hash, profile)
175
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
176
+ ''', (
177
+ content,
178
+ row_dict.get('summary'),
179
+ row_dict.get('project_path'),
180
+ row_dict.get('project_name'),
181
+ row_dict.get('tags'),
182
+ row_dict.get('category'),
183
+ row_dict.get('memory_type', 'session'),
184
+ row_dict.get('importance', 5),
185
+ row_dict.get('created_at'),
186
+ row_dict.get('updated_at'),
187
+ content_hash,
188
+ profile_name
189
+ ))
190
+ imported += 1
191
+ existing_hashes.add(content_hash)
192
+ except sqlite3.IntegrityError:
193
+ pass
194
+
195
+ main_conn.commit()
196
+ finally:
148
197
  old_conn.close()
149
198
  main_conn.close()
150
- return
151
-
152
- old_cursor.execute(f"SELECT {', '.join(available_cols)} FROM memories")
153
- rows = old_cursor.fetchall()
154
-
155
- imported = 0
156
- for row in rows:
157
- row_dict = dict(zip(available_cols, row))
158
- content = row_dict.get('content', '')
159
- content_hash = row_dict.get('content_hash')
160
-
161
- if not content:
162
- continue
163
-
164
- # Generate hash if missing
165
- if not content_hash:
166
- content_hash = hashlib.sha256(content.encode()).hexdigest()[:32]
167
-
168
- if content_hash in existing_hashes:
169
- continue
170
-
171
- try:
172
- main_cursor.execute('''
173
- INSERT INTO memories (content, summary, project_path, project_name, tags,
174
- category, memory_type, importance, created_at, updated_at,
175
- content_hash, profile)
176
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
177
- ''', (
178
- content,
179
- row_dict.get('summary'),
180
- row_dict.get('project_path'),
181
- row_dict.get('project_name'),
182
- row_dict.get('tags'),
183
- row_dict.get('category'),
184
- row_dict.get('memory_type', 'session'),
185
- row_dict.get('importance', 5),
186
- row_dict.get('created_at'),
187
- row_dict.get('updated_at'),
188
- content_hash,
189
- profile_name
190
- ))
191
- imported += 1
192
- existing_hashes.add(content_hash)
193
- except sqlite3.IntegrityError:
194
- pass
195
-
196
- main_conn.commit()
197
- old_conn.close()
198
- main_conn.close()
199
199
 
200
200
  if imported > 0:
201
201
  # Add profile to config if not present
@@ -257,17 +257,19 @@ class ProfileManager:
257
257
  return 0
258
258
 
259
259
  conn = sqlite3.connect(self.db_path)
260
- cursor = conn.cursor()
261
- cursor.execute("SELECT COUNT(*) FROM memories WHERE profile = ?", (profile_name,))
262
- count = cursor.fetchone()[0]
263
- conn.close()
260
+ try:
261
+ cursor = conn.cursor()
262
+ cursor.execute("SELECT COUNT(*) FROM memories WHERE profile = ?", (profile_name,))
263
+ count = cursor.fetchone()[0]
264
+ finally:
265
+ conn.close()
264
266
  return count
265
267
 
266
- def get_active_profile(self):
268
+ def get_active_profile(self) -> str:
267
269
  """Get the currently active profile name."""
268
270
  return self.config.get('active_profile', 'default')
269
271
 
270
- def list_profiles(self):
272
+ def list_profiles(self) -> list:
271
273
  """List all available profiles with memory counts."""
272
274
  print("\n" + "=" * 60)
273
275
  print("AVAILABLE MEMORY PROFILES")
@@ -410,11 +412,13 @@ class ProfileManager:
410
412
  # Move memories to default profile
411
413
  if self.db_path.exists() and count > 0:
412
414
  conn = sqlite3.connect(self.db_path)
413
- cursor = conn.cursor()
414
- cursor.execute("UPDATE memories SET profile = 'default' WHERE profile = ?", (name,))
415
- moved = cursor.rowcount
416
- conn.commit()
417
- conn.close()
415
+ try:
416
+ cursor = conn.cursor()
417
+ cursor.execute("UPDATE memories SET profile = 'default' WHERE profile = ?", (name,))
418
+ moved = cursor.rowcount
419
+ conn.commit()
420
+ finally:
421
+ conn.close()
418
422
  print(f" Moved {moved} memories to 'default' profile")
419
423
 
420
424
  # Remove from config
@@ -445,10 +449,12 @@ class ProfileManager:
445
449
  # Show total memories across all profiles
446
450
  if self.db_path.exists():
447
451
  conn = sqlite3.connect(self.db_path)
448
- cursor = conn.cursor()
449
- cursor.execute("SELECT COUNT(*) FROM memories")
450
- total = cursor.fetchone()[0]
451
- conn.close()
452
+ try:
453
+ cursor = conn.cursor()
454
+ cursor.execute("SELECT COUNT(*) FROM memories")
455
+ total = cursor.fetchone()[0]
456
+ finally:
457
+ conn.close()
452
458
  print(f"Total memories (all profiles): {total}")
453
459
  else:
454
460
  print(f"Warning: Current profile '{active}' not found in config")
@@ -475,11 +481,13 @@ class ProfileManager:
475
481
  # Update profile column in all memories
476
482
  if self.db_path.exists():
477
483
  conn = sqlite3.connect(self.db_path)
478
- cursor = conn.cursor()
479
- cursor.execute("UPDATE memories SET profile = ? WHERE profile = ?", (new_name, old_name))
480
- updated = cursor.rowcount
481
- conn.commit()
482
- conn.close()
484
+ try:
485
+ cursor = conn.cursor()
486
+ cursor.execute("UPDATE memories SET profile = ? WHERE profile = ?", (new_name, old_name))
487
+ updated = cursor.rowcount
488
+ conn.commit()
489
+ finally:
490
+ conn.close()
483
491
  print(f" Updated {updated} memories")
484
492
 
485
493
  # Update config