superlocalmemory 2.7.6 → 2.8.0

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 (170) hide show
  1. package/CHANGELOG.md +120 -155
  2. package/README.md +115 -89
  3. package/api_server.py +2 -12
  4. package/docs/PATTERN-LEARNING.md +64 -199
  5. package/docs/example_graph_usage.py +4 -6
  6. package/install.sh +59 -0
  7. package/mcp_server.py +83 -7
  8. package/package.json +1 -8
  9. package/scripts/generate-thumbnails.py +3 -5
  10. package/skills/slm-build-graph/SKILL.md +1 -1
  11. package/skills/slm-list-recent/SKILL.md +1 -1
  12. package/skills/slm-recall/SKILL.md +1 -1
  13. package/skills/slm-remember/SKILL.md +1 -1
  14. package/skills/slm-show-patterns/SKILL.md +1 -1
  15. package/skills/slm-status/SKILL.md +1 -1
  16. package/skills/slm-switch-profile/SKILL.md +1 -1
  17. package/src/agent_registry.py +7 -18
  18. package/src/auth_middleware.py +3 -5
  19. package/src/auto_backup.py +3 -7
  20. package/src/behavioral/__init__.py +49 -0
  21. package/src/behavioral/behavioral_listener.py +203 -0
  22. package/src/behavioral/behavioral_patterns.py +275 -0
  23. package/src/behavioral/cross_project_transfer.py +206 -0
  24. package/src/behavioral/outcome_inference.py +194 -0
  25. package/src/behavioral/outcome_tracker.py +193 -0
  26. package/src/behavioral/tests/__init__.py +4 -0
  27. package/src/behavioral/tests/test_behavioral_integration.py +108 -0
  28. package/src/behavioral/tests/test_behavioral_patterns.py +150 -0
  29. package/src/behavioral/tests/test_cross_project_transfer.py +142 -0
  30. package/src/behavioral/tests/test_mcp_behavioral.py +139 -0
  31. package/src/behavioral/tests/test_mcp_report_outcome.py +117 -0
  32. package/src/behavioral/tests/test_outcome_inference.py +107 -0
  33. package/src/behavioral/tests/test_outcome_tracker.py +96 -0
  34. package/src/cache_manager.py +4 -6
  35. package/src/compliance/__init__.py +48 -0
  36. package/src/compliance/abac_engine.py +149 -0
  37. package/src/compliance/abac_middleware.py +116 -0
  38. package/src/compliance/audit_db.py +215 -0
  39. package/src/compliance/audit_logger.py +148 -0
  40. package/src/compliance/retention_manager.py +289 -0
  41. package/src/compliance/retention_scheduler.py +186 -0
  42. package/src/compliance/tests/__init__.py +4 -0
  43. package/src/compliance/tests/test_abac_enforcement.py +95 -0
  44. package/src/compliance/tests/test_abac_engine.py +124 -0
  45. package/src/compliance/tests/test_abac_mcp_integration.py +118 -0
  46. package/src/compliance/tests/test_audit_db.py +123 -0
  47. package/src/compliance/tests/test_audit_logger.py +98 -0
  48. package/src/compliance/tests/test_mcp_audit.py +128 -0
  49. package/src/compliance/tests/test_mcp_retention_policy.py +125 -0
  50. package/src/compliance/tests/test_retention_manager.py +131 -0
  51. package/src/compliance/tests/test_retention_scheduler.py +99 -0
  52. package/src/db_connection_manager.py +2 -12
  53. package/src/embedding_engine.py +61 -669
  54. package/src/embeddings/__init__.py +47 -0
  55. package/src/embeddings/cache.py +70 -0
  56. package/src/embeddings/cli.py +113 -0
  57. package/src/embeddings/constants.py +47 -0
  58. package/src/embeddings/database.py +91 -0
  59. package/src/embeddings/engine.py +247 -0
  60. package/src/embeddings/model_loader.py +145 -0
  61. package/src/event_bus.py +3 -13
  62. package/src/graph/__init__.py +36 -0
  63. package/src/graph/build_helpers.py +74 -0
  64. package/src/graph/cli.py +87 -0
  65. package/src/graph/cluster_builder.py +188 -0
  66. package/src/graph/cluster_summary.py +148 -0
  67. package/src/graph/constants.py +47 -0
  68. package/src/graph/edge_builder.py +162 -0
  69. package/src/graph/entity_extractor.py +95 -0
  70. package/src/graph/graph_core.py +226 -0
  71. package/src/graph/graph_search.py +231 -0
  72. package/src/graph/hierarchical.py +207 -0
  73. package/src/graph/schema.py +99 -0
  74. package/src/graph_engine.py +45 -1451
  75. package/src/hnsw_index.py +3 -7
  76. package/src/hybrid_search.py +36 -683
  77. package/src/learning/__init__.py +27 -12
  78. package/src/learning/adaptive_ranker.py +50 -12
  79. package/src/learning/cross_project_aggregator.py +2 -12
  80. package/src/learning/engagement_tracker.py +2 -12
  81. package/src/learning/feature_extractor.py +175 -43
  82. package/src/learning/feedback_collector.py +7 -12
  83. package/src/learning/learning_db.py +180 -12
  84. package/src/learning/project_context_manager.py +2 -12
  85. package/src/learning/source_quality_scorer.py +2 -12
  86. package/src/learning/synthetic_bootstrap.py +2 -12
  87. package/src/learning/tests/__init__.py +2 -0
  88. package/src/learning/tests/test_adaptive_ranker.py +2 -6
  89. package/src/learning/tests/test_adaptive_ranker_v28.py +60 -0
  90. package/src/learning/tests/test_aggregator.py +2 -6
  91. package/src/learning/tests/test_auto_retrain_v28.py +35 -0
  92. package/src/learning/tests/test_e2e_ranking_v28.py +82 -0
  93. package/src/learning/tests/test_feature_extractor_v28.py +93 -0
  94. package/src/learning/tests/test_feedback_collector.py +2 -6
  95. package/src/learning/tests/test_learning_db.py +2 -6
  96. package/src/learning/tests/test_learning_db_v28.py +110 -0
  97. package/src/learning/tests/test_learning_init_v28.py +48 -0
  98. package/src/learning/tests/test_outcome_signals.py +48 -0
  99. package/src/learning/tests/test_project_context.py +2 -6
  100. package/src/learning/tests/test_schema_migration.py +319 -0
  101. package/src/learning/tests/test_signal_inference.py +11 -13
  102. package/src/learning/tests/test_source_quality.py +2 -6
  103. package/src/learning/tests/test_synthetic_bootstrap.py +3 -7
  104. package/src/learning/tests/test_workflow_miner.py +2 -6
  105. package/src/learning/workflow_pattern_miner.py +2 -12
  106. package/src/lifecycle/__init__.py +54 -0
  107. package/src/lifecycle/bounded_growth.py +239 -0
  108. package/src/lifecycle/compaction_engine.py +226 -0
  109. package/src/lifecycle/lifecycle_engine.py +302 -0
  110. package/src/lifecycle/lifecycle_evaluator.py +225 -0
  111. package/src/lifecycle/lifecycle_scheduler.py +130 -0
  112. package/src/lifecycle/retention_policy.py +285 -0
  113. package/src/lifecycle/tests/__init__.py +4 -0
  114. package/src/lifecycle/tests/test_bounded_growth.py +193 -0
  115. package/src/lifecycle/tests/test_compaction.py +179 -0
  116. package/src/lifecycle/tests/test_lifecycle_engine.py +137 -0
  117. package/src/lifecycle/tests/test_lifecycle_evaluation.py +177 -0
  118. package/src/lifecycle/tests/test_lifecycle_scheduler.py +127 -0
  119. package/src/lifecycle/tests/test_lifecycle_search.py +109 -0
  120. package/src/lifecycle/tests/test_mcp_compact.py +149 -0
  121. package/src/lifecycle/tests/test_mcp_lifecycle_status.py +114 -0
  122. package/src/lifecycle/tests/test_retention_policy.py +162 -0
  123. package/src/mcp_tools_v28.py +280 -0
  124. package/src/memory-profiles.py +2 -12
  125. package/src/memory-reset.py +2 -12
  126. package/src/memory_compression.py +2 -12
  127. package/src/memory_store_v2.py +76 -20
  128. package/src/migrate_v1_to_v2.py +2 -12
  129. package/src/pattern_learner.py +29 -975
  130. package/src/patterns/__init__.py +24 -0
  131. package/src/patterns/analyzers.py +247 -0
  132. package/src/patterns/learner.py +267 -0
  133. package/src/patterns/scoring.py +167 -0
  134. package/src/patterns/store.py +223 -0
  135. package/src/patterns/terminology.py +138 -0
  136. package/src/provenance_tracker.py +4 -14
  137. package/src/query_optimizer.py +4 -6
  138. package/src/rate_limiter.py +2 -6
  139. package/src/search/__init__.py +20 -0
  140. package/src/search/cli.py +77 -0
  141. package/src/search/constants.py +26 -0
  142. package/src/search/engine.py +239 -0
  143. package/src/search/fusion.py +122 -0
  144. package/src/search/index_loader.py +112 -0
  145. package/src/search/methods.py +162 -0
  146. package/src/search_engine_v2.py +4 -6
  147. package/src/setup_validator.py +7 -13
  148. package/src/subscription_manager.py +2 -12
  149. package/src/tree/__init__.py +59 -0
  150. package/src/tree/builder.py +183 -0
  151. package/src/tree/nodes.py +196 -0
  152. package/src/tree/queries.py +252 -0
  153. package/src/tree/schema.py +76 -0
  154. package/src/tree_manager.py +10 -711
  155. package/src/trust/__init__.py +45 -0
  156. package/src/trust/constants.py +66 -0
  157. package/src/trust/queries.py +157 -0
  158. package/src/trust/schema.py +95 -0
  159. package/src/trust/scorer.py +299 -0
  160. package/src/trust/signals.py +95 -0
  161. package/src/trust_scorer.py +39 -697
  162. package/src/webhook_dispatcher.py +2 -12
  163. package/ui/app.js +1 -1
  164. package/ui/js/agents.js +1 -1
  165. package/ui_server.py +2 -14
  166. package/ATTRIBUTION.md +0 -140
  167. package/docs/ARCHITECTURE-V2.5.md +0 -190
  168. package/docs/GRAPH-ENGINE.md +0 -503
  169. package/docs/architecture-diagram.drawio +0 -405
  170. package/docs/plans/2026-02-13-benchmark-suite.md +0 -1349
