superlocalmemory 2.3.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 (100) hide show
  1. package/ATTRIBUTION.md +140 -0
  2. package/CHANGELOG.md +1749 -0
  3. package/LICENSE +21 -0
  4. package/README.md +600 -0
  5. package/bin/aider-smart +72 -0
  6. package/bin/slm +202 -0
  7. package/bin/slm-npm +73 -0
  8. package/bin/slm.bat +195 -0
  9. package/bin/slm.cmd +10 -0
  10. package/bin/superlocalmemoryv2:list +3 -0
  11. package/bin/superlocalmemoryv2:profile +3 -0
  12. package/bin/superlocalmemoryv2:recall +3 -0
  13. package/bin/superlocalmemoryv2:remember +3 -0
  14. package/bin/superlocalmemoryv2:reset +3 -0
  15. package/bin/superlocalmemoryv2:status +3 -0
  16. package/completions/slm.bash +58 -0
  17. package/completions/slm.zsh +76 -0
  18. package/configs/antigravity-mcp.json +13 -0
  19. package/configs/chatgpt-desktop-mcp.json +7 -0
  20. package/configs/claude-desktop-mcp.json +15 -0
  21. package/configs/codex-mcp.toml +13 -0
  22. package/configs/cody-commands.json +29 -0
  23. package/configs/continue-mcp.yaml +14 -0
  24. package/configs/continue-skills.yaml +26 -0
  25. package/configs/cursor-mcp.json +15 -0
  26. package/configs/gemini-cli-mcp.json +11 -0
  27. package/configs/jetbrains-mcp.json +11 -0
  28. package/configs/opencode-mcp.json +12 -0
  29. package/configs/perplexity-mcp.json +9 -0
  30. package/configs/vscode-copilot-mcp.json +12 -0
  31. package/configs/windsurf-mcp.json +16 -0
  32. package/configs/zed-mcp.json +12 -0
  33. package/docs/ARCHITECTURE.md +877 -0
  34. package/docs/CLI-COMMANDS-REFERENCE.md +425 -0
  35. package/docs/COMPETITIVE-ANALYSIS.md +210 -0
  36. package/docs/COMPRESSION-README.md +390 -0
  37. package/docs/GRAPH-ENGINE.md +503 -0
  38. package/docs/MCP-MANUAL-SETUP.md +720 -0
  39. package/docs/MCP-TROUBLESHOOTING.md +787 -0
  40. package/docs/PATTERN-LEARNING.md +363 -0
  41. package/docs/PROFILES-GUIDE.md +453 -0
  42. package/docs/RESET-GUIDE.md +353 -0
  43. package/docs/SEARCH-ENGINE-V2.2.0.md +748 -0
  44. package/docs/SEARCH-INTEGRATION-GUIDE.md +502 -0
  45. package/docs/UI-SERVER.md +254 -0
  46. package/docs/UNIVERSAL-INTEGRATION.md +432 -0
  47. package/docs/V2.2.0-OPTIONAL-SEARCH.md +666 -0
  48. package/docs/WINDOWS-INSTALL-README.txt +34 -0
  49. package/docs/WINDOWS-POST-INSTALL.txt +45 -0
  50. package/docs/example_graph_usage.py +148 -0
  51. package/hooks/memory-list-skill.js +130 -0
  52. package/hooks/memory-profile-skill.js +284 -0
  53. package/hooks/memory-recall-skill.js +109 -0
  54. package/hooks/memory-remember-skill.js +127 -0
  55. package/hooks/memory-reset-skill.js +274 -0
  56. package/install-skills.sh +436 -0
  57. package/install.ps1 +417 -0
  58. package/install.sh +755 -0
  59. package/mcp_server.py +585 -0
  60. package/package.json +94 -0
  61. package/requirements-core.txt +24 -0
  62. package/requirements.txt +10 -0
  63. package/scripts/postinstall.js +126 -0
  64. package/scripts/preuninstall.js +57 -0
  65. package/skills/slm-build-graph/SKILL.md +423 -0
  66. package/skills/slm-list-recent/SKILL.md +348 -0
  67. package/skills/slm-recall/SKILL.md +325 -0
  68. package/skills/slm-remember/SKILL.md +194 -0
  69. package/skills/slm-status/SKILL.md +363 -0
  70. package/skills/slm-switch-profile/SKILL.md +442 -0
  71. package/src/__pycache__/cache_manager.cpython-312.pyc +0 -0
  72. package/src/__pycache__/embedding_engine.cpython-312.pyc +0 -0
  73. package/src/__pycache__/graph_engine.cpython-312.pyc +0 -0
  74. package/src/__pycache__/hnsw_index.cpython-312.pyc +0 -0
  75. package/src/__pycache__/hybrid_search.cpython-312.pyc +0 -0
  76. package/src/__pycache__/memory-profiles.cpython-312.pyc +0 -0
  77. package/src/__pycache__/memory-reset.cpython-312.pyc +0 -0
  78. package/src/__pycache__/memory_compression.cpython-312.pyc +0 -0
  79. package/src/__pycache__/memory_store_v2.cpython-312.pyc +0 -0
  80. package/src/__pycache__/migrate_v1_to_v2.cpython-312.pyc +0 -0
  81. package/src/__pycache__/pattern_learner.cpython-312.pyc +0 -0
  82. package/src/__pycache__/query_optimizer.cpython-312.pyc +0 -0
  83. package/src/__pycache__/search_engine_v2.cpython-312.pyc +0 -0
  84. package/src/__pycache__/setup_validator.cpython-312.pyc +0 -0
  85. package/src/__pycache__/tree_manager.cpython-312.pyc +0 -0
  86. package/src/cache_manager.py +520 -0
  87. package/src/embedding_engine.py +671 -0
  88. package/src/graph_engine.py +970 -0
  89. package/src/hnsw_index.py +626 -0
  90. package/src/hybrid_search.py +693 -0
  91. package/src/memory-profiles.py +518 -0
  92. package/src/memory-reset.py +485 -0
  93. package/src/memory_compression.py +999 -0
  94. package/src/memory_store_v2.py +1088 -0
  95. package/src/migrate_v1_to_v2.py +638 -0
  96. package/src/pattern_learner.py +898 -0
  97. package/src/query_optimizer.py +513 -0
  98. package/src/search_engine_v2.py +403 -0
  99. package/src/setup_validator.py +479 -0
  100. package/src/tree_manager.py +720 -0
