superlocalmemory 2.8.1 → 2.8.3

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 (90) hide show
  1. package/ATTRIBUTION.md +50 -0
  2. package/CHANGELOG.md +8 -0
  3. package/README.md +31 -20
  4. package/api_server.py +5 -0
  5. package/bin/aider-smart +2 -2
  6. package/bin/slm +18 -18
  7. package/bin/slm.bat +3 -3
  8. package/configs/continue-skills.yaml +4 -4
  9. package/docs/ARCHITECTURE.md +3 -3
  10. package/docs/CLI-COMMANDS-REFERENCE.md +18 -18
  11. package/docs/FRAMEWORK-INTEGRATIONS.md +4 -4
  12. package/docs/SECURITY-QUICK-REFERENCE.md +214 -0
  13. package/docs/UNIVERSAL-INTEGRATION.md +15 -15
  14. package/install.ps1 +11 -11
  15. package/install.sh +4 -4
  16. package/mcp_server.py +4 -4
  17. package/package.json +5 -3
  18. package/requirements-core.txt +16 -18
  19. package/requirements-learning.txt +8 -8
  20. package/requirements.txt +9 -7
  21. package/scripts/prepack.js +33 -0
  22. package/scripts/verify-v27.ps1 +301 -0
  23. package/src/agent_registry.py +32 -28
  24. package/src/auto_backup.py +12 -6
  25. package/src/cache_manager.py +2 -2
  26. package/src/compression/__init__.py +25 -0
  27. package/src/compression/cli.py +150 -0
  28. package/src/compression/cold_storage.py +217 -0
  29. package/src/compression/config.py +72 -0
  30. package/src/compression/orchestrator.py +133 -0
  31. package/src/compression/tier2_compressor.py +228 -0
  32. package/src/compression/tier3_compressor.py +153 -0
  33. package/src/compression/tier_classifier.py +148 -0
  34. package/src/db_connection_manager.py +5 -5
  35. package/src/event_bus.py +24 -22
  36. package/src/graph/graph_core.py +3 -3
  37. package/src/hnsw_index.py +3 -3
  38. package/src/learning/__init__.py +5 -4
  39. package/src/learning/adaptive_ranker.py +14 -265
  40. package/src/learning/bootstrap/__init__.py +69 -0
  41. package/src/learning/bootstrap/constants.py +93 -0
  42. package/src/learning/bootstrap/db_queries.py +316 -0
  43. package/src/learning/bootstrap/sampling.py +82 -0
  44. package/src/learning/bootstrap/text_utils.py +71 -0
  45. package/src/learning/cross_project_aggregator.py +58 -57
  46. package/src/learning/db/__init__.py +40 -0
  47. package/src/learning/db/constants.py +44 -0
  48. package/src/learning/db/schema.py +279 -0
  49. package/src/learning/learning_db.py +15 -234
  50. package/src/learning/ranking/__init__.py +33 -0
  51. package/src/learning/ranking/constants.py +84 -0
  52. package/src/learning/ranking/helpers.py +278 -0
  53. package/src/learning/source_quality_scorer.py +66 -65
  54. package/src/learning/synthetic_bootstrap.py +28 -310
  55. package/src/memory/__init__.py +36 -0
  56. package/src/memory/cli.py +205 -0
  57. package/src/memory/constants.py +39 -0
  58. package/src/memory/helpers.py +28 -0
  59. package/src/memory/schema.py +166 -0
  60. package/src/memory-profiles.py +94 -86
  61. package/src/memory-reset.py +187 -185
  62. package/src/memory_compression.py +2 -2
  63. package/src/memory_store_v2.py +40 -355
  64. package/src/migrate_v1_to_v2.py +11 -10
  65. package/src/patterns/analyzers.py +104 -100
  66. package/src/patterns/learner.py +17 -13
  67. package/src/patterns/scoring.py +25 -21
  68. package/src/patterns/store.py +40 -38
  69. package/src/patterns/terminology.py +53 -51
  70. package/src/provenance_tracker.py +2 -2
  71. package/src/qualixar_attribution.py +139 -0
  72. package/src/qualixar_watermark.py +78 -0
  73. package/src/search/engine.py +16 -14
  74. package/src/search/index_loader.py +13 -11
  75. package/src/setup_validator.py +162 -160
  76. package/src/subscription_manager.py +20 -18
  77. package/src/tree/builder.py +66 -64
  78. package/src/tree/nodes.py +103 -97
  79. package/src/tree/queries.py +142 -137
  80. package/src/tree/schema.py +46 -42
  81. package/src/webhook_dispatcher.py +3 -3
  82. package/ui_server.py +7 -4
  83. /package/bin/{superlocalmemoryv2:learning → superlocalmemoryv2-learning} +0 -0
  84. /package/bin/{superlocalmemoryv2:list → superlocalmemoryv2-list} +0 -0
  85. /package/bin/{superlocalmemoryv2:patterns → superlocalmemoryv2-patterns} +0 -0
  86. /package/bin/{superlocalmemoryv2:profile → superlocalmemoryv2-profile} +0 -0
  87. /package/bin/{superlocalmemoryv2:recall → superlocalmemoryv2-recall} +0 -0
  88. /package/bin/{superlocalmemoryv2:remember → superlocalmemoryv2-remember} +0 -0
  89. /package/bin/{superlocalmemoryv2:reset → superlocalmemoryv2-reset} +0 -0
  90. /package/bin/{superlocalmemoryv2:status → superlocalmemoryv2-status} +0 -0