@@ -0,0 +1,162 @@
1
+ #!/usr/bin/env python3
2
+ # SPDX-License-Identifier: MIT
3
+ # Copyright (c) 2026 SuperLocalMemory (superlocalmemory.com)
4
+ """Individual search methods (BM25, semantic, graph) for hybrid search.
5
+ """
6
+ from typing import List, Tuple
7
+
8
+
9
+ class SearchMethodsMixin:
10
+ """
11
+ Mixin providing individual search method implementations.
12
+
13
+ Expects the host class to have:
14
+ - self.bm25: BM25SearchEngine
15
+ - self.optimizer: QueryOptimizer
16
+ - self._tfidf_vectorizer
17
+ - self._tfidf_vectors
18
+ - self._memory_ids: list
19
+ - self._load_graph_engine() method
20
+ - self.search_bm25() method (for graph seed)
21
+ """
22
+
23
+ def search_bm25(
24
+ self,
25
+ query: str,
26
+ limit: int = 10,
27
+ score_threshold: float = 0.0
28
+ ) -> List[Tuple[int, float]]:
29
+ """
30
+ Search using BM25 keyword matching.
31
+
32
+ Args:
33
+ query: Search query
34
+ limit: Maximum results
35
+ score_threshold: Minimum score threshold
36
+
37
+ Returns:
38
+ List of (memory_id, score) tuples
39
+ """
40
+ # Optimize query
41
+ optimized = self.optimizer.optimize(
42
+ query,
43
+ enable_spell_correction=True,
44
+ enable_expansion=False # Expansion can hurt precision
45
+ )
46
+
47
+ # Search with BM25
48
+ results = self.bm25.search(optimized, limit, score_threshold)
49
+
50
+ return results
51
+
52
+ def search_semantic(
53
+ self,
54
+ query: str,
55
+ limit: int = 10,
56
+ score_threshold: float = 0.05
57
+ ) -> List[Tuple[int, float]]:
58
+ """
59
+ Search using TF-IDF semantic similarity.
60
+
61
+ Args:
62
+ query: Search query
63
+ limit: Maximum results
64
+ score_threshold: Minimum similarity threshold
65
+
66
+ Returns:
67
+ List of (memory_id, score) tuples
68
+ """
69
+ if self._tfidf_vectorizer is None or self._tfidf_vectors is None:
70
+ return []
71
+
72
+ try:
73
+ from sklearn.metrics.pairwise import cosine_similarity
74
+ import numpy as np
75
+
76
+ # Vectorize query
77
+ query_vec = self._tfidf_vectorizer.transform([query])
78
+
79
+ # Calculate similarities
80
+ similarities = cosine_similarity(query_vec, self._tfidf_vectors).flatten()
81
+
82
+ # Get top results above threshold
83
+ results = []
84
+ for idx, score in enumerate(similarities):
85
+ if score >= score_threshold:
86
+ memory_id = self._memory_ids[idx]
87
+ results.append((memory_id, float(score)))
88
+
89
+ # Sort by score and limit
90
+ results.sort(key=lambda x: x[1], reverse=True)
91
+ return results[:limit]
92
+
93
+ except Exception as e:
94
+ # Fallback gracefully
95
+ return []
96
+
97
+ def search_graph(
98
+ self,
99
+ query: str,
100
+ limit: int = 10,
101
+ max_depth: int = 2
102
+ ) -> List[Tuple[int, float]]:
103
+ """
104
+ Search using graph traversal from initial matches.
105
+
106
+ Strategy:
107
+ 1. Get seed memories from BM25
108
+ 2. Traverse graph to find related memories
109
+ 3. Score by distance from seed nodes
110
+
111
+ Args:
112
+ query: Search query
113
+ limit: Maximum results
114
+ max_depth: Maximum graph traversal depth
115
+
116
+ Returns:
117
+ List of (memory_id, score) tuples
118
+ """
119
+ graph = self._load_graph_engine()
120
+ if graph is None:
121
+ return []
122
+
123
+ # Get seed memories from BM25
124
+ seed_results = self.search_bm25(query, limit=5)
125
+ if not seed_results:
126
+ return []
127
+
128
+ seed_ids = [mem_id for mem_id, _ in seed_results]
129
+
130
+ # Traverse graph from seed nodes
131
+ visited = set(seed_ids)
132
+ results = []
133
+
134
+ # BFS traversal
135
+ queue = [(mem_id, 1.0, 0) for mem_id in seed_ids] # (id, score, depth)
136
+
137
+ while queue and len(results) < limit:
138
+ current_id, current_score, depth = queue.pop(0)
139
+
140
+ if depth > max_depth:
141
+ continue
142
+
143
+ # Add to results
144
+ if current_id not in [r[0] for r in results]:
145
+ results.append((current_id, current_score))
146
+
147
+ # Get related memories from graph
148
+ try:
149
+ related = graph.get_related_memories(current_id, limit=5)
150
+
151
+ for rel_id, similarity in related:
152
+ if rel_id not in visited:
153
+ visited.add(rel_id)
154
+ # Decay score by depth
155
+ new_score = current_score * similarity * (0.7 ** depth)
156
+ queue.append((rel_id, new_score, depth + 1))
157
+
158
+ except Exception:
159
+ # Graph operation failed - skip
160
+ continue
161
+
162
+ return results[:limit]
@@ -1,16 +1,14 @@
1
1
  #!/usr/bin/env python3