@@ -0,0 +1,720 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ SuperLocalMemory V2 - Intelligent Local Memory System
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
+
14
+ """
15
+ TreeManager - Hierarchical Memory Tree Management
16
+ Implements PageIndex-style materialized path navigation for fast subtree queries.
17
+
18
+ Key Features:
19
+ - Materialized path storage (e.g., "1.2.5" for fast subtree retrieval)
20
+ - No recursive CTEs needed (SQLite compatible)
21
+ - Build hierarchical index from flat memories
22
+ - Aggregated counts and metadata at each node
23
+ """
24
+
25
+ import sqlite3
26
+ from pathlib import Path
27
+ from typing import Optional, List, Dict, Any, Tuple
28
+ from datetime import datetime
29
+
30
+ MEMORY_DIR = Path.home() / ".claude-memory"
31
+ DB_PATH = MEMORY_DIR / "memory.db"
32
+
33
+
34
+ class TreeManager:
35
+ """
36
+ Manages hierarchical tree structure for memory navigation.
37
+
38
+ Tree Structure:
39
+ Root
40
+ ├── Project: NextJS-App
41
+ │ ├── Category: Frontend
42
+ │ │ ├── Memory: React Components
43
+ │ │ └── Memory: State Management
44
+ │ └── Category: Backend
45
+ │ └── Memory: API Routes
46
+ └── Project: Python-ML
47
+
48
+ Materialized Path Format:
49
+ - Root: "1"
50
+ - Project: "1.2"
51
+ - Category: "1.2.3"
52
+ - Memory: "1.2.3.4"
53
+
54
+ Benefits:
55
+ - Fast subtree queries: WHERE tree_path LIKE '1.2.%'
56
+ - O(1) depth calculation: count dots in path
57
+ - O(1) parent lookup: parse path
58
+ - No recursive CTEs needed
59
+ """
60
+
61
+ def __init__(self, db_path: Optional[Path] = None):
62
+ """
63
+ Initialize TreeManager.
64
+
65
+ Args:
66
+ db_path: Optional custom database path
67
+ """
68
+ self.db_path = db_path or DB_PATH
69
+ self._init_db()
70
+ self.root_id = self._ensure_root()
71
+
72
+ def _init_db(self):
73
+ """Initialize memory_tree table."""
74
+ conn = sqlite3.connect(self.db_path)
75
+ cursor = conn.cursor()
76
+
77
+ cursor.execute('''
78
+ CREATE TABLE IF NOT EXISTS memory_tree (
79
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
80
+ node_type TEXT NOT NULL,
81
+ name TEXT NOT NULL,
82
+ description TEXT,
83
+
84
+ parent_id INTEGER,
85
+ tree_path TEXT NOT NULL,
86
+ depth INTEGER DEFAULT 0,
87
+
88
+ memory_count INTEGER DEFAULT 0,
89
+ total_size INTEGER DEFAULT 0,
90
+ last_updated TIMESTAMP,
91
+
92
+ memory_id INTEGER,
93
+
94
+ FOREIGN KEY (parent_id) REFERENCES memory_tree(id) ON DELETE CASCADE,
95
+ FOREIGN KEY (memory_id) REFERENCES memories(id) ON DELETE CASCADE
96
+ )
97
+ ''')
98
+
99
+ cursor.execute('CREATE INDEX IF NOT EXISTS idx_tree_path_layer2 ON memory_tree(tree_path)')
100
+ cursor.execute('CREATE INDEX IF NOT EXISTS idx_node_type ON memory_tree(node_type)')
101
+ cursor.execute('CREATE INDEX IF NOT EXISTS idx_parent_id_tree ON memory_tree(parent_id)')
102
+
103
+ conn.commit()
104
+ conn.close()
105
+
106
+ def _ensure_root(self) -> int:
107
+ """Ensure root node exists and return its ID."""
108
+ conn = sqlite3.connect(self.db_path)
109
+ cursor = conn.cursor()
110
+
111
+ cursor.execute('SELECT id FROM memory_tree WHERE node_type = ? AND parent_id IS NULL', ('root',))
112
+ result = cursor.fetchone()
113
+
114
+ if result:
115
+ root_id = result[0]
116
+ else:
117
+ cursor.execute('''
118
+ INSERT INTO memory_tree (node_type, name, tree_path, depth, last_updated)
119
+ VALUES (?, ?, ?, ?, ?)
120
+ ''', ('root', 'Root', '1', 0, datetime.now().isoformat()))
121
+ root_id = cursor.lastrowid
122
+
123
+ # Update tree_path with actual ID
124
+ cursor.execute('UPDATE memory_tree SET tree_path = ? WHERE id = ?', (str(root_id), root_id))
125
+ conn.commit()
126
+
127
+ conn.close()
128
+ return root_id
129
+
130
+ def build_tree(self):
131
+ """
132
+ Build complete tree structure from memories table.
133
+
134
+ Process:
135
+ 1. Clear existing tree (except root)
136
+ 2. Group memories by project
137
+ 3. Group by category within projects
138
+ 4. Link individual memories as leaf nodes
139
+ 5. Update aggregated counts
140
+ """
141
+ conn = sqlite3.connect(self.db_path)
142
+ cursor = conn.cursor()
143
+
144
+ # Clear existing tree (keep root)
145
+ cursor.execute('DELETE FROM memory_tree WHERE node_type != ?', ('root',))
146
+
147
+ # Step 1: Create project nodes
148
+ cursor.execute('''
149
+ SELECT DISTINCT project_path, project_name
150
+ FROM memories
151
+ WHERE project_path IS NOT NULL
152
+ ORDER BY project_path
153
+ ''')
154
+ projects = cursor.fetchall()
155
+
156
+ project_map = {} # project_path -> node_id
157
+
158
+ for project_path, project_name in projects:
159
+ name = project_name or project_path.split('/')[-1]
160
+ node_id = self.add_node('project', name, self.root_id, description=project_path)
161
+ project_map[project_path] = node_id
162
+
163
+ # Step 2: Create category nodes within projects
164
+ cursor.execute('''
165
+ SELECT DISTINCT project_path, category
166
+ FROM memories
167
+ WHERE project_path IS NOT NULL AND category IS NOT NULL
168
+ ORDER BY project_path, category
169
+ ''')
170
+ categories = cursor.fetchall()
171
+
172
+ category_map = {} # (project_path, category) -> node_id
173
+
174
+ for project_path, category in categories:
175
+ parent_id = project_map.get(project_path)
176
+ if parent_id:
177
+ node_id = self.add_node('category', category, parent_id)
178
+ category_map[(project_path, category)] = node_id
179
+
180
+ # Step 3: Link memories as leaf nodes
181
+ cursor.execute('''
182
+ SELECT id, content, summary, project_path, category, importance, created_at
183
+ FROM memories
184
+ ORDER BY created_at DESC
185
+ ''')
186
+ memories = cursor.fetchall()
187
+
188
+ for mem_id, content, summary, project_path, category, importance, created_at in memories:
189
+ # Determine parent node
190
+ if project_path and category and (project_path, category) in category_map:
191
+ parent_id = category_map[(project_path, category)]
192
+ elif project_path and project_path in project_map:
193
+ parent_id = project_map[project_path]
194
+ else:
195
+ parent_id = self.root_id
196
+
197
+ # Create memory node
198
+ name = summary or content[:60].replace('\n', ' ')
199
+ self.add_node('memory', name, parent_id, memory_id=mem_id, description=content[:200])
200
+
201
+ # Step 4: Update aggregated counts
202
+ self._update_all_counts()
203
+
204
+ conn.commit()
205
+ conn.close()
206
+
207
+ def add_node(
208
+ self,
209
+ node_type: str,
210
+ name: str,
211
+ parent_id: int,
212
+ description: Optional[str] = None,
213
+ memory_id: Optional[int] = None
214
+ ) -> int:
215
+ """
216
+ Add a new node to the tree.
217
+
218
+ Args:
219
+ node_type: Type of node ('root', 'project', 'category', 'memory')
220
+ name: Display name
221
+ parent_id: Parent node ID
222
+ description: Optional description
223
+ memory_id: Link to memories table (for leaf nodes)
224
+
225
+ Returns:
226
+ New node ID
227
+ """
228
+ conn = sqlite3.connect(self.db_path)
229
+ cursor = conn.cursor()
230
+
231
+ # Get parent path and depth
232
+ cursor.execute('SELECT tree_path, depth FROM memory_tree WHERE id = ?', (parent_id,))
233
+ result = cursor.fetchone()
234
+
235
+ if not result:
236
+ raise ValueError(f"Parent node {parent_id} not found")
237
+
238
+ parent_path, parent_depth = result
239
+
240
+ # Calculate new node position
241
+ depth = parent_depth + 1
242
+
243
+ cursor.execute('''
244
+ INSERT INTO memory_tree (
245
+ node_type, name, description,
246
+ parent_id, tree_path, depth,
247
+ memory_id, last_updated
248
+ )
249
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?)
250
+ ''', (
251
+ node_type,
252
+ name,
253
+ description,
254
+ parent_id,
255
+ '', # Placeholder, updated below
256
+ depth,
257
+ memory_id,
258
+ datetime.now().isoformat()
259
+ ))
260
+
261
+ node_id = cursor.lastrowid
262
+
263
+ # Update tree_path with actual node_id
264
+ tree_path = f"{parent_path}.{node_id}"
265
+ cursor.execute('UPDATE memory_tree SET tree_path = ? WHERE id = ?', (tree_path, node_id))
266
+
267
+ conn.commit()
268
+ conn.close()
269
+
270
+ return node_id
271
+
272
+ def get_tree(
273
+ self,
274
+ project_name: Optional[str] = None,
275
+ max_depth: Optional[int] = None
276
+ ) -> Dict[str, Any]:
277
+ """
278
+ Get tree structure as nested dictionary.
279
+
280
+ Args:
281
+ project_name: Filter by specific project
282
+ max_depth: Maximum depth to retrieve
283
+
284
+ Returns:
285
+ Nested dictionary representing tree structure
286
+ """
287
+ conn = sqlite3.connect(self.db_path)
288
+ cursor = conn.cursor()
289
+
290
+ # Build query
291
+ if project_name:
292
+ # Find project node
293
+ cursor.execute('''
294
+ SELECT id, tree_path FROM memory_tree
295
+ WHERE node_type = 'project' AND name = ?
296
+ ''', (project_name,))
297
+ result = cursor.fetchone()
298
+
299
+ if not result:
300
+ conn.close()
301
+ return {'error': f"Project '{project_name}' not found"}
302
+
303
+ project_id, project_path = result
304
+
305
+ # Get subtree
306
+ if max_depth is not None:
307
+ cursor.execute('''
308
+ SELECT id, node_type, name, description, parent_id, tree_path,
309
+ depth, memory_count, total_size, last_updated, memory_id
310
+ FROM memory_tree
311
+ WHERE (id = ? OR tree_path LIKE ?) AND depth <= ?
312
+ ORDER BY tree_path
313
+ ''', (project_id, f"{project_path}.%", max_depth))
314
+ else:
315
+ cursor.execute('''
316
+ SELECT id, node_type, name, description, parent_id, tree_path,
317
+ depth, memory_count, total_size, last_updated, memory_id
318
+ FROM memory_tree
319
+ WHERE id = ? OR tree_path LIKE ?
320
+ ORDER BY tree_path
321
+ ''', (project_id, f"{project_path}.%"))
322
+ else:
323
+ # Get entire tree
324
+ if max_depth is not None:
325
+ cursor.execute('''
326
+ SELECT id, node_type, name, description, parent_id, tree_path,
327
+ depth, memory_count, total_size, last_updated, memory_id
328
+ FROM memory_tree
329
+ WHERE depth <= ?
330
+ ORDER BY tree_path
331
+ ''', (max_depth,))
332
+ else:
333
+ cursor.execute('''
334
+ SELECT id, node_type, name, description, parent_id, tree_path,
335
+ depth, memory_count, total_size, last_updated, memory_id
336
+ FROM memory_tree
337
+ ORDER BY tree_path
338
+ ''')
339
+
340
+ rows = cursor.fetchall()
341
+ conn.close()
342
+
343
+ if not rows:
344
+ return {'error': 'No tree nodes found'}
345
+
346
+ # Build nested structure
347
+ nodes = {}
348
+ root = None
349
+
350
+ for row in rows:
351
+ node = {
352
+ 'id': row[0],
353
+ 'type': row[1],
354
+ 'name': row[2],
355
+ 'description': row[3],
356
+ 'parent_id': row[4],
357
+ 'tree_path': row[5],
358
+ 'depth': row[6],
359
+ 'memory_count': row[7],
360
+ 'total_size': row[8],
361
+ 'last_updated': row[9],
362
+ 'memory_id': row[10],
363
+ 'children': []
364
+ }
365
+ nodes[node['id']] = node
366
+
367
+ if node['parent_id'] is None or (project_name and node['type'] == 'project'):
368
+ root = node
369
+ elif node['parent_id'] in nodes:
370
+ nodes[node['parent_id']]['children'].append(node)
371
+
372
+ return root or {'error': 'Root node not found'}
373
+
374
+ def get_subtree(self, node_id: int) -> List[Dict[str, Any]]:
375
+ """
376
+ Get all descendants of a specific node (flat list).
377
+
378
+ Args:
379
+ node_id: Node ID to get subtree for
380
+
381
+ Returns:
382
+ List of descendant nodes
383
+ """
384
+ conn = sqlite3.connect(self.db_path)
385
+ cursor = conn.cursor()
386
+
387
+ # Get node's tree_path
388
+ cursor.execute('SELECT tree_path FROM memory_tree WHERE id = ?', (node_id,))
389
+ result = cursor.fetchone()
390
+
391
+ if not result:
392
+ conn.close()
393
+ return []
394
+
395
+ tree_path = result[0]
396
+
397
+ # Get all descendants
398
+ cursor.execute('''
399
+ SELECT id, node_type, name, description, parent_id, tree_path,
400
+ depth, memory_count, total_size, last_updated, memory_id
401
+ FROM memory_tree
402
+ WHERE tree_path LIKE ?
403
+ ORDER BY tree_path
404
+ ''', (f"{tree_path}.%",))
405
+
406
+ results = []
407
+ for row in cursor.fetchall():
408
+ results.append({
409
+ 'id': row[0],
410
+ 'type': row[1],
411
+ 'name': row[2],
412
+ 'description': row[3],
413
+ 'parent_id': row[4],
414
+ 'tree_path': row[5],
415
+ 'depth': row[6],
416
+ 'memory_count': row[7],
417
+ 'total_size': row[8],
418
+ 'last_updated': row[9],
419
+ 'memory_id': row[10]
420
+ })
421
+
422
+ conn.close()
423
+ return results
424
+
425
+ def get_path_to_root(self, node_id: int) -> List[Dict[str, Any]]:
426
+ """
427
+ Get path from node to root (breadcrumb trail).
428
+
429
+ Args:
430
+ node_id: Starting node ID
431
+
432
+ Returns:
433
+ List of nodes from root to target node
434
+ """
435
+ conn = sqlite3.connect(self.db_path)
436
+ cursor = conn.cursor()
437
+
438
+ # Get node's tree_path
439
+ cursor.execute('SELECT tree_path FROM memory_tree WHERE id = ?', (node_id,))
440
+ result = cursor.fetchone()
441
+
442
+ if not result:
443
+ conn.close()
444
+ return []
445
+
446
+ tree_path = result[0]
447
+
448
+ # Parse path to get all ancestor IDs
449
+ path_ids = [int(x) for x in tree_path.split('.')]
450
+
451
+ # Get all ancestor nodes
452
+ placeholders = ','.join('?' * len(path_ids))
453
+ cursor.execute(f'''
454
+ SELECT id, node_type, name, description, parent_id, tree_path,
455
+ depth, memory_count, total_size, last_updated, memory_id
456
+ FROM memory_tree
457
+ WHERE id IN ({placeholders})
458
+ ORDER BY depth
459
+ ''', path_ids)
460
+
461
+ results = []
462
+ for row in cursor.fetchall():
463
+ results.append({
464
+ 'id': row[0],
465
+ 'type': row[1],
466
+ 'name': row[2],
467
+ 'description': row[3],
468
+ 'parent_id': row[4],
469
+ 'tree_path': row[5],
470
+ 'depth': row[6],
471
+ 'memory_count': row[7],
472
+ 'total_size': row[8],
473
+ 'last_updated': row[9],
474
+ 'memory_id': row[10]
475
+ })
476
+
477
+ conn.close()
478
+ return results
479
+
480
+ def update_counts(self, node_id: int):
481
+ """
482
+ Update aggregated counts for a node (memory_count, total_size).
483
+ Recursively updates all ancestors.
484
+
485
+ Args:
486
+ node_id: Node ID to update
487
+ """
488
+ conn = sqlite3.connect(self.db_path)
489
+ cursor = conn.cursor()
490
+
491
+ # Get all descendant memory nodes
492
+ cursor.execute('SELECT tree_path FROM memory_tree WHERE id = ?', (node_id,))
493
+ result = cursor.fetchone()
494
+
495
+ if not result:
496
+ conn.close()
497
+ return
498
+
499
+ tree_path = result[0]
500
+
501
+ # Count memories in subtree
502
+ cursor.execute('''
503
+ SELECT COUNT(*), COALESCE(SUM(LENGTH(m.content)), 0)
504
+ FROM memory_tree t
505
+ LEFT JOIN memories m ON t.memory_id = m.id
506
+ WHERE t.tree_path LIKE ? AND t.memory_id IS NOT NULL
507
+ ''', (f"{tree_path}%",))
508
+
509
+ memory_count, total_size = cursor.fetchone()
510
+
511
+ # Update node
512
+ cursor.execute('''
513
+ UPDATE memory_tree
514
+ SET memory_count = ?, total_size = ?, last_updated = ?
515
+ WHERE id = ?
516
+ ''', (memory_count, total_size, datetime.now().isoformat(), node_id))
517
+
518
+ # Update all ancestors
519
+ path_ids = [int(x) for x in tree_path.split('.')]
520
+ for ancestor_id in path_ids[:-1]: # Exclude current node
521
+ self.update_counts(ancestor_id)
522
+
523
+ conn.commit()
524
+ conn.close()
525
+
526
+ def _update_all_counts(self):
527
+ """Update counts for all nodes (used after build_tree)."""
528
+ conn = sqlite3.connect(self.db_path)
529
+ cursor = conn.cursor()
530
+
531
+ # Get all nodes in reverse depth order (leaves first)
532
+ cursor.execute('''
533
+ SELECT id FROM memory_tree
534
+ ORDER BY depth DESC
535
+ ''')
536
+
537
+ node_ids = [row[0] for row in cursor.fetchall()]
538
+ conn.close()
539
+
540
+ # Update each node (will cascade to parents)
541
+ processed = set()
542
+ for node_id in node_ids:
543
+ if node_id not in processed:
544
+ self.update_counts(node_id)
545
+ processed.add(node_id)
546
+
547
+ def _generate_tree_path(self, parent_path: str, node_id: int) -> str:
548
+ """Generate tree_path for a new node."""
549
+ if parent_path:
550
+ return f"{parent_path}.{node_id}"
551
+ return str(node_id)
552
+
553
+ def _calculate_depth(self, tree_path: str) -> int:
554
+ """Calculate depth from tree_path (count dots)."""
555
+ return tree_path.count('.')
556
+
557
+ def delete_node(self, node_id: int) -> bool:
558
+ """
559
+ Delete a node and all its descendants.
560
+
561
+ Args:
562
+ node_id: Node ID to delete
563
+
564
+ Returns:
565
+ True if deleted, False if not found
566
+ """
567
+ if node_id == self.root_id:
568
+ raise ValueError("Cannot delete root node")
569
+
570
+ conn = sqlite3.connect(self.db_path)
571
+ cursor = conn.cursor()
572
+
573
+ # Get tree_path
574
+ cursor.execute('SELECT tree_path, parent_id FROM memory_tree WHERE id = ?', (node_id,))
575
+ result = cursor.fetchone()
576
+
577
+ if not result:
578
+ conn.close()
579
+ return False
580
+
581
+ tree_path, parent_id = result
582
+
583
+ # Delete node and all descendants (CASCADE handles children)
584
+ cursor.execute('DELETE FROM memory_tree WHERE id = ? OR tree_path LIKE ?',
585
+ (node_id, f"{tree_path}.%"))
586
+
587
+ deleted = cursor.rowcount > 0
588
+ conn.commit()
589
+ conn.close()
590
+
591
+ # Update parent counts
592
+ if deleted and parent_id:
593
+ self.update_counts(parent_id)
594
+
595
+ return deleted
596
+
597
+ def get_stats(self) -> Dict[str, Any]:
598
+ """Get tree statistics."""
599
+ conn = sqlite3.connect(self.db_path)
600
+ cursor = conn.cursor()
601
+
602
+ cursor.execute('SELECT COUNT(*) FROM memory_tree')
603
+ total_nodes = cursor.fetchone()[0]
604
+
605
+ cursor.execute('SELECT node_type, COUNT(*) FROM memory_tree GROUP BY node_type')
606
+ by_type = dict(cursor.fetchall())
607
+
608
+ cursor.execute('SELECT MAX(depth) FROM memory_tree')
609
+ max_depth = cursor.fetchone()[0] or 0
610
+
611
+ cursor.execute('''
612
+ SELECT SUM(memory_count), SUM(total_size)
613
+ FROM memory_tree
614
+ WHERE node_type = 'root'
615
+ ''')
616
+ total_memories, total_size = cursor.fetchone()
617
+
618
+ conn.close()
619
+
620
+ return {
621
+ 'total_nodes': total_nodes,
622
+ 'by_type': by_type,
623
+ 'max_depth': max_depth,
624
+ 'total_memories': total_memories or 0,
625
+ 'total_size_bytes': total_size or 0
626
+ }
627
+
628
+
629
+ # CLI interface
630
+ if __name__ == "__main__":
631
+ import sys
632
+ import json
633
+
634
+ tree_mgr = TreeManager()
635
+
636
+ if len(sys.argv) < 2:
637
+ print("TreeManager CLI")
638
+ print("\nCommands:")
639
+ print(" python tree_manager.py build # Build tree from memories")
640
+ print(" python tree_manager.py show [project] [depth] # Show tree structure")
641
+ print(" python tree_manager.py subtree <node_id> # Get subtree")
642
+ print(" python tree_manager.py path <node_id> # Get path to root")
643
+ print(" python tree_manager.py stats # Show statistics")
644
+ print(" python tree_manager.py add <type> <name> <parent_id> # Add node")
645
+ print(" python tree_manager.py delete <node_id> # Delete node")
646
+ sys.exit(0)
647
+
648
+ command = sys.argv[1]
649
+
650
+ if command == "build":
651
+ print("Building tree from memories...")
652
+ tree_mgr.build_tree()
653
+ stats = tree_mgr.get_stats()
654
+ print(f"Tree built: {stats['total_nodes']} nodes, {stats['total_memories']} memories")
655
+
656
+ elif command == "show":
657
+ project = sys.argv[2] if len(sys.argv) > 2 else None
658
+ max_depth = int(sys.argv[3]) if len(sys.argv) > 3 else None
659
+
660
+ tree = tree_mgr.get_tree(project, max_depth)
661
+
662
+ def print_tree(node, indent=0):
663
+ if 'error' in node:
664
+ print(node['error'])
665
+ return
666
+
667
+ prefix = " " * indent
668
+ icon = {"root": "🌳", "project": "📁", "category": "📂", "memory": "📄"}.get(node['type'], "•")
669
+
670
+ print(f"{prefix}{icon} {node['name']} (id={node['id']}, memories={node['memory_count']})")
671
+
672
+ for child in node.get('children', []):
673
+ print_tree(child, indent + 1)
674
+
675
+ print_tree(tree)
676
+
677
+ elif command == "subtree" and len(sys.argv) >= 3:
678
+ node_id = int(sys.argv[2])
679
+ nodes = tree_mgr.get_subtree(node_id)
680
+
681
+ if not nodes:
682
+ print(f"No subtree found for node {node_id}")
683
+ else:
684
+ print(f"Subtree of node {node_id}:")
685
+ for node in nodes:
686
+ indent = " " * (node['depth'] - nodes[0]['depth'] + 1)
687
+ print(f"{indent}- {node['name']} (id={node['id']})")
688
+
689
+ elif command == "path" and len(sys.argv) >= 3:
690
+ node_id = int(sys.argv[2])
691
+ path = tree_mgr.get_path_to_root(node_id)
692
+
693
+ if not path:
694
+ print(f"Node {node_id} not found")
695
+ else:
696
+ print("Path to root:")
697
+ print(" > ".join([f"{n['name']} (id={n['id']})" for n in path]))
698
+
699
+ elif command == "stats":
700
+ stats = tree_mgr.get_stats()
701
+ print(json.dumps(stats, indent=2))
702
+
703
+ elif command == "add" and len(sys.argv) >= 5:
704
+ node_type = sys.argv[2]
705
+ name = sys.argv[3]
706
+ parent_id = int(sys.argv[4])
707
+
708
+ node_id = tree_mgr.add_node(node_type, name, parent_id)
709
+ print(f"Node created with ID: {node_id}")
710
+
711
+ elif command == "delete" and len(sys.argv) >= 3:
712
+ node_id = int(sys.argv[2])
713
+ if tree_mgr.delete_node(node_id):
714
+ print(f"Node {node_id} deleted")
715
+ else:
716
+ print(f"Node {node_id} not found")
717
+
718
+ else:
719
+ print(f"Unknown command: {command}")
720
+ sys.exit(1)