@@ -53,7 +53,7 @@ class SubscriptionManager:
53
53
  return cls._instances[key]
54
54
 
55
55
  @classmethod
56
- def reset_instance(cls, db_path: Optional[Path] = None):
56
+ def reset_instance(cls, db_path: Optional[Path] = None) -> None:
57
57
  """Remove singleton. Used for testing."""
58
58
  with cls._instances_lock:
59
59
  if db_path is None:
@@ -103,22 +103,24 @@ class SubscriptionManager:
103
103
  except ImportError:
104
104
  import sqlite3
105
105
  conn = sqlite3.connect(str(self.db_path))
106
- conn.execute('''
107
- CREATE TABLE IF NOT EXISTS subscriptions (
108
- id INTEGER PRIMARY KEY AUTOINCREMENT,
109
- subscriber_id TEXT NOT NULL UNIQUE,
110
- channel TEXT NOT NULL,
111
- filter TEXT NOT NULL DEFAULT '{}',
112
- webhook_url TEXT,
113
- durable INTEGER DEFAULT 1,
114
- last_event_id INTEGER DEFAULT 0,
115
- created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
116
- updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
117
- )
118
- ''')
119
- conn.execute('CREATE INDEX IF NOT EXISTS idx_subs_channel ON subscriptions(channel)')
120
- conn.commit()
121
- conn.close()
106
+ try:
107
+ conn.execute('''
108
+ CREATE TABLE IF NOT EXISTS subscriptions (
109
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
110
+ subscriber_id TEXT NOT NULL UNIQUE,
111
+ channel TEXT NOT NULL,
112
+ filter TEXT NOT NULL DEFAULT '{}',
113
+ webhook_url TEXT,
114
+ durable INTEGER DEFAULT 1,
115
+ last_event_id INTEGER DEFAULT 0,
116
+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
117
+ updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
118
+ )
119
+ ''')
120
+ conn.execute('CREATE INDEX IF NOT EXISTS idx_subs_channel ON subscriptions(channel)')
121
+ conn.commit()
122
+ finally:
123
+ conn.close()
122
124
 
123
125
  # =========================================================================
124
126
  # Subscribe / Unsubscribe
@@ -244,7 +246,7 @@ class SubscriptionManager:
244
246
 
245
247
  return removed
246
248
 
247
- def update_last_event_id(self, subscriber_id: str, event_id: int):
249
+ def update_last_event_id(self, subscriber_id: str, event_id: int) -> None:
248
250
  """Update the last event ID received by a durable subscriber (for replay)."""
249
251
  try:
250
252
  from db_connection_manager import DbConnectionManager
@@ -22,70 +22,72 @@ class TreeBuilderMixin:
22
22
  5. Update aggregated counts
