claude-memory-agent 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (100) hide show
  1. package/.env.example +107 -0
  2. package/README.md +200 -0
  3. package/agent_card.py +512 -0
  4. package/bin/cli.js +181 -0
  5. package/bin/postinstall.js +216 -0
  6. package/config.py +104 -0
  7. package/dashboard.html +2689 -0
  8. package/hooks/README.md +196 -0
  9. package/hooks/__pycache__/auto-detect-response.cpython-312.pyc +0 -0
  10. package/hooks/__pycache__/auto_capture.cpython-312.pyc +0 -0
  11. package/hooks/__pycache__/session_end.cpython-312.pyc +0 -0
  12. package/hooks/__pycache__/session_start.cpython-312.pyc +0 -0
  13. package/hooks/auto-detect-response.py +348 -0
  14. package/hooks/auto_capture.py +255 -0
  15. package/hooks/detect-correction.py +173 -0
  16. package/hooks/grounding-hook.py +348 -0
  17. package/hooks/log-tool-use.py +234 -0
  18. package/hooks/log-user-request.py +208 -0
  19. package/hooks/pre-tool-decision.py +218 -0
  20. package/hooks/problem-detector.py +343 -0
  21. package/hooks/session_end.py +192 -0
  22. package/hooks/session_start.py +227 -0
  23. package/install.py +887 -0
  24. package/main.py +2859 -0
  25. package/manager.py +997 -0
  26. package/package.json +55 -0
  27. package/requirements.txt +8 -0
  28. package/run_server.py +136 -0
  29. package/services/__init__.py +50 -0
  30. package/services/__pycache__/__init__.cpython-312.pyc +0 -0
  31. package/services/__pycache__/agent_registry.cpython-312.pyc +0 -0
  32. package/services/__pycache__/auth.cpython-312.pyc +0 -0
  33. package/services/__pycache__/auto_inject.cpython-312.pyc +0 -0
  34. package/services/__pycache__/claude_md_sync.cpython-312.pyc +0 -0
  35. package/services/__pycache__/cleanup.cpython-312.pyc +0 -0
  36. package/services/__pycache__/compaction_flush.cpython-312.pyc +0 -0
  37. package/services/__pycache__/confidence.cpython-312.pyc +0 -0
  38. package/services/__pycache__/daily_log.cpython-312.pyc +0 -0
  39. package/services/__pycache__/database.cpython-312.pyc +0 -0
  40. package/services/__pycache__/embeddings.cpython-312.pyc +0 -0
  41. package/services/__pycache__/insights.cpython-312.pyc +0 -0
  42. package/services/__pycache__/llm_analyzer.cpython-312.pyc +0 -0
  43. package/services/__pycache__/memory_md_sync.cpython-312.pyc +0 -0
  44. package/services/__pycache__/retry_queue.cpython-312.pyc +0 -0
  45. package/services/__pycache__/timeline.cpython-312.pyc +0 -0
  46. package/services/__pycache__/vector_index.cpython-312.pyc +0 -0
  47. package/services/__pycache__/websocket.cpython-312.pyc +0 -0
  48. package/services/agent_registry.py +753 -0
  49. package/services/auth.py +331 -0
  50. package/services/auto_inject.py +250 -0
  51. package/services/claude_md_sync.py +275 -0
  52. package/services/cleanup.py +667 -0
  53. package/services/compaction_flush.py +447 -0
  54. package/services/confidence.py +301 -0
  55. package/services/daily_log.py +333 -0
  56. package/services/database.py +2485 -0
  57. package/services/embeddings.py +358 -0
  58. package/services/insights.py +632 -0
  59. package/services/llm_analyzer.py +595 -0
  60. package/services/memory_md_sync.py +409 -0
  61. package/services/retry_queue.py +453 -0
  62. package/services/timeline.py +579 -0
  63. package/services/vector_index.py +398 -0
  64. package/services/websocket.py +257 -0
  65. package/skills/__init__.py +6 -0
  66. package/skills/__pycache__/__init__.cpython-312.pyc +0 -0
  67. package/skills/__pycache__/admin.cpython-312.pyc +0 -0
  68. package/skills/__pycache__/checkpoint.cpython-312.pyc +0 -0
  69. package/skills/__pycache__/claude_md.cpython-312.pyc +0 -0
  70. package/skills/__pycache__/cleanup.cpython-312.pyc +0 -0
  71. package/skills/__pycache__/grounding.cpython-312.pyc +0 -0
  72. package/skills/__pycache__/insights.cpython-312.pyc +0 -0
  73. package/skills/__pycache__/natural_language.cpython-312.pyc +0 -0
  74. package/skills/__pycache__/retrieve.cpython-312.pyc +0 -0
  75. package/skills/__pycache__/search.cpython-312.pyc +0 -0
  76. package/skills/__pycache__/state.cpython-312.pyc +0 -0
  77. package/skills/__pycache__/store.cpython-312.pyc +0 -0
  78. package/skills/__pycache__/summarize.cpython-312.pyc +0 -0
  79. package/skills/__pycache__/timeline.cpython-312.pyc +0 -0
  80. package/skills/__pycache__/verification.cpython-312.pyc +0 -0
  81. package/skills/admin.py +469 -0
  82. package/skills/checkpoint.py +198 -0
  83. package/skills/claude_md.py +363 -0
  84. package/skills/cleanup.py +241 -0
  85. package/skills/grounding.py +801 -0
  86. package/skills/insights.py +231 -0
  87. package/skills/natural_language.py +277 -0
  88. package/skills/retrieve.py +67 -0
  89. package/skills/search.py +213 -0
  90. package/skills/state.py +182 -0
  91. package/skills/store.py +179 -0
  92. package/skills/summarize.py +588 -0
  93. package/skills/timeline.py +387 -0
  94. package/skills/verification.py +391 -0
  95. package/start_daemon.py +155 -0
  96. package/test_automation.py +221 -0
  97. package/test_complete.py +338 -0
  98. package/test_full.py +322 -0
  99. package/update_system.py +817 -0
  100. package/verify_db.py +134 -0
