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.
- package/ATTRIBUTION.md +140 -0
- package/CHANGELOG.md +1749 -0
- package/LICENSE +21 -0
- package/README.md +600 -0
- package/bin/aider-smart +72 -0
- package/bin/slm +202 -0
- package/bin/slm-npm +73 -0
- package/bin/slm.bat +195 -0
- package/bin/slm.cmd +10 -0
- package/bin/superlocalmemoryv2:list +3 -0
- package/bin/superlocalmemoryv2:profile +3 -0
- package/bin/superlocalmemoryv2:recall +3 -0
- package/bin/superlocalmemoryv2:remember +3 -0
- package/bin/superlocalmemoryv2:reset +3 -0
- package/bin/superlocalmemoryv2:status +3 -0
- package/completions/slm.bash +58 -0
- package/completions/slm.zsh +76 -0
- package/configs/antigravity-mcp.json +13 -0
- package/configs/chatgpt-desktop-mcp.json +7 -0
- package/configs/claude-desktop-mcp.json +15 -0
- package/configs/codex-mcp.toml +13 -0
- package/configs/cody-commands.json +29 -0
- package/configs/continue-mcp.yaml +14 -0
- package/configs/continue-skills.yaml +26 -0
- package/configs/cursor-mcp.json +15 -0
- package/configs/gemini-cli-mcp.json +11 -0
- package/configs/jetbrains-mcp.json +11 -0
- package/configs/opencode-mcp.json +12 -0
- package/configs/perplexity-mcp.json +9 -0
- package/configs/vscode-copilot-mcp.json +12 -0
- package/configs/windsurf-mcp.json +16 -0
- package/configs/zed-mcp.json +12 -0
- package/docs/ARCHITECTURE.md +877 -0
- package/docs/CLI-COMMANDS-REFERENCE.md +425 -0
- package/docs/COMPETITIVE-ANALYSIS.md +210 -0
- package/docs/COMPRESSION-README.md +390 -0
- package/docs/GRAPH-ENGINE.md +503 -0
- package/docs/MCP-MANUAL-SETUP.md +720 -0
- package/docs/MCP-TROUBLESHOOTING.md +787 -0
- package/docs/PATTERN-LEARNING.md +363 -0
- package/docs/PROFILES-GUIDE.md +453 -0
- package/docs/RESET-GUIDE.md +353 -0
- package/docs/SEARCH-ENGINE-V2.2.0.md +748 -0
- package/docs/SEARCH-INTEGRATION-GUIDE.md +502 -0
- package/docs/UI-SERVER.md +254 -0
- package/docs/UNIVERSAL-INTEGRATION.md +432 -0
- package/docs/V2.2.0-OPTIONAL-SEARCH.md +666 -0
- package/docs/WINDOWS-INSTALL-README.txt +34 -0
- package/docs/WINDOWS-POST-INSTALL.txt +45 -0
- package/docs/example_graph_usage.py +148 -0
- package/hooks/memory-list-skill.js +130 -0
- package/hooks/memory-profile-skill.js +284 -0
- package/hooks/memory-recall-skill.js +109 -0
- package/hooks/memory-remember-skill.js +127 -0
- package/hooks/memory-reset-skill.js +274 -0
- package/install-skills.sh +436 -0
- package/install.ps1 +417 -0
- package/install.sh +755 -0
- package/mcp_server.py +585 -0
- package/package.json +94 -0
- package/requirements-core.txt +24 -0
- package/requirements.txt +10 -0
- package/scripts/postinstall.js +126 -0
- package/scripts/preuninstall.js +57 -0
- package/skills/slm-build-graph/SKILL.md +423 -0
- package/skills/slm-list-recent/SKILL.md +348 -0
- package/skills/slm-recall/SKILL.md +325 -0
- package/skills/slm-remember/SKILL.md +194 -0
- package/skills/slm-status/SKILL.md +363 -0
- package/skills/slm-switch-profile/SKILL.md +442 -0
- package/src/__pycache__/cache_manager.cpython-312.pyc +0 -0
- package/src/__pycache__/embedding_engine.cpython-312.pyc +0 -0
- package/src/__pycache__/graph_engine.cpython-312.pyc +0 -0
- package/src/__pycache__/hnsw_index.cpython-312.pyc +0 -0
- package/src/__pycache__/hybrid_search.cpython-312.pyc +0 -0
- package/src/__pycache__/memory-profiles.cpython-312.pyc +0 -0
- package/src/__pycache__/memory-reset.cpython-312.pyc +0 -0
- package/src/__pycache__/memory_compression.cpython-312.pyc +0 -0
- package/src/__pycache__/memory_store_v2.cpython-312.pyc +0 -0
- package/src/__pycache__/migrate_v1_to_v2.cpython-312.pyc +0 -0
- package/src/__pycache__/pattern_learner.cpython-312.pyc +0 -0
- package/src/__pycache__/query_optimizer.cpython-312.pyc +0 -0
- package/src/__pycache__/search_engine_v2.cpython-312.pyc +0 -0
- package/src/__pycache__/setup_validator.cpython-312.pyc +0 -0
- package/src/__pycache__/tree_manager.cpython-312.pyc +0 -0
- package/src/cache_manager.py +520 -0
- package/src/embedding_engine.py +671 -0
- package/src/graph_engine.py +970 -0
- package/src/hnsw_index.py +626 -0
- package/src/hybrid_search.py +693 -0
- package/src/memory-profiles.py +518 -0
- package/src/memory-reset.py +485 -0
- package/src/memory_compression.py +999 -0
- package/src/memory_store_v2.py +1088 -0
- package/src/migrate_v1_to_v2.py +638 -0
- package/src/pattern_learner.py +898 -0
- package/src/query_optimizer.py +513 -0
- package/src/search_engine_v2.py +403 -0
- package/src/setup_validator.py +479 -0
- 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.")
|