iflow-mcp_developermode-korea_reversecore-mcp 1.0.0__py3-none-any.whl
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.
- iflow_mcp_developermode_korea_reversecore_mcp-1.0.0.dist-info/METADATA +543 -0
- iflow_mcp_developermode_korea_reversecore_mcp-1.0.0.dist-info/RECORD +79 -0
- iflow_mcp_developermode_korea_reversecore_mcp-1.0.0.dist-info/WHEEL +5 -0
- iflow_mcp_developermode_korea_reversecore_mcp-1.0.0.dist-info/entry_points.txt +2 -0
- iflow_mcp_developermode_korea_reversecore_mcp-1.0.0.dist-info/licenses/LICENSE +21 -0
- iflow_mcp_developermode_korea_reversecore_mcp-1.0.0.dist-info/top_level.txt +1 -0
- reversecore_mcp/__init__.py +9 -0
- reversecore_mcp/core/__init__.py +78 -0
- reversecore_mcp/core/audit.py +101 -0
- reversecore_mcp/core/binary_cache.py +138 -0
- reversecore_mcp/core/command_spec.py +357 -0
- reversecore_mcp/core/config.py +432 -0
- reversecore_mcp/core/container.py +288 -0
- reversecore_mcp/core/decorators.py +152 -0
- reversecore_mcp/core/error_formatting.py +93 -0
- reversecore_mcp/core/error_handling.py +142 -0
- reversecore_mcp/core/evidence.py +229 -0
- reversecore_mcp/core/exceptions.py +296 -0
- reversecore_mcp/core/execution.py +240 -0
- reversecore_mcp/core/ghidra.py +642 -0
- reversecore_mcp/core/ghidra_helper.py +481 -0
- reversecore_mcp/core/ghidra_manager.py +234 -0
- reversecore_mcp/core/json_utils.py +131 -0
- reversecore_mcp/core/loader.py +73 -0
- reversecore_mcp/core/logging_config.py +206 -0
- reversecore_mcp/core/memory.py +721 -0
- reversecore_mcp/core/metrics.py +198 -0
- reversecore_mcp/core/mitre_mapper.py +365 -0
- reversecore_mcp/core/plugin.py +45 -0
- reversecore_mcp/core/r2_helpers.py +404 -0
- reversecore_mcp/core/r2_pool.py +403 -0
- reversecore_mcp/core/report_generator.py +268 -0
- reversecore_mcp/core/resilience.py +252 -0
- reversecore_mcp/core/resource_manager.py +169 -0
- reversecore_mcp/core/result.py +132 -0
- reversecore_mcp/core/security.py +213 -0
- reversecore_mcp/core/validators.py +238 -0
- reversecore_mcp/dashboard/__init__.py +221 -0
- reversecore_mcp/prompts/__init__.py +56 -0
- reversecore_mcp/prompts/common.py +24 -0
- reversecore_mcp/prompts/game.py +280 -0
- reversecore_mcp/prompts/malware.py +1219 -0
- reversecore_mcp/prompts/report.py +150 -0
- reversecore_mcp/prompts/security.py +136 -0
- reversecore_mcp/resources.py +329 -0
- reversecore_mcp/server.py +727 -0
- reversecore_mcp/tools/__init__.py +49 -0
- reversecore_mcp/tools/analysis/__init__.py +74 -0
- reversecore_mcp/tools/analysis/capa_tools.py +215 -0
- reversecore_mcp/tools/analysis/die_tools.py +180 -0
- reversecore_mcp/tools/analysis/diff_tools.py +643 -0
- reversecore_mcp/tools/analysis/lief_tools.py +272 -0
- reversecore_mcp/tools/analysis/signature_tools.py +591 -0
- reversecore_mcp/tools/analysis/static_analysis.py +479 -0
- reversecore_mcp/tools/common/__init__.py +58 -0
- reversecore_mcp/tools/common/file_operations.py +352 -0
- reversecore_mcp/tools/common/memory_tools.py +516 -0
- reversecore_mcp/tools/common/patch_explainer.py +230 -0
- reversecore_mcp/tools/common/server_tools.py +115 -0
- reversecore_mcp/tools/ghidra/__init__.py +19 -0
- reversecore_mcp/tools/ghidra/decompilation.py +975 -0
- reversecore_mcp/tools/ghidra/ghidra_tools.py +1052 -0
- reversecore_mcp/tools/malware/__init__.py +61 -0
- reversecore_mcp/tools/malware/adaptive_vaccine.py +579 -0
- reversecore_mcp/tools/malware/dormant_detector.py +756 -0
- reversecore_mcp/tools/malware/ioc_tools.py +228 -0
- reversecore_mcp/tools/malware/vulnerability_hunter.py +519 -0
- reversecore_mcp/tools/malware/yara_tools.py +214 -0
- reversecore_mcp/tools/patch_explainer.py +19 -0
- reversecore_mcp/tools/radare2/__init__.py +13 -0
- reversecore_mcp/tools/radare2/r2_analysis.py +972 -0
- reversecore_mcp/tools/radare2/r2_session.py +376 -0
- reversecore_mcp/tools/radare2/radare2_mcp_tools.py +1183 -0
- reversecore_mcp/tools/report/__init__.py +4 -0
- reversecore_mcp/tools/report/email.py +82 -0
- reversecore_mcp/tools/report/report_mcp_tools.py +344 -0
- reversecore_mcp/tools/report/report_tools.py +1076 -0
- reversecore_mcp/tools/report/session.py +194 -0
- reversecore_mcp/tools/report_tools.py +11 -0
|
@@ -0,0 +1,721 @@
|
|
|
1
|
+
"""
|
|
2
|
+
AI Long-term Memory Storage System.
|
|
3
|
+
|
|
4
|
+
This module provides persistent memory storage for AI analysis sessions,
|
|
5
|
+
enabling multi-session memory, cross-project knowledge transfer, and
|
|
6
|
+
context retrieval (injection) capabilities.
|
|
7
|
+
|
|
8
|
+
Storage backend: SQLite with FTS5 for full-text search.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import json
|
|
14
|
+
import uuid
|
|
15
|
+
from contextlib import asynccontextmanager
|
|
16
|
+
from datetime import datetime
|
|
17
|
+
from pathlib import Path
|
|
18
|
+
from typing import Any
|
|
19
|
+
|
|
20
|
+
import aiosqlite
|
|
21
|
+
|
|
22
|
+
from reversecore_mcp.core.logging_config import get_logger
|
|
23
|
+
|
|
24
|
+
logger = get_logger(__name__)
|
|
25
|
+
|
|
26
|
+
# Default database path
|
|
27
|
+
DEFAULT_MEMORY_DB_PATH = Path.home() / ".reversecore_mcp" / "memory.db"
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class MemoryStore:
|
|
31
|
+
"""
|
|
32
|
+
AI Long-term Memory Storage.
|
|
33
|
+
|
|
34
|
+
Provides persistent storage for analysis sessions and memories,
|
|
35
|
+
enabling context preservation across multiple sessions.
|
|
36
|
+
|
|
37
|
+
Features:
|
|
38
|
+
- Multi-session memory: Resume analysis with full context
|
|
39
|
+
- Cross-project knowledge transfer: Find similar patterns from past analyses
|
|
40
|
+
- Long-term storage: Persist function addresses, vulnerability patterns, user instructions
|
|
41
|
+
- Context retrieval: Inject relevant past information into current analysis
|
|
42
|
+
"""
|
|
43
|
+
|
|
44
|
+
def __init__(self, db_path: Path | None = None):
|
|
45
|
+
"""
|
|
46
|
+
Initialize the memory store.
|
|
47
|
+
|
|
48
|
+
Args:
|
|
49
|
+
db_path: Path to SQLite database file. Defaults to ~/.reversecore_mcp/memory.db
|
|
50
|
+
"""
|
|
51
|
+
self.db_path = db_path or DEFAULT_MEMORY_DB_PATH
|
|
52
|
+
self._db: aiosqlite.Connection | None = None
|
|
53
|
+
self._initialized = False
|
|
54
|
+
|
|
55
|
+
async def initialize(self) -> None:
|
|
56
|
+
"""
|
|
57
|
+
Initialize the database connection and create schema if needed.
|
|
58
|
+
|
|
59
|
+
Creates parent directories and database file if they don't exist.
|
|
60
|
+
"""
|
|
61
|
+
if self._initialized:
|
|
62
|
+
return
|
|
63
|
+
|
|
64
|
+
# Ensure parent directory exists
|
|
65
|
+
self.db_path.parent.mkdir(parents=True, exist_ok=True)
|
|
66
|
+
|
|
67
|
+
self._db = await aiosqlite.connect(self.db_path)
|
|
68
|
+
self._db.row_factory = aiosqlite.Row
|
|
69
|
+
|
|
70
|
+
await self._create_schema()
|
|
71
|
+
|
|
72
|
+
# Enable WAL mode for better concurrency
|
|
73
|
+
await self._db.execute("PRAGMA journal_mode=WAL;")
|
|
74
|
+
|
|
75
|
+
self._initialized = True
|
|
76
|
+
logger.info(f"Memory store initialized at {self.db_path} (WAL enabled)")
|
|
77
|
+
|
|
78
|
+
async def _create_schema(self) -> None:
|
|
79
|
+
"""Create database schema if not exists."""
|
|
80
|
+
assert self._db is not None
|
|
81
|
+
|
|
82
|
+
# Analysis sessions table
|
|
83
|
+
await self._db.execute("""
|
|
84
|
+
CREATE TABLE IF NOT EXISTS analysis_sessions (
|
|
85
|
+
id TEXT PRIMARY KEY,
|
|
86
|
+
name TEXT NOT NULL,
|
|
87
|
+
binary_name TEXT,
|
|
88
|
+
binary_hash TEXT,
|
|
89
|
+
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|
90
|
+
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|
91
|
+
analysis_duration_seconds REAL DEFAULT 0,
|
|
92
|
+
status TEXT DEFAULT 'in_progress',
|
|
93
|
+
summary TEXT
|
|
94
|
+
)
|
|
95
|
+
""")
|
|
96
|
+
|
|
97
|
+
# Memories table
|
|
98
|
+
await self._db.execute("""
|
|
99
|
+
CREATE TABLE IF NOT EXISTS memories (
|
|
100
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
101
|
+
session_id TEXT NOT NULL,
|
|
102
|
+
memory_type TEXT NOT NULL,
|
|
103
|
+
category TEXT,
|
|
104
|
+
content TEXT NOT NULL,
|
|
105
|
+
user_prompt TEXT,
|
|
106
|
+
importance INTEGER DEFAULT 5,
|
|
107
|
+
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|
108
|
+
FOREIGN KEY (session_id) REFERENCES analysis_sessions(id)
|
|
109
|
+
)
|
|
110
|
+
""")
|
|
111
|
+
|
|
112
|
+
# Patterns table for cross-session similarity search
|
|
113
|
+
await self._db.execute("""
|
|
114
|
+
CREATE TABLE IF NOT EXISTS patterns (
|
|
115
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
116
|
+
session_id TEXT NOT NULL,
|
|
117
|
+
pattern_type TEXT NOT NULL,
|
|
118
|
+
pattern_signature TEXT,
|
|
119
|
+
description TEXT,
|
|
120
|
+
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|
121
|
+
FOREIGN KEY (session_id) REFERENCES analysis_sessions(id)
|
|
122
|
+
)
|
|
123
|
+
""")
|
|
124
|
+
|
|
125
|
+
# Indexes for faster queries
|
|
126
|
+
await self._db.execute("""
|
|
127
|
+
CREATE INDEX IF NOT EXISTS idx_memories_session
|
|
128
|
+
ON memories(session_id)
|
|
129
|
+
""")
|
|
130
|
+
await self._db.execute("""
|
|
131
|
+
CREATE INDEX IF NOT EXISTS idx_memories_type
|
|
132
|
+
ON memories(memory_type, category)
|
|
133
|
+
""")
|
|
134
|
+
await self._db.execute("""
|
|
135
|
+
CREATE INDEX IF NOT EXISTS idx_patterns_signature
|
|
136
|
+
ON patterns(pattern_signature)
|
|
137
|
+
""")
|
|
138
|
+
await self._db.execute("""
|
|
139
|
+
CREATE INDEX IF NOT EXISTS idx_sessions_status
|
|
140
|
+
ON analysis_sessions(status)
|
|
141
|
+
""")
|
|
142
|
+
|
|
143
|
+
# FTS5 virtual table for full-text search
|
|
144
|
+
await self._db.execute("""
|
|
145
|
+
CREATE VIRTUAL TABLE IF NOT EXISTS memories_fts
|
|
146
|
+
USING fts5(content, content=memories, content_rowid=id)
|
|
147
|
+
""")
|
|
148
|
+
|
|
149
|
+
# Triggers to keep FTS index in sync
|
|
150
|
+
await self._db.execute("""
|
|
151
|
+
CREATE TRIGGER IF NOT EXISTS memories_ai AFTER INSERT ON memories BEGIN
|
|
152
|
+
INSERT INTO memories_fts(rowid, content) VALUES (new.id, new.content);
|
|
153
|
+
END
|
|
154
|
+
""")
|
|
155
|
+
await self._db.execute("""
|
|
156
|
+
CREATE TRIGGER IF NOT EXISTS memories_ad AFTER DELETE ON memories BEGIN
|
|
157
|
+
INSERT INTO memories_fts(memories_fts, rowid, content)
|
|
158
|
+
VALUES('delete', old.id, old.content);
|
|
159
|
+
END
|
|
160
|
+
""")
|
|
161
|
+
await self._db.execute("""
|
|
162
|
+
CREATE TRIGGER IF NOT EXISTS memories_au AFTER UPDATE ON memories BEGIN
|
|
163
|
+
INSERT INTO memories_fts(memories_fts, rowid, content)
|
|
164
|
+
VALUES('delete', old.id, old.content);
|
|
165
|
+
INSERT INTO memories_fts(rowid, content) VALUES (new.id, new.content);
|
|
166
|
+
END
|
|
167
|
+
""")
|
|
168
|
+
|
|
169
|
+
await self._db.commit()
|
|
170
|
+
|
|
171
|
+
async def close(self) -> None:
|
|
172
|
+
"""Close database connection."""
|
|
173
|
+
if self._db:
|
|
174
|
+
await self._db.close()
|
|
175
|
+
self._db = None
|
|
176
|
+
self._initialized = False
|
|
177
|
+
logger.info("Memory store closed")
|
|
178
|
+
|
|
179
|
+
@asynccontextmanager
|
|
180
|
+
async def _ensure_connection(self):
|
|
181
|
+
"""Ensure database is connected before operations."""
|
|
182
|
+
if not self._initialized:
|
|
183
|
+
await self.initialize()
|
|
184
|
+
yield self._db
|
|
185
|
+
|
|
186
|
+
# =========================================================================
|
|
187
|
+
# Session Management
|
|
188
|
+
# =========================================================================
|
|
189
|
+
|
|
190
|
+
async def create_session(
|
|
191
|
+
self,
|
|
192
|
+
name: str,
|
|
193
|
+
binary_name: str | None = None,
|
|
194
|
+
binary_hash: str | None = None,
|
|
195
|
+
) -> str:
|
|
196
|
+
"""
|
|
197
|
+
Create a new analysis session.
|
|
198
|
+
|
|
199
|
+
Args:
|
|
200
|
+
name: Template name for the session (e.g., "malware_analysis_2024_001")
|
|
201
|
+
binary_name: Name of the binary being analyzed
|
|
202
|
+
binary_hash: SHA256 hash of the binary
|
|
203
|
+
|
|
204
|
+
Returns:
|
|
205
|
+
Session ID (UUID)
|
|
206
|
+
"""
|
|
207
|
+
async with self._ensure_connection() as db:
|
|
208
|
+
session_id = str(uuid.uuid4())
|
|
209
|
+
now = datetime.utcnow().isoformat()
|
|
210
|
+
|
|
211
|
+
await db.execute(
|
|
212
|
+
"""
|
|
213
|
+
INSERT INTO analysis_sessions
|
|
214
|
+
(id, name, binary_name, binary_hash, created_at, updated_at)
|
|
215
|
+
VALUES (?, ?, ?, ?, ?, ?)
|
|
216
|
+
""",
|
|
217
|
+
(session_id, name, binary_name, binary_hash, now, now),
|
|
218
|
+
)
|
|
219
|
+
await db.commit()
|
|
220
|
+
|
|
221
|
+
logger.info(f"Created session: {name} (ID: {session_id[:8]}...)")
|
|
222
|
+
return session_id
|
|
223
|
+
|
|
224
|
+
async def get_session(self, session_id: str) -> dict | None:
|
|
225
|
+
"""
|
|
226
|
+
Get session details by ID.
|
|
227
|
+
|
|
228
|
+
Args:
|
|
229
|
+
session_id: Session UUID
|
|
230
|
+
|
|
231
|
+
Returns:
|
|
232
|
+
Session details dict or None if not found
|
|
233
|
+
"""
|
|
234
|
+
async with self._ensure_connection() as db:
|
|
235
|
+
cursor = await db.execute(
|
|
236
|
+
"SELECT * FROM analysis_sessions WHERE id = ?",
|
|
237
|
+
(session_id,),
|
|
238
|
+
)
|
|
239
|
+
row = await cursor.fetchone()
|
|
240
|
+
return dict(row) if row else None
|
|
241
|
+
|
|
242
|
+
async def list_sessions(
|
|
243
|
+
self,
|
|
244
|
+
status: str | None = None,
|
|
245
|
+
limit: int = 20,
|
|
246
|
+
offset: int = 0,
|
|
247
|
+
) -> list[dict]:
|
|
248
|
+
"""
|
|
249
|
+
List analysis sessions with optional filtering.
|
|
250
|
+
|
|
251
|
+
Args:
|
|
252
|
+
status: Filter by status ('in_progress', 'completed', 'paused')
|
|
253
|
+
limit: Maximum number of results
|
|
254
|
+
offset: Pagination offset
|
|
255
|
+
|
|
256
|
+
Returns:
|
|
257
|
+
List of session dictionaries
|
|
258
|
+
"""
|
|
259
|
+
async with self._ensure_connection() as db:
|
|
260
|
+
if status:
|
|
261
|
+
cursor = await db.execute(
|
|
262
|
+
"""
|
|
263
|
+
SELECT * FROM analysis_sessions
|
|
264
|
+
WHERE status = ?
|
|
265
|
+
ORDER BY updated_at DESC
|
|
266
|
+
LIMIT ? OFFSET ?
|
|
267
|
+
""",
|
|
268
|
+
(status, limit, offset),
|
|
269
|
+
)
|
|
270
|
+
else:
|
|
271
|
+
cursor = await db.execute(
|
|
272
|
+
"""
|
|
273
|
+
SELECT * FROM analysis_sessions
|
|
274
|
+
ORDER BY updated_at DESC
|
|
275
|
+
LIMIT ? OFFSET ?
|
|
276
|
+
""",
|
|
277
|
+
(limit, offset),
|
|
278
|
+
)
|
|
279
|
+
|
|
280
|
+
rows = await cursor.fetchall()
|
|
281
|
+
return [dict(row) for row in rows]
|
|
282
|
+
|
|
283
|
+
async def update_session(
|
|
284
|
+
self,
|
|
285
|
+
session_id: str,
|
|
286
|
+
status: str | None = None,
|
|
287
|
+
summary: str | None = None,
|
|
288
|
+
add_duration: float = 0,
|
|
289
|
+
) -> bool:
|
|
290
|
+
"""
|
|
291
|
+
Update session status and metadata.
|
|
292
|
+
|
|
293
|
+
Args:
|
|
294
|
+
session_id: Session UUID
|
|
295
|
+
status: New status value
|
|
296
|
+
summary: AI-generated analysis summary
|
|
297
|
+
add_duration: Additional analysis time to accumulate (seconds)
|
|
298
|
+
|
|
299
|
+
Returns:
|
|
300
|
+
True if session was updated
|
|
301
|
+
"""
|
|
302
|
+
async with self._ensure_connection() as db:
|
|
303
|
+
updates = ["updated_at = ?"]
|
|
304
|
+
params: list[Any] = [datetime.utcnow().isoformat()]
|
|
305
|
+
|
|
306
|
+
if status:
|
|
307
|
+
updates.append("status = ?")
|
|
308
|
+
params.append(status)
|
|
309
|
+
if summary:
|
|
310
|
+
updates.append("summary = ?")
|
|
311
|
+
params.append(summary)
|
|
312
|
+
if add_duration > 0:
|
|
313
|
+
updates.append("analysis_duration_seconds = analysis_duration_seconds + ?")
|
|
314
|
+
params.append(add_duration)
|
|
315
|
+
|
|
316
|
+
params.append(session_id)
|
|
317
|
+
|
|
318
|
+
cursor = await db.execute(
|
|
319
|
+
f"UPDATE analysis_sessions SET {', '.join(updates)} WHERE id = ?", # nosec
|
|
320
|
+
params,
|
|
321
|
+
)
|
|
322
|
+
await db.commit()
|
|
323
|
+
|
|
324
|
+
return cursor.rowcount > 0
|
|
325
|
+
|
|
326
|
+
async def find_latest_session(self, binary_name: str | None = None) -> dict | None:
|
|
327
|
+
"""
|
|
328
|
+
Find the most recent session, optionally filtered by binary name.
|
|
329
|
+
|
|
330
|
+
Args:
|
|
331
|
+
binary_name: Filter by binary name (optional)
|
|
332
|
+
|
|
333
|
+
Returns:
|
|
334
|
+
Latest session dict or None
|
|
335
|
+
"""
|
|
336
|
+
async with self._ensure_connection() as db:
|
|
337
|
+
if binary_name:
|
|
338
|
+
cursor = await db.execute(
|
|
339
|
+
"""
|
|
340
|
+
SELECT * FROM analysis_sessions
|
|
341
|
+
WHERE binary_name = ?
|
|
342
|
+
ORDER BY updated_at DESC
|
|
343
|
+
LIMIT 1
|
|
344
|
+
""",
|
|
345
|
+
(binary_name,),
|
|
346
|
+
)
|
|
347
|
+
else:
|
|
348
|
+
cursor = await db.execute(
|
|
349
|
+
"""
|
|
350
|
+
SELECT * FROM analysis_sessions
|
|
351
|
+
ORDER BY updated_at DESC
|
|
352
|
+
LIMIT 1
|
|
353
|
+
"""
|
|
354
|
+
)
|
|
355
|
+
|
|
356
|
+
row = await cursor.fetchone()
|
|
357
|
+
return dict(row) if row else None
|
|
358
|
+
|
|
359
|
+
# =========================================================================
|
|
360
|
+
# Memory Operations
|
|
361
|
+
# =========================================================================
|
|
362
|
+
|
|
363
|
+
async def save_memory(
|
|
364
|
+
self,
|
|
365
|
+
session_id: str,
|
|
366
|
+
memory_type: str,
|
|
367
|
+
content: dict | str,
|
|
368
|
+
category: str | None = None,
|
|
369
|
+
user_prompt: str | None = None,
|
|
370
|
+
importance: int = 5,
|
|
371
|
+
) -> int:
|
|
372
|
+
"""
|
|
373
|
+
Save a memory entry to the store.
|
|
374
|
+
|
|
375
|
+
Args:
|
|
376
|
+
session_id: Parent session ID
|
|
377
|
+
memory_type: Type of memory ('finding', 'pattern', 'instruction', 'context')
|
|
378
|
+
content: Memory content (dict or string, stored as JSON)
|
|
379
|
+
category: Optional category ('function', 'vulnerability', 'string', 'structure')
|
|
380
|
+
user_prompt: User's prompt at the time of saving
|
|
381
|
+
importance: Importance level 1-10 (default 5)
|
|
382
|
+
|
|
383
|
+
Returns:
|
|
384
|
+
Memory ID
|
|
385
|
+
"""
|
|
386
|
+
async with self._ensure_connection() as db:
|
|
387
|
+
content_str = json.dumps(content) if isinstance(content, dict) else content
|
|
388
|
+
|
|
389
|
+
cursor = await db.execute(
|
|
390
|
+
"""
|
|
391
|
+
INSERT INTO memories
|
|
392
|
+
(session_id, memory_type, category, content, user_prompt, importance)
|
|
393
|
+
VALUES (?, ?, ?, ?, ?, ?)
|
|
394
|
+
""",
|
|
395
|
+
(session_id, memory_type, category, content_str, user_prompt, importance),
|
|
396
|
+
)
|
|
397
|
+
await db.commit()
|
|
398
|
+
|
|
399
|
+
# Also update session timestamp
|
|
400
|
+
await self.update_session(session_id)
|
|
401
|
+
|
|
402
|
+
memory_id = cursor.lastrowid
|
|
403
|
+
logger.debug(f"Saved memory {memory_id} to session {session_id[:8]}...")
|
|
404
|
+
return memory_id
|
|
405
|
+
|
|
406
|
+
async def recall_memories(
|
|
407
|
+
self,
|
|
408
|
+
query: str,
|
|
409
|
+
session_id: str | None = None,
|
|
410
|
+
memory_type: str | None = None,
|
|
411
|
+
limit: int = 10,
|
|
412
|
+
) -> list[dict]:
|
|
413
|
+
"""
|
|
414
|
+
Recall memories matching a search query using full-text search.
|
|
415
|
+
|
|
416
|
+
Args:
|
|
417
|
+
query: Search query
|
|
418
|
+
session_id: Limit to specific session (optional)
|
|
419
|
+
memory_type: Filter by memory type (optional)
|
|
420
|
+
limit: Maximum results
|
|
421
|
+
|
|
422
|
+
Returns:
|
|
423
|
+
List of matching memories with relevance ranking
|
|
424
|
+
"""
|
|
425
|
+
async with self._ensure_connection() as db:
|
|
426
|
+
# Build query with optional filters
|
|
427
|
+
base_query = """
|
|
428
|
+
SELECT m.*, s.name as session_name, s.binary_name
|
|
429
|
+
FROM memories m
|
|
430
|
+
JOIN analysis_sessions s ON m.session_id = s.id
|
|
431
|
+
JOIN memories_fts fts ON m.id = fts.rowid
|
|
432
|
+
WHERE memories_fts MATCH ?
|
|
433
|
+
"""
|
|
434
|
+
# Sanitize query for FTS5
|
|
435
|
+
# 1. Escape double quotes
|
|
436
|
+
safe_query = query.replace('"', '""')
|
|
437
|
+
# 2. Wrap in quotes to treat as phrase if it contains spaces or symbols
|
|
438
|
+
# This prevents syntax errors from FTS5 operators in user input
|
|
439
|
+
if any(c in safe_query for c in " .-_"):
|
|
440
|
+
safe_query = f'"{safe_query}"'
|
|
441
|
+
|
|
442
|
+
# Use raw query if it seems to be an explicit FTS query (this is a heuristic)
|
|
443
|
+
if " OR " in query or " AND " in query or "NEAR(" in query:
|
|
444
|
+
# Trust the user if they look like they know FTS syntax, but still risk error
|
|
445
|
+
# For safety in this fix, we prioritize stability over advanced syntax for raw inputs
|
|
446
|
+
# so we stick to the safe version unless we validate it.
|
|
447
|
+
# Reverting to safe method for now as per plan.
|
|
448
|
+
pass
|
|
449
|
+
|
|
450
|
+
params: list[Any] = [safe_query]
|
|
451
|
+
|
|
452
|
+
if session_id:
|
|
453
|
+
base_query += " AND m.session_id = ?"
|
|
454
|
+
params.append(session_id)
|
|
455
|
+
if memory_type:
|
|
456
|
+
base_query += " AND m.memory_type = ?"
|
|
457
|
+
params.append(memory_type)
|
|
458
|
+
|
|
459
|
+
base_query += " ORDER BY m.importance DESC, m.created_at DESC LIMIT ?"
|
|
460
|
+
params.append(limit)
|
|
461
|
+
|
|
462
|
+
try:
|
|
463
|
+
cursor = await db.execute(base_query, params)
|
|
464
|
+
rows = await cursor.fetchall()
|
|
465
|
+
return [dict(row) for row in rows]
|
|
466
|
+
except Exception as e:
|
|
467
|
+
# FTS match might fail on invalid queries, fall back to LIKE
|
|
468
|
+
logger.warning(f"FTS search failed, falling back to LIKE: {e}")
|
|
469
|
+
return await self._recall_memories_fallback(query, session_id, memory_type, limit)
|
|
470
|
+
|
|
471
|
+
async def _recall_memories_fallback(
|
|
472
|
+
self,
|
|
473
|
+
query: str,
|
|
474
|
+
session_id: str | None,
|
|
475
|
+
memory_type: str | None,
|
|
476
|
+
limit: int,
|
|
477
|
+
) -> list[dict]:
|
|
478
|
+
"""Fallback search using LIKE when FTS fails."""
|
|
479
|
+
async with self._ensure_connection() as db:
|
|
480
|
+
base_query = """
|
|
481
|
+
SELECT m.*, s.name as session_name, s.binary_name
|
|
482
|
+
FROM memories m
|
|
483
|
+
JOIN analysis_sessions s ON m.session_id = s.id
|
|
484
|
+
WHERE m.content LIKE ?
|
|
485
|
+
"""
|
|
486
|
+
# PERFORMANCE: Use suffix-only wildcard when possible for index usage
|
|
487
|
+
# Note: still uses leading wildcard for fallback - consider FTS5 for better perf
|
|
488
|
+
params: list[Any] = [f"%{query}%"]
|
|
489
|
+
|
|
490
|
+
if session_id:
|
|
491
|
+
base_query += " AND m.session_id = ?"
|
|
492
|
+
params.append(session_id)
|
|
493
|
+
if memory_type:
|
|
494
|
+
base_query += " AND m.memory_type = ?"
|
|
495
|
+
params.append(memory_type)
|
|
496
|
+
|
|
497
|
+
base_query += " ORDER BY m.importance DESC, m.created_at DESC LIMIT ?"
|
|
498
|
+
params.append(limit)
|
|
499
|
+
|
|
500
|
+
cursor = await db.execute(base_query, params)
|
|
501
|
+
rows = await cursor.fetchall()
|
|
502
|
+
return [dict(row) for row in rows]
|
|
503
|
+
|
|
504
|
+
async def get_session_memories(
|
|
505
|
+
self,
|
|
506
|
+
session_id: str,
|
|
507
|
+
memory_type: str | None = None,
|
|
508
|
+
) -> list[dict]:
|
|
509
|
+
"""
|
|
510
|
+
Get all memories for a session.
|
|
511
|
+
|
|
512
|
+
Args:
|
|
513
|
+
session_id: Session ID
|
|
514
|
+
memory_type: Optional filter by type
|
|
515
|
+
|
|
516
|
+
Returns:
|
|
517
|
+
List of memories for the session
|
|
518
|
+
"""
|
|
519
|
+
async with self._ensure_connection() as db:
|
|
520
|
+
if memory_type:
|
|
521
|
+
cursor = await db.execute(
|
|
522
|
+
"""
|
|
523
|
+
SELECT * FROM memories
|
|
524
|
+
WHERE session_id = ? AND memory_type = ?
|
|
525
|
+
ORDER BY importance DESC, created_at ASC
|
|
526
|
+
""",
|
|
527
|
+
(session_id, memory_type),
|
|
528
|
+
)
|
|
529
|
+
else:
|
|
530
|
+
cursor = await db.execute(
|
|
531
|
+
"""
|
|
532
|
+
SELECT * FROM memories
|
|
533
|
+
WHERE session_id = ?
|
|
534
|
+
ORDER BY importance DESC, created_at ASC
|
|
535
|
+
""",
|
|
536
|
+
(session_id,),
|
|
537
|
+
)
|
|
538
|
+
|
|
539
|
+
rows = await cursor.fetchall()
|
|
540
|
+
return [dict(row) for row in rows]
|
|
541
|
+
|
|
542
|
+
# =========================================================================
|
|
543
|
+
# Pattern Recognition
|
|
544
|
+
# =========================================================================
|
|
545
|
+
|
|
546
|
+
async def save_pattern(
|
|
547
|
+
self,
|
|
548
|
+
session_id: str,
|
|
549
|
+
pattern_type: str,
|
|
550
|
+
pattern_signature: str,
|
|
551
|
+
description: str | None = None,
|
|
552
|
+
) -> int:
|
|
553
|
+
"""
|
|
554
|
+
Save a pattern for cross-session similarity search.
|
|
555
|
+
|
|
556
|
+
Args:
|
|
557
|
+
session_id: Session where pattern was found
|
|
558
|
+
pattern_type: Type ('api_sequence', 'code_pattern', 'behavior')
|
|
559
|
+
pattern_signature: Normalized pattern signature for matching
|
|
560
|
+
description: Human-readable description
|
|
561
|
+
|
|
562
|
+
Returns:
|
|
563
|
+
Pattern ID
|
|
564
|
+
"""
|
|
565
|
+
async with self._ensure_connection() as db:
|
|
566
|
+
cursor = await db.execute(
|
|
567
|
+
"""
|
|
568
|
+
INSERT INTO patterns
|
|
569
|
+
(session_id, pattern_type, pattern_signature, description)
|
|
570
|
+
VALUES (?, ?, ?, ?)
|
|
571
|
+
""",
|
|
572
|
+
(session_id, pattern_type, pattern_signature, description),
|
|
573
|
+
)
|
|
574
|
+
await db.commit()
|
|
575
|
+
return cursor.lastrowid
|
|
576
|
+
|
|
577
|
+
async def find_similar_patterns(
|
|
578
|
+
self,
|
|
579
|
+
pattern_signature: str,
|
|
580
|
+
pattern_type: str | None = None,
|
|
581
|
+
exclude_session: str | None = None,
|
|
582
|
+
limit: int = 10,
|
|
583
|
+
) -> list[dict]:
|
|
584
|
+
"""
|
|
585
|
+
Find similar patterns from previous analyses.
|
|
586
|
+
|
|
587
|
+
Args:
|
|
588
|
+
pattern_signature: Pattern to search for
|
|
589
|
+
pattern_type: Limit to specific pattern type
|
|
590
|
+
exclude_session: Exclude patterns from this session
|
|
591
|
+
limit: Maximum results
|
|
592
|
+
|
|
593
|
+
Returns:
|
|
594
|
+
List of similar patterns with session info
|
|
595
|
+
"""
|
|
596
|
+
async with self._ensure_connection() as db:
|
|
597
|
+
base_query = """
|
|
598
|
+
SELECT p.*, s.name as session_name, s.binary_name
|
|
599
|
+
FROM patterns p
|
|
600
|
+
JOIN analysis_sessions s ON p.session_id = s.id
|
|
601
|
+
WHERE p.pattern_signature LIKE ?
|
|
602
|
+
"""
|
|
603
|
+
# PERFORMANCE: For exact prefix matching, use suffix-only wildcard
|
|
604
|
+
# This allows SQLite to use the idx_patterns_signature index
|
|
605
|
+
params: list[Any] = [f"{pattern_signature}%"]
|
|
606
|
+
|
|
607
|
+
if pattern_type:
|
|
608
|
+
base_query += " AND p.pattern_type = ?"
|
|
609
|
+
params.append(pattern_type)
|
|
610
|
+
if exclude_session:
|
|
611
|
+
base_query += " AND p.session_id != ?"
|
|
612
|
+
params.append(exclude_session)
|
|
613
|
+
|
|
614
|
+
base_query += " ORDER BY p.created_at DESC LIMIT ?"
|
|
615
|
+
params.append(limit)
|
|
616
|
+
|
|
617
|
+
cursor = await db.execute(base_query, params)
|
|
618
|
+
rows = await cursor.fetchall()
|
|
619
|
+
return [dict(row) for row in rows]
|
|
620
|
+
|
|
621
|
+
# =========================================================================
|
|
622
|
+
# Context Retrieval (Injection)
|
|
623
|
+
# =========================================================================
|
|
624
|
+
|
|
625
|
+
async def get_session_context(self, session_id: str) -> dict:
|
|
626
|
+
"""
|
|
627
|
+
Get full context for a session including all memories and patterns.
|
|
628
|
+
|
|
629
|
+
This is used to restore full context when resuming a session.
|
|
630
|
+
|
|
631
|
+
Args:
|
|
632
|
+
session_id: Session ID
|
|
633
|
+
|
|
634
|
+
Returns:
|
|
635
|
+
Dict containing session info, memories, and patterns
|
|
636
|
+
"""
|
|
637
|
+
session = await self.get_session(session_id)
|
|
638
|
+
if not session:
|
|
639
|
+
return {}
|
|
640
|
+
|
|
641
|
+
memories = await self.get_session_memories(session_id)
|
|
642
|
+
|
|
643
|
+
async with self._ensure_connection() as db:
|
|
644
|
+
cursor = await db.execute(
|
|
645
|
+
"SELECT * FROM patterns WHERE session_id = ?",
|
|
646
|
+
(session_id,),
|
|
647
|
+
)
|
|
648
|
+
patterns = [dict(row) for row in await cursor.fetchall()]
|
|
649
|
+
|
|
650
|
+
return {
|
|
651
|
+
"session": session,
|
|
652
|
+
"memories": memories,
|
|
653
|
+
"patterns": patterns,
|
|
654
|
+
"memory_count": len(memories),
|
|
655
|
+
"pattern_count": len(patterns),
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
async def get_relevant_context(
|
|
659
|
+
self,
|
|
660
|
+
current_analysis: str,
|
|
661
|
+
current_session_id: str | None = None,
|
|
662
|
+
limit: int = 5,
|
|
663
|
+
) -> list[dict]:
|
|
664
|
+
"""
|
|
665
|
+
Get relevant context from past analyses for current work.
|
|
666
|
+
|
|
667
|
+
This enables the "Hey, this looks similar to what we saw before" feature.
|
|
668
|
+
|
|
669
|
+
Args:
|
|
670
|
+
current_analysis: Description of current analysis focus
|
|
671
|
+
current_session_id: Current session to exclude from results
|
|
672
|
+
limit: Maximum relevant items to return
|
|
673
|
+
|
|
674
|
+
Returns:
|
|
675
|
+
List of relevant memories from past sessions
|
|
676
|
+
"""
|
|
677
|
+
memories = await self.recall_memories(
|
|
678
|
+
query=current_analysis,
|
|
679
|
+
limit=limit * 2, # Fetch more to filter
|
|
680
|
+
)
|
|
681
|
+
|
|
682
|
+
# Filter out current session if specified
|
|
683
|
+
if current_session_id:
|
|
684
|
+
memories = [m for m in memories if m.get("session_id") != current_session_id]
|
|
685
|
+
|
|
686
|
+
return memories[:limit]
|
|
687
|
+
|
|
688
|
+
|
|
689
|
+
# Module-level singleton instance
|
|
690
|
+
_memory_store: MemoryStore | None = None
|
|
691
|
+
|
|
692
|
+
|
|
693
|
+
def get_memory_store(db_path: Path | None = None) -> MemoryStore:
|
|
694
|
+
"""
|
|
695
|
+
Get the global memory store instance.
|
|
696
|
+
|
|
697
|
+
Args:
|
|
698
|
+
db_path: Optional custom database path
|
|
699
|
+
|
|
700
|
+
Returns:
|
|
701
|
+
MemoryStore singleton instance
|
|
702
|
+
"""
|
|
703
|
+
global _memory_store
|
|
704
|
+
if _memory_store is None:
|
|
705
|
+
_memory_store = MemoryStore(db_path)
|
|
706
|
+
return _memory_store
|
|
707
|
+
|
|
708
|
+
|
|
709
|
+
async def initialize_memory_store(db_path: Path | None = None) -> MemoryStore:
|
|
710
|
+
"""
|
|
711
|
+
Initialize and return the memory store.
|
|
712
|
+
|
|
713
|
+
Args:
|
|
714
|
+
db_path: Optional custom database path
|
|
715
|
+
|
|
716
|
+
Returns:
|
|
717
|
+
Initialized MemoryStore instance
|
|
718
|
+
"""
|
|
719
|
+
store = get_memory_store(db_path)
|
|
720
|
+
await store.initialize()
|
|
721
|
+
return store
|