23
23
  """
24
24
  conn = sqlite3.connect(self.db_path)
25
- cursor = conn.cursor()
26
-
27
- # Clear existing tree (keep root)
28
- cursor.execute('DELETE FROM memory_tree WHERE node_type != ?', ('root',))
29
-
30
- # Step 1: Create project nodes
31
- cursor.execute('''
32
- SELECT DISTINCT project_path, project_name
33
- FROM memories
34
- WHERE project_path IS NOT NULL
35
- ORDER BY project_path
36
- ''')
37
- projects = cursor.fetchall()
38
-
39
- project_map = {} # project_path -> node_id
40
-
41
- for project_path, project_name in projects:
42
- name = project_name or project_path.split('/')[-1]
43
- node_id = self.add_node('project', name, self.root_id, description=project_path)
44
- project_map[project_path] = node_id
45
-
46
- # Step 2: Create category nodes within projects
47
- cursor.execute('''
48
- SELECT DISTINCT project_path, category
49
- FROM memories
50
- WHERE project_path IS NOT NULL AND category IS NOT NULL
51
- ORDER BY project_path, category
52
- ''')
53
- categories = cursor.fetchall()
54
-
55
- category_map = {} # (project_path, category) -> node_id
56
-
57
- for project_path, category in categories:
58
- parent_id = project_map.get(project_path)
59
- if parent_id:
60
- node_id = self.add_node('category', category, parent_id)
61
- category_map[(project_path, category)] = node_id
62
-
63
- # Step 3: Link memories as leaf nodes
64
- cursor.execute('''
65
- SELECT id, content, summary, project_path, category, importance, created_at
66
- FROM memories
67
- ORDER BY created_at DESC
68
- ''')
69
- memories = cursor.fetchall()
70
-
71
- for mem_id, content, summary, project_path, category, importance, created_at in memories:
72
- # Determine parent node
73
- if project_path and category and (project_path, category) in category_map:
74
- parent_id = category_map[(project_path, category)]
75
- elif project_path and project_path in project_map:
76
- parent_id = project_map[project_path]
77
- else:
78
- parent_id = self.root_id
79
-
80
- # Create memory node
81
- name = summary or content[:60].replace('\n', ' ')
82
- self.add_node('memory', name, parent_id, memory_id=mem_id, description=content[:200])
83
-
84
- # Step 4: Update aggregated counts
85
- self._update_all_counts()
86
-
87
- conn.commit()
88
- conn.close()
25
+ try:
26
+ cursor = conn.cursor()
27
+
28
+ # Clear existing tree (keep root)
29
+ cursor.execute('DELETE FROM memory_tree WHERE node_type != ?', ('root',))
30
+
31
+ # Step 1: Create project nodes
32
+ cursor.execute('''
33
+ SELECT DISTINCT project_path, project_name
34
+ FROM memories
35
+ WHERE project_path IS NOT NULL
36
+ ORDER BY project_path
37
+ ''')
38
+ projects = cursor.fetchall()
39
+
40
+ project_map = {} # project_path -> node_id
41
+
42
+ for project_path, project_name in projects:
43
+ name = project_name or project_path.split('/')[-1]
44
+ node_id = self.add_node('project', name, self.root_id, description=project_path)
45
+ project_map[project_path] = node_id
46
+
47
+ # Step 2: Create category nodes within projects
48
+ cursor.execute('''
49
+ SELECT DISTINCT project_path, category
50
+ FROM memories
51
+ WHERE project_path IS NOT NULL AND category IS NOT NULL
52
+ ORDER BY project_path, category
53
+ ''')
54
+ categories = cursor.fetchall()
55
+
56
+ category_map = {} # (project_path, category) -> node_id
57
+
58
+ for project_path, category in categories:
59
+ parent_id = project_map.get(project_path)
60
+ if parent_id:
61
+ node_id = self.add_node('category', category, parent_id)
62
+ category_map[(project_path, category)] = node_id
63
+
64
+ # Step 3: Link memories as leaf nodes
65
+ cursor.execute('''
66
+ SELECT id, content, summary, project_path, category, importance, created_at
67
+ FROM memories
68
+ ORDER BY created_at DESC
69
+ ''')
70
+ memories = cursor.fetchall()
71
+
72
+ for mem_id, content, summary, project_path, category, importance, created_at in memories:
73
+ # Determine parent node
74
+ if project_path and category and (project_path, category) in category_map:
75
+ parent_id = category_map[(project_path, category)]
76
+ elif project_path and project_path in project_map:
77
+ parent_id = project_map[project_path]
78
+ else:
79
+ parent_id = self.root_id
80
+
81
+ # Create memory node
82
+ name = summary or content[:60].replace('\n', ' ')
83
+ self.add_node('memory', name, parent_id, memory_id=mem_id, description=content[:200])
84
+
85
+ # Step 4: Update aggregated counts
86
+ self._update_all_counts()
87
+
88
+ conn.commit()
89
+ finally:
90
+ conn.close()
89
91
 
90
92
 
91
93
  def run_cli():
package/src/tree/nodes.py CHANGED
@@ -35,46 +35,48 @@ class TreeNodesMixin:
35
35
  New node ID
36
36
  """
