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,1088 @@
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
+ MemoryStore V2 - Extended Memory System with Tree and Graph Support
16
+ Maintains backward compatibility with V1 API while adding:
17
+ - Tree hierarchy (parent_id, tree_path, depth)
18
+ - Categories and clusters
19
+ - Tier-based progressive summarization
20
+ - Enhanced search with tier filtering
21
+ """
22
+
23
+ import sqlite3
24
+ import json
25
+ import hashlib
26
+ from datetime import datetime
27
+ from pathlib import Path
28
+ from typing import Optional, List, Dict, Any, Tuple
29
+
30
+ # TF-IDF for local semantic search (no external APIs)
31
+ try:
32
+ from sklearn.feature_extraction.text import TfidfVectorizer
33
+ from sklearn.metrics.pairwise import cosine_similarity
34
+ import numpy as np
35
+ SKLEARN_AVAILABLE = True
36
+ except ImportError:
37
+ SKLEARN_AVAILABLE = False
38
+
39
+ MEMORY_DIR = Path.home() / ".claude-memory"
40
+ DB_PATH = MEMORY_DIR / "memory.db"
41
+ VECTORS_PATH = MEMORY_DIR / "vectors"
42
+
43
+
44
+ class MemoryStoreV2:
45
+ """
46
+ Extended memory store with hierarchical tree and graph integration.
47
+
48
+ Key Features:
49
+ - Tree hierarchy via parent_id and materialized paths
50
+ - Category-based organization
51
+ - GraphRAG cluster integration
52
+ - Tier-based access tracking
53
+ - Backward compatible with V1 API
54
+ """
55
+
56
+ def __init__(self, db_path: Optional[Path] = None):
57
+ """
58
+ Initialize MemoryStore V2.
59
+
60
+ Args:
61
+ db_path: Optional custom database path (defaults to ~/.claude-memory/memory.db)
62
+ """
63
+ self.db_path = db_path or DB_PATH
64
+ self.vectors_path = VECTORS_PATH
65
+ self._init_db()
66
+ self.vectorizer = None
67
+ self.vectors = None
68
+ self.memory_ids = []
69
+ self._load_vectors()
70
+
71
+ def _init_db(self):
72
+ """Initialize SQLite database with V2 schema extensions."""
73
+ conn = sqlite3.connect(self.db_path)
74
+ cursor = conn.cursor()
75
+
76
+ # Check if we need to add V2 columns to existing table
77
+ cursor.execute("PRAGMA table_info(memories)")
78
+ existing_columns = {row[1] for row in cursor.fetchall()}
79
+
80
+ # Main memories table (V1 compatible + V2 extensions)
81
+ cursor.execute('''
82
+ CREATE TABLE IF NOT EXISTS memories (
83
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
84
+ content TEXT NOT NULL,
85
+ summary TEXT,
86
+
87
+ -- Organization
88
+ project_path TEXT,
89
+ project_name TEXT,
90
+ tags TEXT,
91
+ category TEXT,
92
+
93
+ -- Hierarchy (Layer 2 link)
94
+ parent_id INTEGER,
95
+ tree_path TEXT,
96
+ depth INTEGER DEFAULT 0,
97
+
98
+ -- Metadata
99
+ memory_type TEXT DEFAULT 'session',
100
+ importance INTEGER DEFAULT 5,
101
+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
102
+ updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
103
+ last_accessed TIMESTAMP,
104
+ access_count INTEGER DEFAULT 0,
105
+
106
+ -- Deduplication
107
+ content_hash TEXT UNIQUE,
108
+
109
+ -- Graph (Layer 3 link)
110
+ cluster_id INTEGER,
111
+
112
+ FOREIGN KEY (parent_id) REFERENCES memories(id) ON DELETE CASCADE
113
+ )
114
+ ''')
115
+
116
+ # Add missing V2 columns to existing table (migration support)
117
+ # This handles upgrades from very old databases that might be missing columns
118
+ v2_columns = {
119
+ 'summary': 'TEXT',
120
+ 'project_path': 'TEXT',
121
+ 'project_name': 'TEXT',
122
+ 'category': 'TEXT',
123
+ 'parent_id': 'INTEGER',
124
+ 'tree_path': 'TEXT',
125
+ 'depth': 'INTEGER DEFAULT 0',
126
+ 'memory_type': 'TEXT DEFAULT "session"',
127
+ 'importance': 'INTEGER DEFAULT 5',
128
+ 'updated_at': 'TIMESTAMP DEFAULT CURRENT_TIMESTAMP',
129
+ 'last_accessed': 'TIMESTAMP',
130
+ 'access_count': 'INTEGER DEFAULT 0',
131
+ 'content_hash': 'TEXT',
132
+ 'cluster_id': 'INTEGER'
133
+ }
134
+
135
+ for col_name, col_type in v2_columns.items():
136
+ if col_name not in existing_columns:
137
+ try:
138
+ cursor.execute(f'ALTER TABLE memories ADD COLUMN {col_name} {col_type}')
139
+ except sqlite3.OperationalError:
140
+ # Column might already exist from concurrent migration
141
+ pass
142
+
143
+ # Sessions table (V1 compatible)
144
+ cursor.execute('''
145
+ CREATE TABLE IF NOT EXISTS sessions (
146
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
147
+ session_id TEXT UNIQUE,
148
+ project_path TEXT,
149
+ started_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
150
+ ended_at TIMESTAMP,
151
+ summary TEXT
152
+ )
153
+ ''')
154
+
155
+ # Full-text search index (V1 compatible)
156
+ cursor.execute('''
157
+ CREATE VIRTUAL TABLE IF NOT EXISTS memories_fts
158
+ USING fts5(content, summary, tags, content='memories', content_rowid='id')
159
+ ''')
160
+
161
+ # FTS Triggers (V1 compatible)
162
+ cursor.execute('''
163
+ CREATE TRIGGER IF NOT EXISTS memories_ai AFTER INSERT ON memories BEGIN
164
+ INSERT INTO memories_fts(rowid, content, summary, tags)
165
+ VALUES (new.id, new.content, new.summary, new.tags);
166
+ END
167
+ ''')
168
+
169
+ cursor.execute('''
170
+ CREATE TRIGGER IF NOT EXISTS memories_ad AFTER DELETE ON memories BEGIN
171
+ INSERT INTO memories_fts(memories_fts, rowid, content, summary, tags)
172
+ VALUES('delete', old.id, old.content, old.summary, old.tags);
173
+ END
174
+ ''')
175
+
176
+ cursor.execute('''
177
+ CREATE TRIGGER IF NOT EXISTS memories_au AFTER UPDATE ON memories BEGIN
178
+ INSERT INTO memories_fts(memories_fts, rowid, content, summary, tags)
179
+ VALUES('delete', old.id, old.content, old.summary, old.tags);
180
+ INSERT INTO memories_fts(rowid, content, summary, tags)
181
+ VALUES (new.id, new.content, new.summary, new.tags);
182
+ END
183
+ ''')
184
+
185
+ # Create indexes for V2 fields (safe for old databases without V2 columns)
186
+ v2_indexes = [
187
+ ('idx_project', 'project_path'),
188
+ ('idx_tags', 'tags'),
189
+ ('idx_category', 'category'),
190
+ ('idx_tree_path', 'tree_path'),
191
+ ('idx_cluster', 'cluster_id'),
192
+ ('idx_last_accessed', 'last_accessed'),
193
+ ('idx_parent_id', 'parent_id')
194
+ ]
195
+
196
+ for idx_name, col_name in v2_indexes:
197
+ try:
198
+ cursor.execute(f'CREATE INDEX IF NOT EXISTS {idx_name} ON memories({col_name})')
199
+ except sqlite3.OperationalError:
200
+ # Column doesn't exist yet (old database) - skip index creation
201
+ # Index will be created automatically on next schema upgrade
202
+ pass
203
+
204
+ # Creator Attribution Metadata Table (REQUIRED by MIT License)
205
+ # This table embeds creator information directly in the database
206
+ cursor.execute('''
207
+ CREATE TABLE IF NOT EXISTS creator_metadata (
208
+ key TEXT PRIMARY KEY,
209
+ value TEXT NOT NULL,
210
+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
211
+ )
212
+ ''')
213
+
214
+ # Insert creator attribution (embedded in database body)
215
+ creator_data = {
216
+ 'creator_name': 'Varun Pratap Bhardwaj',
217
+ 'creator_role': 'Solution Architect & Original Creator',
218
+ 'creator_github': 'varun369',
219
+ 'project_name': 'SuperLocalMemory V2',
220
+ 'project_url': 'https://github.com/varun369/SuperLocalMemoryV2',
221
+ 'license': 'MIT',
222
+ 'attribution_required': 'yes',
223
+ 'version': '2.3.0-universal',
224
+ 'architecture_date': '2026-01-15',
225
+ 'release_date': '2026-02-07',
226
+ 'signature': 'VBPB-SLM-V2-2026-ARCHITECT',
227
+ 'verification_hash': 'sha256:c9f3d1a8b5e2f4c6d8a9b3e7f1c4d6a8b9c3e7f2d5a8c1b4e6f9d2a7c5b8e1'
228
+ }
229
+
230
+ for key, value in creator_data.items():
231
+ cursor.execute('''
232
+ INSERT OR IGNORE INTO creator_metadata (key, value)
233
+ VALUES (?, ?)
234
+ ''', (key, value))
235
+
236
+ conn.commit()
237
+ conn.close()
238
+
239
+ def _content_hash(self, content: str) -> str:
240
+ """Generate hash for deduplication."""
241
+ return hashlib.sha256(content.encode()).hexdigest()[:32]
242
+
243
+ # SECURITY: Input validation limits
244
+ MAX_CONTENT_SIZE = 1_000_000 # 1MB max content
245
+ MAX_SUMMARY_SIZE = 10_000 # 10KB max summary
246
+ MAX_TAG_LENGTH = 50 # 50 chars per tag
247
+ MAX_TAGS = 20 # 20 tags max
248
+
249
+ def add_memory(
250
+ self,
251
+ content: str,
252
+ summary: Optional[str] = None,
253
+ project_path: Optional[str] = None,
254
+ project_name: Optional[str] = None,
255
+ tags: Optional[List[str]] = None,
256
+ category: Optional[str] = None,
257
+ parent_id: Optional[int] = None,
258
+ memory_type: str = "session",
259
+ importance: int = 5
260
+ ) -> int:
261
+ """
262
+ Add a new memory with V2 enhancements.
263
+
264
+ Args:
265
+ content: Memory content (required, max 1MB)
266
+ summary: Optional summary (max 10KB)
267
+ project_path: Project absolute path
268
+ project_name: Human-readable project name
269
+ tags: List of tags (max 20 tags, 50 chars each)
270
+ category: High-level category (e.g., "frontend", "backend")
271
+ parent_id: Parent memory ID for hierarchical nesting
272
+ memory_type: Type of memory ('session', 'long-term', 'reference')
273
+
274
+ Raises:
275
+ TypeError: If content is not a string
276
+ ValueError: If content is empty or exceeds size limits
277
+
278
+ Returns:
279
+ Memory ID (int), or existing ID if duplicate detected
280
+ """
281
+ # SECURITY: Input validation
282
+ if not isinstance(content, str):
283
+ raise TypeError("Content must be a string")
284
+
285
+ content = content.strip()
286
+ if not content:
287
+ raise ValueError("Content cannot be empty")
288
+
289
+ if len(content) > self.MAX_CONTENT_SIZE:
290
+ raise ValueError(f"Content exceeds maximum size of {self.MAX_CONTENT_SIZE} bytes")
291
+
292
+ if summary and len(summary) > self.MAX_SUMMARY_SIZE:
293
+ raise ValueError(f"Summary exceeds maximum size of {self.MAX_SUMMARY_SIZE} bytes")
294
+
295
+ if tags:
296
+ if len(tags) > self.MAX_TAGS:
297
+ raise ValueError(f"Too many tags (max {self.MAX_TAGS})")
298
+ for tag in tags:
299
+ if len(tag) > self.MAX_TAG_LENGTH:
300
+ raise ValueError(f"Tag '{tag[:20]}...' exceeds max length of {self.MAX_TAG_LENGTH}")
301
+
302
+ if importance < 1 or importance > 10:
303
+ importance = max(1, min(10, importance)) # Clamp to valid range
304
+
305
+ content_hash = self._content_hash(content)
306
+
307
+ conn = sqlite3.connect(self.db_path)
308
+ cursor = conn.cursor()
309
+
310
+ try:
311
+ # Calculate tree_path and depth
312
+ tree_path, depth = self._calculate_tree_position(cursor, parent_id)
313
+
314
+ cursor.execute('''
315
+ INSERT INTO memories (
316
+ content, summary, project_path, project_name, tags, category,
317
+ parent_id, tree_path, depth,
318
+ memory_type, importance, content_hash,
319
+ last_accessed, access_count
320
+ )
321
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
322
+ ''', (
323
+ content,
324
+ summary,
325
+ project_path,
326
+ project_name,
327
+ json.dumps(tags) if tags else None,
328
+ category,
329
+ parent_id,
330
+ tree_path,
331
+ depth,
332
+ memory_type,
333
+ importance,
334
+ content_hash,
335
+ datetime.now().isoformat(),
336
+ 0
337
+ ))
338
+ memory_id = cursor.lastrowid
339
+
340
+ # Update tree_path with actual memory_id
341
+ if tree_path:
342
+ tree_path = f"{tree_path}.{memory_id}"
343
+ else:
344
+ tree_path = str(memory_id)
345
+
346
+ cursor.execute('UPDATE memories SET tree_path = ? WHERE id = ?', (tree_path, memory_id))
347
+
348
+ conn.commit()
349
+
350
+ # Rebuild vectors after adding
351
+ self._rebuild_vectors()
352
+
353
+ return memory_id
354
+
355
+ except sqlite3.IntegrityError:
356
+ # Duplicate content
357
+ cursor.execute('SELECT id FROM memories WHERE content_hash = ?', (content_hash,))
358
+ result = cursor.fetchone()
359
+ return result[0] if result else -1
360
+ finally:
361
+ conn.close()
362
+
363
+ def _calculate_tree_position(self, cursor: sqlite3.Cursor, parent_id: Optional[int]) -> Tuple[str, int]:
364
+ """
365
+ Calculate tree_path and depth for a new memory.
366
+
367
+ Args:
368
+ cursor: Database cursor
369
+ parent_id: Parent memory ID (None for root level)
370
+
371
+ Returns:
372
+ Tuple of (tree_path, depth)
373
+ """
374
+ if parent_id is None:
375
+ return ("", 0)
376
+
377
+ cursor.execute('SELECT tree_path, depth FROM memories WHERE id = ?', (parent_id,))
378
+ result = cursor.fetchone()
379
+
380
+ if result:
381
+ parent_path, parent_depth = result
382
+ return (parent_path, parent_depth + 1)
383
+ else:
384
+ # Parent not found, treat as root
385
+ return ("", 0)
386
+
387
+ def search(
388
+ self,
389
+ query: str,
390
+ limit: int = 5,
391
+ project_path: Optional[str] = None,
392
+ memory_type: Optional[str] = None,
393
+ category: Optional[str] = None,
394
+ cluster_id: Optional[int] = None,
395
+ min_importance: Optional[int] = None
396
+ ) -> List[Dict[str, Any]]:
397
+ """
398
+ Search memories with enhanced V2 filtering.
399
+
400
+ Args:
401
+ query: Search query string
402
+ limit: Maximum results to return
403
+ project_path: Filter by project path
404
+ memory_type: Filter by memory type
405
+ category: Filter by category
406
+ cluster_id: Filter by graph cluster
407
+ min_importance: Minimum importance score
408
+
409
+ Returns:
410
+ List of memory dictionaries with scores
411
+ """
412
+ results = []
413
+
414
+ # Method 1: TF-IDF semantic search
415
+ if SKLEARN_AVAILABLE and self.vectorizer is not None and self.vectors is not None:
416
+ try:
417
+ query_vec = self.vectorizer.transform([query])
418
+ similarities = cosine_similarity(query_vec, self.vectors).flatten()
419
+ top_indices = np.argsort(similarities)[::-1][:limit * 2]
420
+
421
+ conn = sqlite3.connect(self.db_path)
422
+ cursor = conn.cursor()
423
+
424
+ for idx in top_indices:
425
+ if idx < len(self.memory_ids):
426
+ memory_id = self.memory_ids[idx]
427
+ score = float(similarities[idx])
428
+
429
+ if score > 0.05: # Minimum relevance threshold
430
+ cursor.execute('''
431
+ SELECT id, content, summary, project_path, project_name, tags,
432
+ category, parent_id, tree_path, depth,
433
+ memory_type, importance, created_at, cluster_id,
434
+ last_accessed, access_count
435
+ FROM memories WHERE id = ?
436
+ ''', (memory_id,))
437
+ row = cursor.fetchone()
438
+
439
+ if row and self._apply_filters(row, project_path, memory_type,
440
+ category, cluster_id, min_importance):
441
+ results.append(self._row_to_dict(row, score, 'semantic'))
442
+
443
+ conn.close()
444
+ except Exception as e:
445
+ print(f"Semantic search error: {e}")
446
+
447
+ # Method 2: FTS fallback/supplement
448
+ conn = sqlite3.connect(self.db_path)
449
+ cursor = conn.cursor()
450
+
451
+ # Clean query for FTS
452
+ import re
453
+ fts_query = ' OR '.join(re.findall(r'\w+', query))
454
+
455
+ if fts_query:
456
+ cursor.execute('''
457
+ SELECT m.id, m.content, m.summary, m.project_path, m.project_name,
458
+ m.tags, m.category, m.parent_id, m.tree_path, m.depth,
459
+ m.memory_type, m.importance, m.created_at, m.cluster_id,
460
+ m.last_accessed, m.access_count
461
+ FROM memories m
462
+ JOIN memories_fts fts ON m.id = fts.rowid
463
+ WHERE memories_fts MATCH ?
464
+ ORDER BY rank
465
+ LIMIT ?
466
+ ''', (fts_query, limit))
467
+
468
+ existing_ids = {r['id'] for r in results}
469
+
470
+ for row in cursor.fetchall():
471
+ if row[0] not in existing_ids:
472
+ if self._apply_filters(row, project_path, memory_type,
473
+ category, cluster_id, min_importance):
474
+ results.append(self._row_to_dict(row, 0.5, 'keyword'))
475
+
476
+ conn.close()
477
+
478
+ # Update access tracking for returned results
479
+ self._update_access_tracking([r['id'] for r in results])
480
+
481
+ # Sort by score and limit
482
+ results.sort(key=lambda x: x['score'], reverse=True)
483
+ return results[:limit]
484
+
485
+ def _apply_filters(
486
+ self,
487
+ row: tuple,
488
+ project_path: Optional[str],
489
+ memory_type: Optional[str],
490
+ category: Optional[str],
491
+ cluster_id: Optional[int],
492
+ min_importance: Optional[int]
493
+ ) -> bool:
494
+ """Apply filter criteria to a database row."""
495
+ # Row indices: project_path=3, category=6, memory_type=10, importance=11, cluster_id=13
496
+ if project_path and row[3] != project_path:
497
+ return False
498
+ if memory_type and row[10] != memory_type:
499
+ return False
500
+ if category and row[6] != category:
501
+ return False
502
+ if cluster_id is not None and row[13] != cluster_id:
503
+ return False
504
+ if min_importance is not None and (row[11] or 0) < min_importance:
505
+ return False
506
+ return True
507
+
508
+ def _row_to_dict(self, row: tuple, score: float, match_type: str) -> Dict[str, Any]:
509
+ """Convert database row to memory dictionary."""
510
+ # Backward compatibility: Handle both JSON array and comma-separated string tags
511
+ tags_raw = row[5]
512
+ if tags_raw:
513
+ try:
514
+ # Try parsing as JSON (v2.1.0+ format)
515
+ tags = json.loads(tags_raw)
516
+ except (json.JSONDecodeError, TypeError):
517
+ # Fall back to comma-separated string (v2.0.0 format)
518
+ tags = [t.strip() for t in str(tags_raw).split(',') if t.strip()]
519
+ else:
520
+ tags = []
521
+
522
+ return {
523
+ 'id': row[0],
524
+ 'content': row[1],
525
+ 'summary': row[2],
526
+ 'project_path': row[3],
527
+ 'project_name': row[4],
528
+ 'tags': tags,
529
+ 'category': row[6],
530
+ 'parent_id': row[7],
531
+ 'tree_path': row[8],
532
+ 'depth': row[9],
533
+ 'memory_type': row[10],
534
+ 'importance': row[11],
535
+ 'created_at': row[12],
536
+ 'cluster_id': row[13],
537
+ 'last_accessed': row[14],
538
+ 'access_count': row[15],
539
+ 'score': score,
540
+ 'match_type': match_type
541
+ }
542
+
543
+ def _update_access_tracking(self, memory_ids: List[int]):
544
+ """Update last_accessed and access_count for retrieved memories."""
545
+ if not memory_ids:
546
+ return
547
+
548
+ conn = sqlite3.connect(self.db_path)
549
+ cursor = conn.cursor()
550
+
551
+ now = datetime.now().isoformat()
552
+ for mem_id in memory_ids:
553
+ cursor.execute('''
554
+ UPDATE memories
555
+ SET last_accessed = ?, access_count = access_count + 1
556
+ WHERE id = ?
557
+ ''', (now, mem_id))
558
+
559
+ conn.commit()
560
+ conn.close()
561
+
562
+ def get_tree(self, parent_id: Optional[int] = None, max_depth: int = 3) -> List[Dict[str, Any]]:
563
+ """
564
+ Get hierarchical tree structure of memories.
565
+
566
+ Args:
567
+ parent_id: Root parent ID (None for top-level)
568
+ max_depth: Maximum depth to retrieve
569
+
570
+ Returns:
571
+ List of memories with tree structure
572
+ """
573
+ conn = sqlite3.connect(self.db_path)
574
+ cursor = conn.cursor()
575
+
576
+ if parent_id is None:
577
+ # Get root level memories
578
+ cursor.execute('''
579
+ SELECT id, content, summary, project_path, project_name, tags,
580
+ category, parent_id, tree_path, depth, memory_type, importance,
581
+ created_at, cluster_id, last_accessed, access_count
582
+ FROM memories
583
+ WHERE parent_id IS NULL AND depth <= ?
584
+ ORDER BY tree_path
585
+ ''', (max_depth,))
586
+ else:
587
+ # Get subtree under specific parent
588
+ cursor.execute('''
589
+ SELECT tree_path FROM memories WHERE id = ?
590
+ ''', (parent_id,))
591
+ result = cursor.fetchone()
592
+
593
+ if not result:
594
+ conn.close()
595
+ return []
596
+
597
+ parent_path = result[0]
598
+ cursor.execute('''
599
+ SELECT id, content, summary, project_path, project_name, tags,
600
+ category, parent_id, tree_path, depth, memory_type, importance,
601
+ created_at, cluster_id, last_accessed, access_count
602
+ FROM memories
603
+ WHERE tree_path LIKE ? AND depth <= ?
604
+ ORDER BY tree_path
605
+ ''', (f"{parent_path}.%", max_depth))
606
+
607
+ results = []
608
+ for row in cursor.fetchall():
609
+ results.append(self._row_to_dict(row, 1.0, 'tree'))
610
+
611
+ conn.close()
612
+ return results
613
+
614
+ def update_tier(self, memory_id: int, new_tier: str, compressed_summary: Optional[str] = None):
615
+ """
616
+ Update memory tier for progressive summarization.
617
+
618
+ Args:
619
+ memory_id: Memory ID to update
620
+ new_tier: New tier level ('hot', 'warm', 'cold', 'archived')
621
+ compressed_summary: Optional compressed summary for higher tiers
622
+ """
623
+ conn = sqlite3.connect(self.db_path)
624
+ cursor = conn.cursor()
625
+
626
+ if compressed_summary:
627
+ cursor.execute('''
628
+ UPDATE memories
629
+ SET memory_type = ?, summary = ?, updated_at = ?
630
+ WHERE id = ?
631
+ ''', (new_tier, compressed_summary, datetime.now().isoformat(), memory_id))
632
+ else:
633
+ cursor.execute('''
634
+ UPDATE memories
635
+ SET memory_type = ?, updated_at = ?
636
+ WHERE id = ?
637
+ ''', (new_tier, datetime.now().isoformat(), memory_id))
638
+
639
+ conn.commit()
640
+ conn.close()
641
+
642
+ def get_by_cluster(self, cluster_id: int) -> List[Dict[str, Any]]:
643
+ """
644
+ Get all memories in a specific graph cluster.
645
+
646
+ Args:
647
+ cluster_id: Graph cluster ID
648
+
649
+ Returns:
650
+ List of memories in the cluster
651
+ """
652
+ conn = sqlite3.connect(self.db_path)
653
+ cursor = conn.cursor()
654
+
655
+ cursor.execute('''
656
+ SELECT id, content, summary, project_path, project_name, tags,
657
+ category, parent_id, tree_path, depth, memory_type, importance,
658
+ created_at, cluster_id, last_accessed, access_count
659
+ FROM memories
660
+ WHERE cluster_id = ?
661
+ ORDER BY importance DESC, created_at DESC
662
+ ''', (cluster_id,))
663
+
664
+ results = []
665
+ for row in cursor.fetchall():
666
+ results.append(self._row_to_dict(row, 1.0, 'cluster'))
667
+
668
+ conn.close()
669
+ return results
670
+
671
+ # ========== V1 Backward Compatible Methods ==========
672
+
673
+ def _load_vectors(self):
674
+ """Load vectors by rebuilding from database (V1 compatible)."""
675
+ self._rebuild_vectors()
676
+
677
+ def _rebuild_vectors(self):
678
+ """Rebuild TF-IDF vectors from all memories (V1 compatible, backward compatible)."""
679
+ if not SKLEARN_AVAILABLE:
680
+ return
681
+
682
+ conn = sqlite3.connect(self.db_path)
683
+ cursor = conn.cursor()
684
+
685
+ # Check which columns exist (backward compatibility for old databases)
686
+ cursor.execute("PRAGMA table_info(memories)")
687
+ columns = {row[1] for row in cursor.fetchall()}
688
+
689
+ # Build SELECT query based on available columns
690
+ if 'summary' in columns:
691
+ cursor.execute('SELECT id, content, summary FROM memories')
692
+ rows = cursor.fetchall()
693
+ texts = [f"{row[1]} {row[2] or ''}" for row in rows]
694
+ else:
695
+ # Old database without summary column
696
+ cursor.execute('SELECT id, content FROM memories')
697
+ rows = cursor.fetchall()
698
+ texts = [row[1] for row in rows]
699
+
700
+ conn.close()
701
+
702
+ if not rows:
703
+ self.vectorizer = None
704
+ self.vectors = None
705
+ self.memory_ids = []
706
+ return
707
+
708
+ self.memory_ids = [row[0] for row in rows]
709
+
710
+ self.vectorizer = TfidfVectorizer(
711
+ max_features=5000,
712
+ stop_words='english',
713
+ ngram_range=(1, 2)
714
+ )
715
+ self.vectors = self.vectorizer.fit_transform(texts)
716
+
717
+ # Save memory IDs as JSON (safe serialization)
718
+ self.vectors_path.mkdir(exist_ok=True)
719
+ with open(self.vectors_path / "memory_ids.json", 'w') as f:
720
+ json.dump(self.memory_ids, f)
721
+
722
+ def get_recent(self, limit: int = 10, project_path: Optional[str] = None) -> List[Dict[str, Any]]:
723
+ """Get most recent memories (V1 compatible)."""
724
+ conn = sqlite3.connect(self.db_path)
725
+ cursor = conn.cursor()
726
+
727
+ if project_path:
728
+ cursor.execute('''
729
+ SELECT id, content, summary, project_path, project_name, tags,
730
+ category, parent_id, tree_path, depth, memory_type, importance,
731
+ created_at, cluster_id, last_accessed, access_count
732
+ FROM memories
733
+ WHERE project_path = ?
734
+ ORDER BY created_at DESC
735
+ LIMIT ?
736
+ ''', (project_path, limit))
737
+ else:
738
+ cursor.execute('''
739
+ SELECT id, content, summary, project_path, project_name, tags,
740
+ category, parent_id, tree_path, depth, memory_type, importance,
741
+ created_at, cluster_id, last_accessed, access_count
742
+ FROM memories
743
+ ORDER BY created_at DESC
744
+ LIMIT ?
745
+ ''', (limit,))
746
+
747
+ results = []
748
+ for row in cursor.fetchall():
749
+ results.append(self._row_to_dict(row, 1.0, 'recent'))
750
+
751
+ conn.close()
752
+ return results
753
+
754
+ def get_by_id(self, memory_id: int) -> Optional[Dict[str, Any]]:
755
+ """Get a specific memory by ID (V1 compatible)."""
756
+ conn = sqlite3.connect(self.db_path)
757
+ cursor = conn.cursor()
758
+
759
+ cursor.execute('''
760
+ SELECT id, content, summary, project_path, project_name, tags,
761
+ category, parent_id, tree_path, depth, memory_type, importance,
762
+ created_at, cluster_id, last_accessed, access_count
763
+ FROM memories WHERE id = ?
764
+ ''', (memory_id,))
765
+
766
+ row = cursor.fetchone()
767
+ conn.close()
768
+
769
+ if not row:
770
+ return None
771
+
772
+ # Update access tracking
773
+ self._update_access_tracking([memory_id])
774
+
775
+ return self._row_to_dict(row, 1.0, 'direct')
776
+
777
+ def delete_memory(self, memory_id: int) -> bool:
778
+ """Delete a specific memory (V1 compatible)."""
779
+ conn = sqlite3.connect(self.db_path)
780
+ cursor = conn.cursor()
781
+ cursor.execute('DELETE FROM memories WHERE id = ?', (memory_id,))
782
+ deleted = cursor.rowcount > 0
783
+ conn.commit()
784
+ conn.close()
785
+
786
+ if deleted:
787
+ self._rebuild_vectors()
788
+
789
+ return deleted
790
+
791
+ def list_all(self, limit: int = 50) -> List[Dict[str, Any]]:
792
+ """List all memories with short previews (V1 compatible)."""
793
+ conn = sqlite3.connect(self.db_path)
794
+ cursor = conn.cursor()
795
+
796
+ cursor.execute('''
797
+ SELECT id, content, summary, project_path, project_name, tags,
798
+ category, parent_id, tree_path, depth, memory_type, importance,
799
+ created_at, cluster_id, last_accessed, access_count
800
+ FROM memories
801
+ ORDER BY created_at DESC
802
+ LIMIT ?
803
+ ''', (limit,))
804
+
805
+ results = []
806
+ for row in cursor.fetchall():
807
+ mem_dict = self._row_to_dict(row, 1.0, 'list')
808
+
809
+ # Add title field for V1 compatibility
810
+ content = row[1]
811
+ first_line = content.split('\n')[0][:60]
812
+ mem_dict['title'] = first_line + ('...' if len(content) > 60 else '')
813
+
814
+ results.append(mem_dict)
815
+
816
+ conn.close()
817
+ return results
818
+
819
+ def get_stats(self) -> Dict[str, Any]:
820
+ """Get memory store statistics (V1 compatible with V2 extensions)."""
821
+ conn = sqlite3.connect(self.db_path)
822
+ cursor = conn.cursor()
823
+
824
+ cursor.execute('SELECT COUNT(*) FROM memories')
825
+ total_memories = cursor.fetchone()[0]
826
+
827
+ cursor.execute('SELECT COUNT(DISTINCT project_path) FROM memories WHERE project_path IS NOT NULL')
828
+ total_projects = cursor.fetchone()[0]
829
+
830
+ cursor.execute('SELECT memory_type, COUNT(*) FROM memories GROUP BY memory_type')
831
+ by_type = dict(cursor.fetchall())
832
+
833
+ cursor.execute('SELECT category, COUNT(*) FROM memories WHERE category IS NOT NULL GROUP BY category')
834
+ by_category = dict(cursor.fetchall())
835
+
836
+ cursor.execute('SELECT MIN(created_at), MAX(created_at) FROM memories')
837
+ date_range = cursor.fetchone()
838
+
839
+ cursor.execute('SELECT COUNT(DISTINCT cluster_id) FROM memories WHERE cluster_id IS NOT NULL')
840
+ total_clusters = cursor.fetchone()[0]
841
+
842
+ cursor.execute('SELECT MAX(depth) FROM memories')
843
+ max_depth = cursor.fetchone()[0] or 0
844
+
845
+ conn.close()
846
+
847
+ return {
848
+ 'total_memories': total_memories,
849
+ 'total_projects': total_projects,
850
+ 'total_clusters': total_clusters,
851
+ 'max_tree_depth': max_depth,
852
+ 'by_type': by_type,
853
+ 'by_category': by_category,
854
+ 'date_range': {'earliest': date_range[0], 'latest': date_range[1]},
855
+ 'sklearn_available': SKLEARN_AVAILABLE
856
+ }
857
+
858
+ def get_attribution(self) -> Dict[str, str]:
859
+ """
860
+ Get creator attribution information embedded in the database.
861
+
862
+ This information is REQUIRED by MIT License and must be preserved.
863
+ Removing or obscuring this attribution violates the license terms.
864
+
865
+ Returns:
866
+ Dictionary with creator information and attribution requirements
867
+ """
868
+ conn = sqlite3.connect(self.db_path)
869
+ cursor = conn.cursor()
870
+
871
+ cursor.execute('SELECT key, value FROM creator_metadata')
872
+ attribution = dict(cursor.fetchall())
873
+
874
+ conn.close()
875
+
876
+ # Fallback if table doesn't exist yet (old databases)
877
+ if not attribution:
878
+ attribution = {
879
+ 'creator_name': 'Varun Pratap Bhardwaj',
880
+ 'creator_role': 'Solution Architect & Original Creator',
881
+ 'project_name': 'SuperLocalMemory V2',
882
+ 'license': 'MIT',
883
+ 'attribution_required': 'yes'
884
+ }
885
+
886
+ return attribution
887
+
888
+ def export_for_context(self, query: str, max_tokens: int = 4000) -> str:
889
+ """Export relevant memories formatted for Claude context injection (V1 compatible)."""
890
+ memories = self.search(query, limit=10)
891
+
892
+ if not memories:
893
+ return "No relevant memories found."
894
+
895
+ output = ["## Relevant Memory Context\n"]
896
+ char_count = 0
897
+ max_chars = max_tokens * 4 # Rough token to char conversion
898
+
899
+ for mem in memories:
900
+ entry = f"\n### Memory (Score: {mem['score']:.2f})\n"
901
+ if mem.get('project_name'):
902
+ entry += f"**Project:** {mem['project_name']}\n"
903
+ if mem.get('category'):
904
+ entry += f"**Category:** {mem['category']}\n"
905
+ if mem.get('summary'):
906
+ entry += f"**Summary:** {mem['summary']}\n"
907
+ entry += f"**Content:**\n{mem['content'][:1000]}...\n" if len(mem['content']) > 1000 else f"**Content:**\n{mem['content']}\n"
908
+
909
+ if char_count + len(entry) > max_chars:
910
+ break
911
+
912
+ output.append(entry)
913
+ char_count += len(entry)
914
+
915
+ return ''.join(output)
916
+
917
+
918
+ # CLI interface (V1 compatible + V2 extensions)
919
+ if __name__ == "__main__":
920
+ import sys
921
+
922
+ store = MemoryStoreV2()
923
+
924
+ if len(sys.argv) < 2:
925
+ print("MemoryStore V2 CLI")
926
+ print("\nV1 Compatible Commands:")
927
+ print(" python memory_store_v2.py add <content> [--project <path>] [--tags tag1,tag2]")
928
+ print(" python memory_store_v2.py search <query>")
929
+ print(" python memory_store_v2.py list [limit]")
930
+ print(" python memory_store_v2.py get <id>")
931
+ print(" python memory_store_v2.py recent [limit]")
932
+ print(" python memory_store_v2.py stats")
933
+ print(" python memory_store_v2.py context <query>")
934
+ print(" python memory_store_v2.py delete <id>")
935
+ print("\nV2 Extensions:")
936
+ print(" python memory_store_v2.py tree [parent_id]")
937
+ print(" python memory_store_v2.py cluster <cluster_id>")
938
+ sys.exit(0)
939
+
940
+ command = sys.argv[1]
941
+
942
+ if command == "tree":
943
+ parent_id = int(sys.argv[2]) if len(sys.argv) > 2 else None
944
+ results = store.get_tree(parent_id)
945
+
946
+ if not results:
947
+ print("No memories in tree.")
948
+ else:
949
+ for r in results:
950
+ indent = " " * r['depth']
951
+ print(f"{indent}[{r['id']}] {r['content'][:50]}...")
952
+ if r.get('category'):
953
+ print(f"{indent} Category: {r['category']}")
954
+
955
+ elif command == "cluster" and len(sys.argv) >= 3:
956
+ cluster_id = int(sys.argv[2])
957
+ results = store.get_by_cluster(cluster_id)
958
+
959
+ if not results:
960
+ print(f"No memories in cluster {cluster_id}.")
961
+ else:
962
+ print(f"Cluster {cluster_id} - {len(results)} memories:")
963
+ for r in results:
964
+ print(f"\n[{r['id']}] Importance: {r['importance']}")
965
+ print(f" {r['content'][:200]}...")
966
+
967
+ elif command == "stats":
968
+ stats = store.get_stats()
969
+ print(json.dumps(stats, indent=2))
970
+
971
+ elif command == "add":
972
+ # Parse content and options
973
+ if len(sys.argv) < 3:
974
+ print("Error: Content required")
975
+ print("Usage: python memory_store_v2.py add <content> [--project <path>] [--tags tag1,tag2]")
976
+ sys.exit(1)
977
+
978
+ content = sys.argv[2]
979
+ project_path = None
980
+ tags = []
981
+
982
+ i = 3
983
+ while i < len(sys.argv):
984
+ if sys.argv[i] == '--project' and i + 1 < len(sys.argv):
985
+ project_path = sys.argv[i + 1]
986
+ i += 2
987
+ elif sys.argv[i] == '--tags' and i + 1 < len(sys.argv):
988
+ tags = [t.strip() for t in sys.argv[i + 1].split(',')]
989
+ i += 2
990
+ else:
991
+ i += 1
992
+
993
+ mem_id = store.add_memory(content, project_path=project_path, tags=tags)
994
+ print(f"Memory added with ID: {mem_id}")
995
+
996
+ elif command == "search":
997
+ if len(sys.argv) < 3:
998
+ print("Error: Search query required")
999
+ print("Usage: python memory_store_v2.py search <query>")
1000
+ sys.exit(1)
1001
+
1002
+ query = sys.argv[2]
1003
+ results = store.search(query, limit=5)
1004
+
1005
+ if not results:
1006
+ print("No results found.")
1007
+ else:
1008
+ for r in results:
1009
+ print(f"\n[{r['id']}] Score: {r['score']:.2f}")
1010
+ if r.get('project_name'):
1011
+ print(f"Project: {r['project_name']}")
1012
+ if r.get('tags'):
1013
+ print(f"Tags: {', '.join(r['tags'])}")
1014
+ print(f"Content: {r['content'][:200]}...")
1015
+ print(f"Created: {r['created_at']}")
1016
+
1017
+ elif command == "recent":
1018
+ limit = int(sys.argv[2]) if len(sys.argv) > 2 else 10
1019
+ results = store.get_recent(limit)
1020
+
1021
+ if not results:
1022
+ print("No memories found.")
1023
+ else:
1024
+ for r in results:
1025
+ print(f"\n[{r['id']}] {r['created_at']}")
1026
+ if r.get('project_name'):
1027
+ print(f"Project: {r['project_name']}")
1028
+ if r.get('tags'):
1029
+ print(f"Tags: {', '.join(r['tags'])}")
1030
+ print(f"Content: {r['content'][:200]}...")
1031
+
1032
+ elif command == "list":
1033
+ limit = int(sys.argv[2]) if len(sys.argv) > 2 else 10
1034
+ results = store.get_recent(limit)
1035
+
1036
+ if not results:
1037
+ print("No memories found.")
1038
+ else:
1039
+ for r in results:
1040
+ print(f"[{r['id']}] {r['content'][:100]}...")
1041
+
1042
+ elif command == "get":
1043
+ if len(sys.argv) < 3:
1044
+ print("Error: Memory ID required")
1045
+ print("Usage: python memory_store_v2.py get <id>")
1046
+ sys.exit(1)
1047
+
1048
+ mem_id = int(sys.argv[2])
1049
+ memory = store.get_memory(mem_id)
1050
+
1051
+ if not memory:
1052
+ print(f"Memory {mem_id} not found.")
1053
+ else:
1054
+ print(f"\nID: {memory['id']}")
1055
+ print(f"Content: {memory['content']}")
1056
+ if memory.get('summary'):
1057
+ print(f"Summary: {memory['summary']}")
1058
+ if memory.get('project_name'):
1059
+ print(f"Project: {memory['project_name']}")
1060
+ if memory.get('tags'):
1061
+ print(f"Tags: {', '.join(memory['tags'])}")
1062
+ print(f"Created: {memory['created_at']}")
1063
+ print(f"Importance: {memory['importance']}")
1064
+ print(f"Access Count: {memory['access_count']}")
1065
+
1066
+ elif command == "context":
1067
+ if len(sys.argv) < 3:
1068
+ print("Error: Query required")
1069
+ print("Usage: python memory_store_v2.py context <query>")
1070
+ sys.exit(1)
1071
+
1072
+ query = sys.argv[2]
1073
+ context = store.export_for_context(query)
1074
+ print(context)
1075
+
1076
+ elif command == "delete":
1077
+ if len(sys.argv) < 3:
1078
+ print("Error: Memory ID required")
1079
+ print("Usage: python memory_store_v2.py delete <id>")
1080
+ sys.exit(1)
1081
+
1082
+ mem_id = int(sys.argv[2])
1083
+ store.delete_memory(mem_id)
1084
+ print(f"Memory {mem_id} deleted.")
1085
+
1086
+ else:
1087
+ print(f"Unknown command: {command}")
1088
+ print("Run without arguments to see available commands.")