2
- """
3
- SuperLocalMemory V2 - BM25 Search Engine
2
+ # SPDX-License-Identifier: MIT
3
+ # Copyright (c) 2026 SuperLocalMemory (superlocalmemory.com)
4
+ """SuperLocalMemory V2 - BM25 Search Engine
4
5
 
5
- Copyright (c) 2026 Varun Pratap Bhardwaj
6
6
  Solution Architect & Original Creator
7
7
 
8
- Licensed under MIT License (see LICENSE file)
9
- Repository: https://github.com/varun369/SuperLocalMemoryV2
8
+ (see LICENSE file)
10
9
 
11
10
  ATTRIBUTION REQUIRED: This notice must be preserved in all copies.
12
11
  """
13
-
14
12
  """
15
13
  BM25 Search Engine - Pure Python Implementation
16
14
 
@@ -1,16 +1,11 @@
1
1
  #!/usr/bin/env python3
2
- """
3
- SuperLocalMemory V2 - Setup Validator
4
- Copyright (c) 2026 Varun Pratap Bhardwaj
5
- Licensed under MIT License
6
-
7
- Repository: https://github.com/varun369/SuperLocalMemoryV2
8
- Author: Varun Pratap Bhardwaj (Solution Architect)
2
+ # SPDX-License-Identifier: MIT
3
+ # Copyright (c) 2026 SuperLocalMemory (superlocalmemory.com)
4
+ """SuperLocalMemory V2 - Setup Validator
9
5
 
