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.
Files changed (79) hide show
  1. iflow_mcp_developermode_korea_reversecore_mcp-1.0.0.dist-info/METADATA +543 -0
  2. iflow_mcp_developermode_korea_reversecore_mcp-1.0.0.dist-info/RECORD +79 -0
  3. iflow_mcp_developermode_korea_reversecore_mcp-1.0.0.dist-info/WHEEL +5 -0
  4. iflow_mcp_developermode_korea_reversecore_mcp-1.0.0.dist-info/entry_points.txt +2 -0
  5. iflow_mcp_developermode_korea_reversecore_mcp-1.0.0.dist-info/licenses/LICENSE +21 -0
  6. iflow_mcp_developermode_korea_reversecore_mcp-1.0.0.dist-info/top_level.txt +1 -0
  7. reversecore_mcp/__init__.py +9 -0
  8. reversecore_mcp/core/__init__.py +78 -0
  9. reversecore_mcp/core/audit.py +101 -0
  10. reversecore_mcp/core/binary_cache.py +138 -0
  11. reversecore_mcp/core/command_spec.py +357 -0
  12. reversecore_mcp/core/config.py +432 -0
  13. reversecore_mcp/core/container.py +288 -0
  14. reversecore_mcp/core/decorators.py +152 -0
  15. reversecore_mcp/core/error_formatting.py +93 -0
  16. reversecore_mcp/core/error_handling.py +142 -0
  17. reversecore_mcp/core/evidence.py +229 -0
  18. reversecore_mcp/core/exceptions.py +296 -0
  19. reversecore_mcp/core/execution.py +240 -0
  20. reversecore_mcp/core/ghidra.py +642 -0
  21. reversecore_mcp/core/ghidra_helper.py +481 -0
  22. reversecore_mcp/core/ghidra_manager.py +234 -0
  23. reversecore_mcp/core/json_utils.py +131 -0
  24. reversecore_mcp/core/loader.py +73 -0
  25. reversecore_mcp/core/logging_config.py +206 -0
  26. reversecore_mcp/core/memory.py +721 -0
  27. reversecore_mcp/core/metrics.py +198 -0
  28. reversecore_mcp/core/mitre_mapper.py +365 -0
  29. reversecore_mcp/core/plugin.py +45 -0
  30. reversecore_mcp/core/r2_helpers.py +404 -0
  31. reversecore_mcp/core/r2_pool.py +403 -0
  32. reversecore_mcp/core/report_generator.py +268 -0
  33. reversecore_mcp/core/resilience.py +252 -0
  34. reversecore_mcp/core/resource_manager.py +169 -0
  35. reversecore_mcp/core/result.py +132 -0
  36. reversecore_mcp/core/security.py +213 -0
  37. reversecore_mcp/core/validators.py +238 -0
  38. reversecore_mcp/dashboard/__init__.py +221 -0
  39. reversecore_mcp/prompts/__init__.py +56 -0
  40. reversecore_mcp/prompts/common.py +24 -0
  41. reversecore_mcp/prompts/game.py +280 -0
  42. reversecore_mcp/prompts/malware.py +1219 -0
  43. reversecore_mcp/prompts/report.py +150 -0
  44. reversecore_mcp/prompts/security.py +136 -0
  45. reversecore_mcp/resources.py +329 -0
  46. reversecore_mcp/server.py +727 -0
  47. reversecore_mcp/tools/__init__.py +49 -0
  48. reversecore_mcp/tools/analysis/__init__.py +74 -0
  49. reversecore_mcp/tools/analysis/capa_tools.py +215 -0
  50. reversecore_mcp/tools/analysis/die_tools.py +180 -0
  51. reversecore_mcp/tools/analysis/diff_tools.py +643 -0
  52. reversecore_mcp/tools/analysis/lief_tools.py +272 -0
  53. reversecore_mcp/tools/analysis/signature_tools.py +591 -0
  54. reversecore_mcp/tools/analysis/static_analysis.py +479 -0
  55. reversecore_mcp/tools/common/__init__.py +58 -0
  56. reversecore_mcp/tools/common/file_operations.py +352 -0
  57. reversecore_mcp/tools/common/memory_tools.py +516 -0
  58. reversecore_mcp/tools/common/patch_explainer.py +230 -0
  59. reversecore_mcp/tools/common/server_tools.py +115 -0
  60. reversecore_mcp/tools/ghidra/__init__.py +19 -0
  61. reversecore_mcp/tools/ghidra/decompilation.py +975 -0
  62. reversecore_mcp/tools/ghidra/ghidra_tools.py +1052 -0
  63. reversecore_mcp/tools/malware/__init__.py +61 -0
  64. reversecore_mcp/tools/malware/adaptive_vaccine.py +579 -0
  65. reversecore_mcp/tools/malware/dormant_detector.py +756 -0
  66. reversecore_mcp/tools/malware/ioc_tools.py +228 -0
  67. reversecore_mcp/tools/malware/vulnerability_hunter.py +519 -0
  68. reversecore_mcp/tools/malware/yara_tools.py +214 -0
  69. reversecore_mcp/tools/patch_explainer.py +19 -0
  70. reversecore_mcp/tools/radare2/__init__.py +13 -0
  71. reversecore_mcp/tools/radare2/r2_analysis.py +972 -0
  72. reversecore_mcp/tools/radare2/r2_session.py +376 -0
  73. reversecore_mcp/tools/radare2/radare2_mcp_tools.py +1183 -0
  74. reversecore_mcp/tools/report/__init__.py +4 -0
  75. reversecore_mcp/tools/report/email.py +82 -0
  76. reversecore_mcp/tools/report/report_mcp_tools.py +344 -0
  77. reversecore_mcp/tools/report/report_tools.py +1076 -0
  78. reversecore_mcp/tools/report/session.py +194 -0
  79. 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