37
37
  conn = sqlite3.connect(self.db_path)
38
- cursor = conn.cursor()
39
-
40
- # Get parent path and depth
41
- cursor.execute('SELECT tree_path, depth FROM memory_tree WHERE id = ?', (parent_id,))
42
- result = cursor.fetchone()
43
-
44
- if not result:
45
- raise ValueError(f"Parent node {parent_id} not found")
46
-
47
- parent_path, parent_depth = result
48
-
49
- # Calculate new node position
50
- depth = parent_depth + 1
51
-
52
- cursor.execute('''
53
- INSERT INTO memory_tree (
54
- node_type, name, description,
55
- parent_id, tree_path, depth,
56
- memory_id, last_updated
57
- )
58
- VALUES (?, ?, ?, ?, ?, ?, ?, ?)
59
- ''', (
60
- node_type,
61
- name,
62
- description,
63
- parent_id,
64
- '', # Placeholder, updated below
65
- depth,
66
- memory_id,
67
- datetime.now().isoformat()
68
- ))
69
-
70
- node_id = cursor.lastrowid
71
-
72
- # Update tree_path with actual node_id
73
- tree_path = f"{parent_path}.{node_id}"
74
- cursor.execute('UPDATE memory_tree SET tree_path = ? WHERE id = ?', (tree_path, node_id))
75
-
76
- conn.commit()
77
- conn.close()
38
+ try:
39
+ cursor = conn.cursor()
40
+
41
+ # Get parent path and depth
42
+ cursor.execute('SELECT tree_path, depth FROM memory_tree WHERE id = ?', (parent_id,))
43
+ result = cursor.fetchone()
44
+
45
+ if not result:
46
+ raise ValueError(f"Parent node {parent_id} not found")
47
+
48
+ parent_path, parent_depth = result
49
+
50
+ # Calculate new node position
51
+ depth = parent_depth + 1
52
+
53
+ cursor.execute('''
54
+ INSERT INTO memory_tree (
55
+ node_type, name, description,
56
+ parent_id, tree_path, depth,
57
+ memory_id, last_updated
58
+ )
59
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?)
60
+ ''', (
61
+ node_type,
62
+ name,
63
+ description,
64
+ parent_id,
65
+ '', # Placeholder, updated below
66
+ depth,
67
+ memory_id,
68
+ datetime.now().isoformat()
69
+ ))
70
+
71
+ node_id = cursor.lastrowid
72
+
73
+ # Update tree_path with actual node_id
74
+ tree_path = f"{parent_path}.{node_id}"
75
+ cursor.execute('UPDATE memory_tree SET tree_path = ? WHERE id = ?', (tree_path, node_id))
76
+
77
+ conn.commit()
78
+ finally:
79
+ conn.close()
78
80
 
79
81
  return node_id
80
82
 
@@ -92,25 +94,26 @@ class TreeNodesMixin:
92
94
  raise ValueError("Cannot delete root node")
93
95
 
94
96
  conn = sqlite3.connect(self.db_path)
95
- cursor = conn.cursor()
97
+ try:
98
+ cursor = conn.cursor()
96
99
 
97
- # Get tree_path
98
- cursor.execute('SELECT tree_path, parent_id FROM memory_tree WHERE id = ?', (node_id,))
99
- result = cursor.fetchone()
100
+ # Get tree_path
101
+ cursor.execute('SELECT tree_path, parent_id FROM memory_tree WHERE id = ?', (node_id,))
102
+ result = cursor.fetchone()
100
103
 
101
- if not result:
102
- conn.close()
103
- return False
104
+ if not result:
105
+ return False
104
106
 
105
- tree_path, parent_id = result
107
+ tree_path, parent_id = result
106
108
 
107
- # Delete node and all descendants (CASCADE handles children)
108
- cursor.execute('DELETE FROM memory_tree WHERE id = ? OR tree_path LIKE ?',
109
- (node_id, f"{tree_path}.%"))
109
+ # Delete node and all descendants (CASCADE handles children)
110
+ cursor.execute('DELETE FROM memory_tree WHERE id = ? OR tree_path LIKE ?',
111
+ (node_id, f"{tree_path}.%"))
110
112
 