10
6
  First-run setup validator that ensures all dependencies are installed
11
7
  and the database is properly initialized.
12
8
  """
13
-
14
9
  import sys
15
10
  import os
16
11
  import sqlite3
@@ -41,9 +36,8 @@ def print_banner():
41
36
  """Print product banner."""
42
37
  print("""
43
38
  ╔══════════════════════════════════════════════════════════════╗
44
- ║ SuperLocalMemory V2 - Setup Validator
45
- by Varun Pratap Bhardwaj
46
- ║ https://github.com/varun369/SuperLocalMemoryV2 ║
39
+ ║ SuperLocalMemory - Setup Validator
40
+ https://superlocalmemory.com
47
41
  ╚══════════════════════════════════════════════════════════════╝
48
42
  """)
49
43
 
@@ -338,8 +332,8 @@ def initialize_database() -> Tuple[bool, str]:
338
332
  # Add system watermark
339
333
  cursor.execute('''
340
334
  INSERT OR REPLACE INTO system_metadata (key, value) VALUES
341
- ('product', 'SuperLocalMemory V2'),
342
- ('author', 'Varun Pratap Bhardwaj'),
335
+ ('product', 'SuperLocalMemory'),
336
+ ('website', 'https://superlocalmemory.com'),
343
337
  ('repository', 'https://github.com/varun369/SuperLocalMemoryV2'),
344
338
  ('license', 'MIT'),
345
339
  ('schema_version', '2.0.0')
@@ -1,16 +1,6 @@
1
1
  #!/usr/bin/env python3
2
- """
3
- SuperLocalMemory V2 - Subscription Manager
4
- Copyright (c) 2026 Varun Pratap Bhardwaj
5
- Licensed under MIT License
6
-
7
- Repository: https://github.com/varun369/SuperLocalMemoryV2
8
- Author: Varun Pratap Bhardwaj (Solution Architect)
9
-
10
- NOTICE: This software is protected by MIT License.
11
- Attribution must be preserved in all copies or derivatives.
12
- """
13
-
2
+ # SPDX-License-Identifier: MIT
3
+ # Copyright (c) 2026 SuperLocalMemory (superlocalmemory.com)
14
4
  """