@@ -0,0 +1,331 @@
1
+ """API authentication service for the Memory Agent.
2
+
3
+ Provides API key-based authentication with:
4
+ - Key generation and storage
5
+ - Request validation via X-Memory-Key header
6
+ - Rate limiting per key
7
+ - Key rotation support
8
+ """
9
+ import os
10
+ import json
11
+ import time
12
+ import secrets
13
+ import hashlib
14
+ from pathlib import Path
15
+ from typing import Dict, Any, Optional, List, Tuple
16
+ from threading import Lock
17
+ from datetime import datetime
18
+ from dotenv import load_dotenv
19
+
20
+ load_dotenv()
21
+
22
+ # Configuration
23
+ AUTH_ENABLED = os.getenv("AUTH_ENABLED", "false").lower() == "true" # Default: disabled for local use
24
+ KEY_FILE = os.getenv("AUTH_KEY_FILE", str(Path.home() / ".claude" / "memory-agent-keys.json"))
25
+ DEFAULT_RATE_LIMIT = int(os.getenv("AUTH_RATE_LIMIT", "100")) # requests per minute
26
+ RATE_LIMIT_WINDOW = int(os.getenv("AUTH_RATE_WINDOW", "60")) # seconds
27
+
28
+ # Endpoints that don't require authentication
29
+ EXEMPT_ENDPOINTS = [
30
+ "/health",
31
+ "/health/live",
32
+ "/ready",
33
+ "/.well-known/agent.json",
34
+ "/docs",
35
+ "/openapi.json",
36
+ "/api/auth/stats", # Allow checking auth status without key
37
+ "/dashboard", # Dashboard needs initial access
38
+ "/favicon.ico",
39
+ # Dashboard API endpoints
40
+ "/api/stats",
41
+ "/api/projects",
42
+ "/api/agents",
43
+ "/api/mcps",
44
+ "/api/hooks",
45
+ "/api/sessions",
46
+ "/ws", # WebSocket
47
+ "/a2a", # Agent-to-Agent protocol (dashboard uses this)
48
+ "/api/project/", # Project config endpoints
49
+ # Automation endpoints
50
+ "/api/inject",
51
+ "/api/memory/natural",
52
+ "/api/memory/", # Covers confidence, verify, outdated
53
+ "/api/claude-md",
54
+ ]
55
+
56
+
57
+ class RateLimiter:
58
+ """Simple sliding window rate limiter."""
59
+
60
+ def __init__(self):
61
+ self._requests: Dict[str, List[float]] = {}
62
+ self._lock = Lock()
63
+
64
+ def is_allowed(self, key: str, limit: int = DEFAULT_RATE_LIMIT, window: int = RATE_LIMIT_WINDOW) -> Tuple[bool, int]:
65
+ """Check if a request is allowed under rate limits.
66
+
67
+ Args:
68
+ key: The API key
69
+ limit: Maximum requests per window
70
+ window: Window size in seconds
71
+
72
+ Returns:
73
+ Tuple of (allowed, remaining_requests)
74
+ """
75
+ now = time.time()
76
+ with self._lock:
77
+ # Initialize or get request list
78
+ if key not in self._requests:
79
+ self._requests[key] = []
80
+
81
+ # Remove expired requests
82
+ cutoff = now - window
83
+ self._requests[key] = [t for t in self._requests[key] if t > cutoff]
84
+
85
+ # Check limit
86
+ current_count = len(self._requests[key])
87
+ if current_count >= limit:
88
+ return False, 0
89
+
90
+ # Record this request
91
+ self._requests[key].append(now)
92
+ return True, limit - current_count - 1
93
+
94
+ def get_stats(self, key: str) -> Dict[str, Any]:
95
+ """Get rate limit stats for a key."""
96
+ now = time.time()
97
+ with self._lock:
98
+ requests = self._requests.get(key, [])
99
+ recent = [t for t in requests if t > now - RATE_LIMIT_WINDOW]
100
+ return {
101
+ "current_count": len(recent),
102
+ "limit": DEFAULT_RATE_LIMIT,
103
+ "window_seconds": RATE_LIMIT_WINDOW,
104
+ "remaining": max(0, DEFAULT_RATE_LIMIT - len(recent))
105
+ }
106
+
107
+
108
+ class AuthService:
109
+ """API key authentication service.
110
+
111
+ Features:
112
+ - Secure key generation
113
+ - Key storage in JSON file
114
+ - Multiple keys support (for different clients)
115
+ - Rate limiting per key
116
+ - Key rotation
117
+ """
118
+
119
+ def __init__(self, key_file: str = KEY_FILE):
120
+ self.key_file = Path(key_file)
121
+ self.enabled = AUTH_ENABLED
122
+ self._keys: Dict[str, Dict[str, Any]] = {}
123
+ self._rate_limiter = RateLimiter()
124
+ self._lock = Lock()
125
+ self._load_keys()
126
+
127
+ def _load_keys(self):
128
+ """Load keys from file."""
129
+ if self.key_file.exists():
130
+ try:
131
+ with open(self.key_file, 'r') as f:
132
+ data = json.load(f)
133
+ self._keys = data.get("keys", {})
134
+ except (json.JSONDecodeError, IOError):
135
+ self._keys = {}
136
+
137
+ # Generate default key if none exist
138
+ if not self._keys and self.enabled:
139
+ self.generate_key("default", "Default API key")
140
+
141
+ def _save_keys(self):
142
+ """Save keys to file."""
143
+ self.key_file.parent.mkdir(parents=True, exist_ok=True)
144
+ with open(self.key_file, 'w') as f:
145
+ json.dump({
146
+ "keys": self._keys,
147
+ "updated_at": datetime.now().isoformat()
148
+ }, f, indent=2)
149
+ # Set restrictive permissions (owner read/write only)
150
+ try:
151
+ os.chmod(self.key_file, 0o600)
152
+ except OSError:
153
+ pass # Windows may not support this
154
+
155
+ def _hash_key(self, key: str) -> str:
156
+ """Hash an API key for storage."""
157
+ return hashlib.sha256(key.encode()).hexdigest()
158
+
159
+ def generate_key(self, name: str, description: str = "", rate_limit: int = DEFAULT_RATE_LIMIT) -> str:
160
+ """Generate a new API key.
161
+
162
+ Args:
163
+ name: Unique name for the key
164
+ description: Description of what this key is for
165
+ rate_limit: Custom rate limit for this key
166
+
167
+ Returns:
168
+ The generated API key (only returned once!)
169
+ """
170
+ with self._lock:
171
+ # Generate a secure random key
172
+ key = f"mem_{secrets.token_urlsafe(32)}"
173
+ key_hash = self._hash_key(key)
174
+
175
+ self._keys[key_hash] = {
176
+ "name": name,
177
+ "description": description,
178
+ "rate_limit": rate_limit,
179
+ "created_at": datetime.now().isoformat(),
180
+ "last_used": None,
181
+ "use_count": 0,
182
+ "revoked": False
183
+ }
184
+
185
+ self._save_keys()
186
+ return key
187
+
188
+ def validate_key(self, key: str) -> Tuple[bool, Optional[str], Optional[Dict[str, Any]]]:
189
+ """Validate an API key.
190
+
191
+ Args:
192
+ key: The API key to validate
193
+
194
+ Returns:
195
+ Tuple of (valid, error_message, key_info)
196
+ """
197
+ if not self.enabled:
198
+ return True, None, {"name": "auth_disabled"}
199
+
200
+ if not key:
201
+ return False, "Missing API key", None
202
+
203
+ key_hash = self._hash_key(key)
204
+
205
+ with self._lock:
206
+ if key_hash not in self._keys:
207
+ return False, "Invalid API key", None
208
+
209
+ key_info = self._keys[key_hash]
210
+
211
+ if key_info.get("revoked"):
212
+ return False, "API key has been revoked", None
213
+
214
+ # Check rate limit
215
+ rate_limit = key_info.get("rate_limit", DEFAULT_RATE_LIMIT)
216
+ allowed, remaining = self._rate_limiter.is_allowed(key_hash, rate_limit)
217
+
218
+ if not allowed:
219
+ return False, "Rate limit exceeded", None
220
+
221
+ # Update usage stats
222
+ key_info["last_used"] = datetime.now().isoformat()
223
+ key_info["use_count"] = key_info.get("use_count", 0) + 1
224
+ self._keys[key_hash] = key_info
225
+
226
+ return True, None, {
227
+ "name": key_info["name"],
228
+ "rate_remaining": remaining
229
+ }
230
+
231
+ def revoke_key(self, name: str) -> bool:
232
+ """Revoke a key by name.
233
+
234
+ Args:
235
+ name: Name of the key to revoke
236
+
237
+ Returns:
238
+ True if key was found and revoked
239
+ """
240
+ with self._lock:
241
+ for key_hash, info in self._keys.items():
242
+ if info["name"] == name:
243
+ info["revoked"] = True
244
+ info["revoked_at"] = datetime.now().isoformat()
245
+ self._keys[key_hash] = info
246
+ self._save_keys()
247
+ return True
248
+ return False
249
+
250
+ def rotate_key(self, name: str) -> Optional[str]:
251
+ """Rotate a key (revoke old, generate new with same name).
252
+
253
+ Args:
254
+ name: Name of the key to rotate
255
+
256
+ Returns:
257
+ New API key, or None if key not found
258
+ """
259
+ with self._lock:
260
+ # Find and revoke old key
261
+ old_info = None
262
+ for key_hash, info in self._keys.items():
263
+ if info["name"] == name and not info.get("revoked"):
264
+ info["revoked"] = True
265
+ info["revoked_at"] = datetime.now().isoformat()
266
+ old_info = info
267
+ break
268
+
269
+ if old_info is None:
270
+ return None
271
+
272
+ # Generate new key with same settings
273
+ return self.generate_key(
274
+ name=name,
275
+ description=old_info.get("description", ""),
276
+ rate_limit=old_info.get("rate_limit", DEFAULT_RATE_LIMIT)
277
+ )
278
+
279
+ def list_keys(self) -> List[Dict[str, Any]]:
280
+ """List all keys (without the actual key values)."""
281
+ with self._lock:
282
+ return [
283
+ {
284
+ "name": info["name"],
285
+ "description": info.get("description", ""),
286
+ "created_at": info["created_at"],
287
+ "last_used": info.get("last_used"),
288
+ "use_count": info.get("use_count", 0),
289
+ "rate_limit": info.get("rate_limit", DEFAULT_RATE_LIMIT),
290
+ "revoked": info.get("revoked", False)
291
+ }
292
+ for info in self._keys.values()
293
+ ]
294
+
295
+ def is_exempt(self, path: str) -> bool:
296
+ """Check if a path is exempt from authentication."""
297
+ return any(path.startswith(exempt) for exempt in EXEMPT_ENDPOINTS)
298
+
299
+ def get_stats(self) -> Dict[str, Any]:
300
+ """Get authentication statistics."""
301
+ with self._lock:
302
+ active_keys = sum(1 for k in self._keys.values() if not k.get("revoked"))
303
+ revoked_keys = sum(1 for k in self._keys.values() if k.get("revoked"))
304
+ total_uses = sum(k.get("use_count", 0) for k in self._keys.values())
305
+
306
+ return {
307
+ "enabled": self.enabled,
308
+ "active_keys": active_keys,
309
+ "revoked_keys": revoked_keys,
310
+ "total_uses": total_uses,
311
+ "rate_limit_default": DEFAULT_RATE_LIMIT,
312
+ "rate_limit_window": RATE_LIMIT_WINDOW,
313
+ "key_file": str(self.key_file)
314
+ }
315
+
316
+
317
+ # Global instance
318
+ _auth: Optional[AuthService] = None
319
+
320
+
321
+ def get_auth_service() -> AuthService:
322
+ """Get the global auth service instance."""
323
+ global _auth
324
+ if _auth is None:
325
+ _auth = AuthService()
326
+ return _auth
327
+
328
+
329
+ def validate_request(key: str) -> Tuple[bool, Optional[str], Optional[Dict[str, Any]]]:
330
+ """Convenience function to validate a request."""
331
+ return get_auth_service().validate_key(key)
@@ -0,0 +1,250 @@
1
+ """Auto-injection service for mid-task relevance.
2
+
3
+ Analyzes current context and automatically retrieves relevant memories.
4
+ Can be called periodically or triggered by specific events.
5
+ """
6
+ import asyncio
7
+ import re
8
+ import time
9
+ from typing import Dict, Any, List, Optional, Set
10
+ from dataclasses import dataclass, field
11
+
12
+
13
+ @dataclass
14
+ class InjectionContext:
15
+ """Tracks what has been injected to avoid repetition."""
16
+ injected_memory_ids: Set[int] = field(default_factory=set)
17
+ last_query: str = ""
18
+ last_injection_time: float = 0
19
+ injection_count: int = 0
20
+
21
+
22
+ class AutoInjector:
23
+ """Automatically injects relevant context during tasks.
24
+
25
+ Features:
26
+ - Analyzes current task/query for keywords
27
+ - Searches memories for relevant context
28
+ - Avoids injecting the same content twice
29
+ - Rate-limits injections to avoid noise
30
+ """
31
+
32
+ def __init__(self, db, embeddings):
33
+ self.db = db
34
+ self.embeddings = embeddings
35
+ self._context = InjectionContext()
36
+ self._min_injection_interval = 30 # seconds between injections
37
+ self._max_injections_per_session = 20
38
+
39
+ def _extract_keywords(self, text: str) -> List[str]:
40
+ """Extract meaningful keywords from text."""
41
+ # Remove common words
42
+ stop_words = {
43
+ 'the', 'a', 'an', 'is', 'are', 'was', 'were', 'be', 'been',
44
+ 'being', 'have', 'has', 'had', 'do', 'does', 'did', 'will',
45
+ 'would', 'could', 'should', 'may', 'might', 'must', 'can',
46
+ 'this', 'that', 'these', 'those', 'i', 'you', 'he', 'she',
47
+ 'it', 'we', 'they', 'what', 'which', 'who', 'whom', 'how',
48
+ 'when', 'where', 'why', 'all', 'each', 'every', 'both',
49
+ 'few', 'more', 'most', 'other', 'some', 'such', 'no', 'not',
50
+ 'only', 'same', 'so', 'than', 'too', 'very', 'just', 'and',
51
+ 'but', 'or', 'if', 'then', 'else', 'for', 'of', 'to', 'in',
52
+ 'on', 'at', 'by', 'from', 'with', 'about', 'into', 'through',
53
+ 'please', 'help', 'me', 'want', 'need', 'let', 'make', 'get'
54
+ }
55
+
56
+ # Extract words
57
+ words = re.findall(r'\b[a-zA-Z_][a-zA-Z0-9_]*\b', text.lower())
58
+
59
+ # Filter and prioritize
60
+ keywords = []
61
+ for word in words:
62
+ if word not in stop_words and len(word) > 2:
63
+ keywords.append(word)
64
+
65
+ # Deduplicate while preserving order
66
+ seen = set()
67
+ unique_keywords = []
68
+ for kw in keywords:
69
+ if kw not in seen:
70
+ seen.add(kw)
71
+ unique_keywords.append(kw)
72
+
73
+ return unique_keywords[:10] # Top 10 keywords
74
+
75
+ def _should_inject(self, query: str) -> bool:
76
+ """Determine if we should inject context now."""
77
+ now = time.time()
78
+
79
+ # Rate limit
80
+ if now - self._context.last_injection_time < self._min_injection_interval:
81
+ return False
82
+
83
+ # Max injections per session
84
+ if self._context.injection_count >= self._max_injections_per_session:
85
+ return False
86
+
87
+ # Skip if query is too similar to last
88
+ if query and self._context.last_query:
89
+ # Simple similarity check
90
+ query_words = set(query.lower().split())
91
+ last_words = set(self._context.last_query.lower().split())
92
+ overlap = len(query_words & last_words) / max(len(query_words), 1)
93
+ if overlap > 0.8:
94
+ return False
95
+
96
+ return True
97
+
98
+ async def get_relevant_context(
99
+ self,
100
+ current_query: str,
101
+ project_path: Optional[str] = None,
102
+ task_type: Optional[str] = None,
103
+ max_results: int = 3
104
+ ) -> Dict[str, Any]:
105
+ """Get relevant context for the current task.
106
+
107
+ Args:
108
+ current_query: The current user query or task description
109
+ project_path: Current project path
110
+ task_type: Type of task (debug, implement, refactor, etc.)
111
+ max_results: Maximum context items to return
112
+
113
+ Returns:
114
+ Dict with relevant memories, patterns, and suggestions
115
+ """
116
+ if not self._should_inject(current_query):
117
+ return {"injected": False, "reason": "rate_limited"}
118
+
119
+ keywords = self._extract_keywords(current_query)
120
+ if not keywords:
121
+ return {"injected": False, "reason": "no_keywords"}
122
+
123
+ search_query = " ".join(keywords)
124
+ results = {
125
+ "injected": True,
126
+ "keywords": keywords,
127
+ "memories": [],
128
+ "patterns": [],
129
+ "warnings": []
130
+ }
131
+
132
+ # 1. Search for relevant memories
133
+ try:
134
+ from skills.search import semantic_search
135
+ memories = await semantic_search(
136
+ db=self.db,
137
+ embeddings=self.embeddings,
138
+ query=search_query,
139
+ project_path=project_path,
140
+ limit=max_results * 2, # Get more, filter later
141
+ threshold=0.6 # Higher threshold for relevance
142
+ )
143
+
144
+ if memories and memories.get("results"):
145
+ for mem in memories["results"]:
146
+ mem_id = mem.get("id")
147
+ if mem_id and mem_id not in self._context.injected_memory_ids:
148
+ results["memories"].append({
149
+ "content": mem["content"][:300],
150
+ "type": mem.get("type"),
151
+ "relevance": mem.get("similarity", 0)
152
+ })
153
+ self._context.injected_memory_ids.add(mem_id)
154
+
155
+ if len(results["memories"]) >= max_results:
156
+ break
157
+ except Exception:
158
+ pass
159
+
160
+ # 2. Search for relevant patterns
161
+ try:
162
+ from skills.search import search_patterns
163
+ patterns = await search_patterns(
164
+ db=self.db,
165
+ embeddings=self.embeddings,
166
+ query=search_query,
167
+ limit=2,
168
+ threshold=0.6
169
+ )
170
+
171
+ if patterns and patterns.get("patterns"):
172
+ for pat in patterns["patterns"][:2]:
173
+ results["patterns"].append({
174
+ "name": pat.get("name"),
175
+ "solution": pat.get("solution", "")[:200]
176
+ })
177
+ except Exception:
178
+ pass
179
+
180
+ # 3. Check for relevant errors (warnings)
181
+ if task_type in ["debug", "fix", "error"]:
182
+ try:
183
+ errors = await semantic_search(
184
+ db=self.db,
185
+ embeddings=self.embeddings,
186
+ query=search_query,
187
+ project_path=project_path,
188
+ memory_type="error",
189
+ success_only=True,
190
+ limit=2,
191
+ threshold=0.65
192
+ )
193
+
194
+ if errors and errors.get("results"):
195
+ for err in errors["results"]:
196
+ results["warnings"].append({
197
+ "past_error": err["content"][:200],
198
+ "had_solution": err.get("success", False)
199
+ })
200
+ except Exception:
201
+ pass
202
+
203
+ # Update context tracking
204
+ self._context.last_query = current_query
205
+ self._context.last_injection_time = time.time()
206
+ self._context.injection_count += 1
207
+
208
+ return results
209
+
210
+ def format_injection(self, context: Dict[str, Any]) -> str:
211
+ """Format context for injection into conversation."""
212
+ if not context.get("injected"):
213
+ return ""
214
+
215
+ parts = []
216
+
217
+ if context.get("memories"):
218
+ parts.append("**Relevant from memory:**")
219
+ for mem in context["memories"]:
220
+ parts.append(f"- [{mem['type']}] {mem['content']}")
221
+
222
+ if context.get("patterns"):
223
+ parts.append("\n**Useful patterns:**")
224
+ for pat in context["patterns"]:
225
+ parts.append(f"- **{pat['name']}**: {pat['solution']}")
226
+
227
+ if context.get("warnings"):
228
+ parts.append("\n**Past related errors:**")
229
+ for warn in context["warnings"]:
230
+ parts.append(f"- {warn['past_error']}")
231
+
232
+ if parts:
233
+ return "\n".join(parts)
234
+ return ""
235
+
236
+ def reset_session(self):
237
+ """Reset injection tracking for new session."""
238
+ self._context = InjectionContext()
239
+
240
+
241
+ # Global injector instance
242
+ _injector: Optional[AutoInjector] = None
243
+
244
+
245
+ def get_auto_injector(db, embeddings) -> AutoInjector:
246
+ """Get the global auto-injector instance."""
247
+ global _injector
248
+ if _injector is None:
249
+ _injector = AutoInjector(db, embeddings)
250
+ return _injector