111
- deleted = cursor.rowcount > 0
112
- conn.commit()
113
- conn.close()
113
+ deleted = cursor.rowcount > 0
114
+ conn.commit()
115
+ finally:
116
+ conn.close()
114
117
 
115
118
  # Update parent counts
116
119
  if deleted and parent_id:
@@ -127,56 +130,59 @@ class TreeNodesMixin:
127
130
  node_id: Node ID to update
128
131
  """
129
132
  conn = sqlite3.connect(self.db_path)
130
- cursor = conn.cursor()
131
-
132
- # Get all descendant memory nodes
133
- cursor.execute('SELECT tree_path FROM memory_tree WHERE id = ?', (node_id,))
134
- result = cursor.fetchone()
135
-
136
- if not result:
133
+ try:
134
+ cursor = conn.cursor()
135
+
136
+ # Get all descendant memory nodes
137
+ cursor.execute('SELECT tree_path FROM memory_tree WHERE id = ?', (node_id,))
138
+ result = cursor.fetchone()
139
+
140
+ if not result:
141
+ return
142
+
143
+ tree_path = result[0]
144
+
145
+ # Count memories in subtree
146
+ cursor.execute('''
147
+ SELECT COUNT(*), COALESCE(SUM(LENGTH(m.content)), 0)
148
+ FROM memory_tree t
149
+ LEFT JOIN memories m ON t.memory_id = m.id
150
+ WHERE t.tree_path LIKE ? AND t.memory_id IS NOT NULL
151
+ ''', (f"{tree_path}%",))
152
+
153
+ memory_count, total_size = cursor.fetchone()
154
+
155
+ # Update node
156
+ cursor.execute('''
157
+ UPDATE memory_tree
158
+ SET memory_count = ?, total_size = ?, last_updated = ?
159
+ WHERE id = ?
160
+ ''', (memory_count, total_size, datetime.now().isoformat(), node_id))
161
+
162
+ # Update all ancestors
163
+ path_ids = [int(x) for x in tree_path.split('.')]
164
+ for ancestor_id in path_ids[:-1]: # Exclude current node
165
+ self.update_counts(ancestor_id)
166
+
167
+ conn.commit()
168
+ finally:
137
169
  conn.close()
138
- return
139
-
140
- tree_path = result[0]
141
-
142
- # Count memories in subtree
143
- cursor.execute('''
144
- SELECT COUNT(*), COALESCE(SUM(LENGTH(m.content)), 0)
145
- FROM memory_tree t
146
- LEFT JOIN memories m ON t.memory_id = m.id
147
- WHERE t.tree_path LIKE ? AND t.memory_id IS NOT NULL
148
- ''', (f"{tree_path}%",))
149
-
150
- memory_count, total_size = cursor.fetchone()
151
-
152
- # Update node
153
- cursor.execute('''
154
- UPDATE memory_tree
155
- SET memory_count = ?, total_size = ?, last_updated = ?
156
- WHERE id = ?
157
- ''', (memory_count, total_size, datetime.now().isoformat(), node_id))
158
-
159
- # Update all ancestors
160
- path_ids = [int(x) for x in tree_path.split('.')]
161
- for ancestor_id in path_ids[:-1]: # Exclude current node
162
- self.update_counts(ancestor_id)
163
-
164
- conn.commit()
165
- conn.close()
166
170
 
167
171
  def _update_all_counts(self):
168
172
  """Update counts for all nodes (used after build_tree)."""
169
173
  conn = sqlite3.connect(self.db_path)
170
- cursor = conn.cursor()
174
+ try:
175
+ cursor = conn.cursor()
171
176
 
172
- # Get all nodes in reverse depth order (leaves first)
173
- cursor.execute('''
174
- SELECT id FROM memory_tree
175
- ORDER BY depth DESC
176
- ''')
177
+ # Get all nodes in reverse depth order (leaves first)
178
+ cursor.execute('''
179
+ SELECT id FROM memory_tree
180
+ ORDER BY depth DESC
181
+ ''')
177
182
 
178
- node_ids = [row[0] for row in cursor.fetchall()]
179
- conn.close()
183
+ node_ids = [row[0] for row in cursor.fetchall()]
184
+ finally:
185
+ conn.close()
180
186
 
181
187
  # Update each node (will cascade to parents)
182
188
  processed = set()