15
5
  SubscriptionManager — Manages durable and ephemeral event subscriptions.
16
6
 
@@ -0,0 +1,59 @@
1
+ # SPDX-License-Identifier: MIT
2
+ # Copyright (c) 2026 SuperLocalMemory (superlocalmemory.com)
3
+ """Tree — Hierarchical Memory Tree Management.
4
+
5
+ Composes the TreeManager class from focused mixin modules:
6
+ - schema.py : DB initialization and root-node bootstrap
7
+ - nodes.py : Node CRUD and count aggregation
8
+ - queries.py : Read-only tree traversal and statistics
9
+ - builder.py : Full tree construction from memories table
10
+ """
11
+ from pathlib import Path
12
+ from typing import Optional
13
+
14
+ from .schema import TreeSchemaMixin, MEMORY_DIR, DB_PATH
15
+ from .nodes import TreeNodesMixin
16
+ from .queries import TreeQueriesMixin
17
+ from .builder import TreeBuilderMixin
18
+
19
+
20
+ class TreeManager(TreeSchemaMixin, TreeNodesMixin, TreeQueriesMixin, TreeBuilderMixin):
21
+ """
22
+ Manages hierarchical tree structure for memory navigation.
23
+
24
+ Tree Structure:
25
+ Root
26
+ +-- Project: NextJS-App
27
+ | +-- Category: Frontend
28
+ | | +-- Memory: React Components
29
+ | | +-- Memory: State Management
30
+ | +-- Category: Backend
31
+ | +-- Memory: API Routes
32
+ +-- Project: Python-ML
33
+
34
+ Materialized Path Format:
35
+ - Root: "1"
36
+ - Project: "1.2"
37
+ - Category: "1.2.3"
38
+ - Memory: "1.2.3.4"
39
+
40
+ Benefits:
41
+ - Fast subtree queries: WHERE tree_path LIKE '1.2.%'
42
+ - O(1) depth calculation: count dots in path
43
+ - O(1) parent lookup: parse path
44
+ - No recursive CTEs needed
45
+ """
46
+
47
+ def __init__(self, db_path: Optional[Path] = None):
48
+ """
49
+ Initialize TreeManager.
50
+
51
+ Args:
52
+ db_path: Optional custom database path
53
+ """
54
+ self.db_path = db_path or DB_PATH
55
+ self._init_db()
56
+ self.root_id = self._ensure_root()
57
+
58
+
59
+ __all__ = ['TreeManager', 'MEMORY_DIR', 'DB_PATH']
@@ -0,0 +1,183 @@
1
+ # SPDX-License-Identifier: MIT
2
+ # Copyright (c) 2026 SuperLocalMemory (superlocalmemory.com)
3
+ """Tree Builder — Constructs the full tree from the memories table.
4
+
5
+ Provides TreeBuilderMixin with build_tree, plus the CLI entry point.
6
+ """
7
+ import sqlite3
8
+
9
+
10
+ class TreeBuilderMixin:
11
+ """Builds the hierarchical tree from flat memory records."""
12
+
13
+ def build_tree(self):
14
+ """
15
+ Build complete tree structure from memories table.
16
+
17
+ Process:
18
+ 1. Clear existing tree (except root)
19
+ 2. Group memories by project
20
+ 3. Group by category within projects
21
+ 4. Link individual memories as leaf nodes
22
+ 5. Update aggregated counts
23
+ """
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()
89
+
90
+
91
+ def run_cli():
92
+ """CLI entry point for tree_manager."""
93
+ import sys
94
+ import json
95
+ from src.tree import TreeManager
96
+
97
+ tree_mgr = TreeManager()
98
+
99
+ if len(sys.argv) < 2:
100
+ print("TreeManager CLI")
101
+ print("\nCommands:")
102
+ print(" python tree_manager.py build # Build tree from memories")
103
+ print(" python tree_manager.py show [project] [depth] # Show tree structure")
104
+ print(" python tree_manager.py subtree <node_id> # Get subtree")
105
+ print(" python tree_manager.py path <node_id> # Get path to root")
106
+ print(" python tree_manager.py stats # Show statistics")
107
+ print(" python tree_manager.py add <type> <name> <parent_id> # Add node")
108
+ print(" python tree_manager.py delete <node_id> # Delete node")
109
+ sys.exit(0)
110
+
111
+ command = sys.argv[1]
112
+
113
+ if command == "build":
114
+ print("Building tree from memories...")
115
+ tree_mgr.build_tree()
116
+ stats = tree_mgr.get_stats()
117
+ print(f"Tree built: {stats['total_nodes']} nodes, {stats['total_memories']} memories")
118
+
119
+ elif command == "show":
120
+ project = sys.argv[2] if len(sys.argv) > 2 else None
121
+ max_depth = int(sys.argv[3]) if len(sys.argv) > 3 else None
122
+
123
+ tree = tree_mgr.get_tree(project, max_depth)
124
+
125
+ def print_tree(node, indent=0):
126
+ if 'error' in node:
127
+ print(node['error'])
128
+ return
129
+
130
+ prefix = " " * indent
131
+ icon = {"root": "\U0001f333", "project": "\U0001f4c1", "category": "\U0001f4c2", "memory": "\U0001f4c4"}.get(node['type'], "\u2022")
132
+
133
+ print(f"{prefix}{icon} {node['name']} (id={node['id']}, memories={node['memory_count']})")
134
+
135
+ for child in node.get('children', []):
136
+ print_tree(child, indent + 1)
137
+
138
+ print_tree(tree)
139
+
140
+ elif command == "subtree" and len(sys.argv) >= 3:
141
+ node_id = int(sys.argv[2])
142
+ nodes = tree_mgr.get_subtree(node_id)
143
+
144
+ if not nodes:
145
+ print(f"No subtree found for node {node_id}")
146
+ else:
147
+ print(f"Subtree of node {node_id}:")
148
+ for node in nodes:
149
+ indent = " " * (node['depth'] - nodes[0]['depth'] + 1)
150
+ print(f"{indent}- {node['name']} (id={node['id']})")
151
+
152
+ elif command == "path" and len(sys.argv) >= 3:
153
+ node_id = int(sys.argv[2])
154
+ path = tree_mgr.get_path_to_root(node_id)
155
+
156
+ if not path:
157
+ print(f"Node {node_id} not found")
158
+ else:
159
+ print("Path to root:")
160
+ print(" > ".join([f"{n['name']} (id={n['id']})" for n in path]))
161
+
162
+ elif command == "stats":
163
+ stats = tree_mgr.get_stats()
164
+ print(json.dumps(stats, indent=2))
165
+
166
+ elif command == "add" and len(sys.argv) >= 5:
167
+ node_type = sys.argv[2]
168
+ name = sys.argv[3]
169
+ parent_id = int(sys.argv[4])
170
+
171
+ node_id = tree_mgr.add_node(node_type, name, parent_id)
172
+ print(f"Node created with ID: {node_id}")
173
+
174
+ elif command == "delete" and len(sys.argv) >= 3:
175
+ node_id = int(sys.argv[2])
176
+ if tree_mgr.delete_node(node_id):
177
+ print(f"Node {node_id} deleted")
178
+ else:
179
+ print(f"Node {node_id} not found")
180
+
181
+ else:
182
+ print(f"Unknown command: {command}")
183
+ sys.exit(1)
@@ -0,0 +1,196 @@
1
+ # SPDX-License-Identifier: MIT
2
+ # Copyright (c) 2026 SuperLocalMemory (superlocalmemory.com)
3
+ """Tree Nodes — CRUD operations and count aggregation.
4
+
5
+ Provides TreeNodesMixin with add_node, delete_node, update_counts,
6
+ and internal helper methods for materialized-path management.
7
+ """
8
+ import sqlite3
9
+ from typing import Optional, Dict, Any
10
+ from datetime import datetime
11
+
12
+
13
+ class TreeNodesMixin:
14
+ """Node CRUD and aggregation logic for the memory tree."""
15
+
16
+ def add_node(
17
+ self,
18
+ node_type: str,
19
+ name: str,
20
+ parent_id: int,
21
+ description: Optional[str] = None,
22
+ memory_id: Optional[int] = None
23
+ ) -> int:
24
+ """
25
+ Add a new node to the tree.
26
+
27
+ Args:
28
+ node_type: Type of node ('root', 'project', 'category', 'memory')
29
+ name: Display name
30
+ parent_id: Parent node ID
31
+ description: Optional description
32
+ memory_id: Link to memories table (for leaf nodes)
33
+
34
+ Returns:
35
+ New node ID
36
+ """
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()
78
+
79
+ return node_id
80
+
81
+ def delete_node(self, node_id: int) -> bool:
82
+ """
83
+ Delete a node and all its descendants.
84
+
85
+ Args:
86
+ node_id: Node ID to delete
87
+
88
+ Returns:
89
+ True if deleted, False if not found
90
+ """
91
+ if node_id == self.root_id:
92
+ raise ValueError("Cannot delete root node")
93
+
94
+ conn = sqlite3.connect(self.db_path)
95
+ cursor = conn.cursor()
96
+
97
+ # Get tree_path
98
+ cursor.execute('SELECT tree_path, parent_id FROM memory_tree WHERE id = ?', (node_id,))
99
+ result = cursor.fetchone()
100
+
101
+ if not result:
102
+ conn.close()
103
+ return False
104
+
105
+ tree_path, parent_id = result
106
+
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}.%"))
110
+
111
+ deleted = cursor.rowcount > 0
112
+ conn.commit()
113
+ conn.close()
114
+
115
+ # Update parent counts
116
+ if deleted and parent_id:
117
+ self.update_counts(parent_id)
118
+
119
+ return deleted
120
+
121
+ def update_counts(self, node_id: int):
122
+ """
123
+ Update aggregated counts for a node (memory_count, total_size).
124
+ Recursively updates all ancestors.
125
+
126
+ Args:
127
+ node_id: Node ID to update
128
+ """
129
+ 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:
137
+ 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
+
167
+ def _update_all_counts(self):
168
+ """Update counts for all nodes (used after build_tree)."""
169
+ conn = sqlite3.connect(self.db_path)
170
+ cursor = conn.cursor()
171
+
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
+
178
+ node_ids = [row[0] for row in cursor.fetchall()]
179
+ conn.close()
180
+
181
+ # Update each node (will cascade to parents)
182
+ processed = set()
183
+ for node_id in node_ids:
184
+ if node_id not in processed:
185
+ self.update_counts(node_id)
186
+ processed.add(node_id)
187
+
188
+ def _generate_tree_path(self, parent_path: str, node_id: int) -> str:
189
+ """Generate tree_path for a new node."""
190
+ if parent_path:
191
+ return f"{parent_path}.{node_id}"
192
+ return str(node_id)
193
+
194
+ def _calculate_depth(self, tree_path: str) -> int:
195
+ """Calculate depth from tree_path (count dots)."""
196
+ return tree_path.count('.')