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.
- package/.env.example +107 -0
- package/README.md +200 -0
- package/agent_card.py +512 -0
- package/bin/cli.js +181 -0
- package/bin/postinstall.js +216 -0
- package/config.py +104 -0
- package/dashboard.html +2689 -0
- package/hooks/README.md +196 -0
- package/hooks/__pycache__/auto-detect-response.cpython-312.pyc +0 -0
- package/hooks/__pycache__/auto_capture.cpython-312.pyc +0 -0
- package/hooks/__pycache__/session_end.cpython-312.pyc +0 -0
- package/hooks/__pycache__/session_start.cpython-312.pyc +0 -0
- package/hooks/auto-detect-response.py +348 -0
- package/hooks/auto_capture.py +255 -0
- package/hooks/detect-correction.py +173 -0
- package/hooks/grounding-hook.py +348 -0
- package/hooks/log-tool-use.py +234 -0
- package/hooks/log-user-request.py +208 -0
- package/hooks/pre-tool-decision.py +218 -0
- package/hooks/problem-detector.py +343 -0
- package/hooks/session_end.py +192 -0
- package/hooks/session_start.py +227 -0
- package/install.py +887 -0
- package/main.py +2859 -0
- package/manager.py +997 -0
- package/package.json +55 -0
- package/requirements.txt +8 -0
- package/run_server.py +136 -0
- package/services/__init__.py +50 -0
- package/services/__pycache__/__init__.cpython-312.pyc +0 -0
- package/services/__pycache__/agent_registry.cpython-312.pyc +0 -0
- package/services/__pycache__/auth.cpython-312.pyc +0 -0
- package/services/__pycache__/auto_inject.cpython-312.pyc +0 -0
- package/services/__pycache__/claude_md_sync.cpython-312.pyc +0 -0
- package/services/__pycache__/cleanup.cpython-312.pyc +0 -0
- package/services/__pycache__/compaction_flush.cpython-312.pyc +0 -0
- package/services/__pycache__/confidence.cpython-312.pyc +0 -0
- package/services/__pycache__/daily_log.cpython-312.pyc +0 -0
- package/services/__pycache__/database.cpython-312.pyc +0 -0
- package/services/__pycache__/embeddings.cpython-312.pyc +0 -0
- package/services/__pycache__/insights.cpython-312.pyc +0 -0
- package/services/__pycache__/llm_analyzer.cpython-312.pyc +0 -0
- package/services/__pycache__/memory_md_sync.cpython-312.pyc +0 -0
- package/services/__pycache__/retry_queue.cpython-312.pyc +0 -0
- package/services/__pycache__/timeline.cpython-312.pyc +0 -0
- package/services/__pycache__/vector_index.cpython-312.pyc +0 -0
- package/services/__pycache__/websocket.cpython-312.pyc +0 -0
- package/services/agent_registry.py +753 -0
- package/services/auth.py +331 -0
- package/services/auto_inject.py +250 -0
- package/services/claude_md_sync.py +275 -0
- package/services/cleanup.py +667 -0
- package/services/compaction_flush.py +447 -0
- package/services/confidence.py +301 -0
- package/services/daily_log.py +333 -0
- package/services/database.py +2485 -0
- package/services/embeddings.py +358 -0
- package/services/insights.py +632 -0
- package/services/llm_analyzer.py +595 -0
- package/services/memory_md_sync.py +409 -0
- package/services/retry_queue.py +453 -0
- package/services/timeline.py +579 -0
- package/services/vector_index.py +398 -0
- package/services/websocket.py +257 -0
- package/skills/__init__.py +6 -0
- package/skills/__pycache__/__init__.cpython-312.pyc +0 -0
- package/skills/__pycache__/admin.cpython-312.pyc +0 -0
- package/skills/__pycache__/checkpoint.cpython-312.pyc +0 -0
- package/skills/__pycache__/claude_md.cpython-312.pyc +0 -0
- package/skills/__pycache__/cleanup.cpython-312.pyc +0 -0
- package/skills/__pycache__/grounding.cpython-312.pyc +0 -0
- package/skills/__pycache__/insights.cpython-312.pyc +0 -0
- package/skills/__pycache__/natural_language.cpython-312.pyc +0 -0
- package/skills/__pycache__/retrieve.cpython-312.pyc +0 -0
- package/skills/__pycache__/search.cpython-312.pyc +0 -0
- package/skills/__pycache__/state.cpython-312.pyc +0 -0
- package/skills/__pycache__/store.cpython-312.pyc +0 -0
- package/skills/__pycache__/summarize.cpython-312.pyc +0 -0
- package/skills/__pycache__/timeline.cpython-312.pyc +0 -0
- package/skills/__pycache__/verification.cpython-312.pyc +0 -0
- package/skills/admin.py +469 -0
- package/skills/checkpoint.py +198 -0
- package/skills/claude_md.py +363 -0
- package/skills/cleanup.py +241 -0
- package/skills/grounding.py +801 -0
- package/skills/insights.py +231 -0
- package/skills/natural_language.py +277 -0
- package/skills/retrieve.py +67 -0
- package/skills/search.py +213 -0
- package/skills/state.py +182 -0
- package/skills/store.py +179 -0
- package/skills/summarize.py +588 -0
- package/skills/timeline.py +387 -0
- package/skills/verification.py +391 -0
- package/start_daemon.py +155 -0
- package/test_automation.py +221 -0
- package/test_complete.py +338 -0
- package/test_full.py +322 -0
- package/update_system.py +817 -0
- package/verify_db.py +134 -0
|
@@ -0,0 +1,343 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Problem Detector Hook for Claude Code
|
|
4
|
+
|
|
5
|
+
This script runs on UserPromptSubmit event and detects when the user
|
|
6
|
+
is describing a problem that might benefit from memory search.
|
|
7
|
+
|
|
8
|
+
When a problem is detected, it:
|
|
9
|
+
1. Updates session state with problem_solving_mode = true
|
|
10
|
+
2. Extracts keywords from the problem description
|
|
11
|
+
3. Outputs a reminder to stdout for injection into Claude's context
|
|
12
|
+
|
|
13
|
+
Configure in Claude Code settings:
|
|
14
|
+
{
|
|
15
|
+
"hooks": {
|
|
16
|
+
"UserPromptSubmit": ["python /path/to/problem-detector.py"]
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
import os
|
|
22
|
+
import sys
|
|
23
|
+
import json
|
|
24
|
+
import re
|
|
25
|
+
import logging
|
|
26
|
+
import requests
|
|
27
|
+
from pathlib import Path
|
|
28
|
+
from typing import Any, Optional, List
|
|
29
|
+
|
|
30
|
+
# Configure logging to stderr (important for Claude Code hooks)
|
|
31
|
+
logging.basicConfig(
|
|
32
|
+
level=logging.INFO,
|
|
33
|
+
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
|
|
34
|
+
stream=sys.stderr
|
|
35
|
+
)
|
|
36
|
+
logger = logging.getLogger(__name__)
|
|
37
|
+
|
|
38
|
+
# Configuration from environment
|
|
39
|
+
MEMORY_AGENT_URL = os.getenv("MEMORY_AGENT_URL", "http://localhost:8102")
|
|
40
|
+
API_TIMEOUT = int(os.getenv("API_TIMEOUT", "30"))
|
|
41
|
+
|
|
42
|
+
# Problem detection patterns (comprehensive)
|
|
43
|
+
PROBLEM_PATTERNS = [
|
|
44
|
+
# Direct error mentions
|
|
45
|
+
r"(?i)(?:error|exception|bug|issue|problem|fail|crash|broke|doesn't work|not working|won't work)",
|
|
46
|
+
# Help requests
|
|
47
|
+
r"(?i)(?:how (?:do|can|to)|help me|fix|solve|debug|troubleshoot|figure out)",
|
|
48
|
+
# Error messages (common programming errors)
|
|
49
|
+
r"(?i)(?:TypeError|SyntaxError|ReferenceError|NameError|AttributeError|ValueError|KeyError|IndexError|Warning|Fatal|Exception|Traceback|undefined|null pointer|segfault|ENOENT|ECONNREFUSED|404|500|403)",
|
|
50
|
+
# Frustration signals
|
|
51
|
+
r"(?i)(?:again|still|keeps|won't|can't|unable|stuck|tried everything)",
|
|
52
|
+
# Code references with issues
|
|
53
|
+
r"(?i)(?:this code|my (?:code|script|function|app)|the (?:bug|error|problem))",
|
|
54
|
+
]
|
|
55
|
+
|
|
56
|
+
# Patterns that indicate simple questions (not problems)
|
|
57
|
+
SIMPLE_QUESTION_PATTERNS = [
|
|
58
|
+
r"(?i)^what (?:is|are|does)",
|
|
59
|
+
r"(?i)^explain\s",
|
|
60
|
+
r"(?i)^show me\s",
|
|
61
|
+
r"(?i)^list\s",
|
|
62
|
+
r"(?i)^describe\s",
|
|
63
|
+
r"(?i)^tell me about\s",
|
|
64
|
+
]
|
|
65
|
+
|
|
66
|
+
# Keywords to extract from problem descriptions
|
|
67
|
+
KEYWORD_PATTERNS = [
|
|
68
|
+
# Error types
|
|
69
|
+
r"\b(TypeError|SyntaxError|ReferenceError|NameError|AttributeError|ValueError|KeyError|IndexError|Exception|Error)\b",
|
|
70
|
+
# Technology keywords
|
|
71
|
+
r"\b(python|javascript|typescript|react|node|npm|pip|docker|git|webpack|vite|laravel|php|mysql|postgres|redis|api|http|https|ssl|cors|auth|token|jwt)\b",
|
|
72
|
+
# Action keywords
|
|
73
|
+
r"\b(import|export|install|build|compile|run|start|stop|deploy|test|debug|connect|load|save|read|write|create|delete|update)\b",
|
|
74
|
+
# Common error codes
|
|
75
|
+
r"\b(404|500|403|401|ENOENT|ECONNREFUSED|ETIMEDOUT|EPERM|EACCES)\b",
|
|
76
|
+
]
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def safe_get(data: Any, *keys, default: Any = None) -> Any:
|
|
80
|
+
"""
|
|
81
|
+
Safely navigate nested data structures (dicts and lists).
|
|
82
|
+
|
|
83
|
+
Args:
|
|
84
|
+
data: The data structure to navigate
|
|
85
|
+
*keys: Keys (str for dict) or indices (int for list) to traverse
|
|
86
|
+
default: Value to return if path doesn't exist
|
|
87
|
+
|
|
88
|
+
Returns:
|
|
89
|
+
The value at the path, or default if not found
|
|
90
|
+
"""
|
|
91
|
+
for key in keys:
|
|
92
|
+
if data is None:
|
|
93
|
+
return default
|
|
94
|
+
if isinstance(data, dict):
|
|
95
|
+
data = data.get(key, default)
|
|
96
|
+
elif isinstance(data, list) and isinstance(key, int):
|
|
97
|
+
if 0 <= key < len(data):
|
|
98
|
+
data = data[key]
|
|
99
|
+
else:
|
|
100
|
+
return default
|
|
101
|
+
else:
|
|
102
|
+
return default
|
|
103
|
+
return data
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def get_project_path():
|
|
107
|
+
"""Get current working directory as project path."""
|
|
108
|
+
return os.getcwd()
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def load_session_data() -> Optional[dict]:
|
|
112
|
+
"""Load session data from JSON file."""
|
|
113
|
+
session_file = Path(get_project_path()) / ".claude_session"
|
|
114
|
+
if session_file.exists():
|
|
115
|
+
try:
|
|
116
|
+
content = session_file.read_text().strip()
|
|
117
|
+
# Try JSON format first
|
|
118
|
+
return json.loads(content)
|
|
119
|
+
except json.JSONDecodeError as e:
|
|
120
|
+
logger.debug(f"JSON decode error, trying legacy format: {e}")
|
|
121
|
+
# Fall back to legacy plain text format (just session_id)
|
|
122
|
+
try:
|
|
123
|
+
content = session_file.read_text().strip()
|
|
124
|
+
return {"session_id": content}
|
|
125
|
+
except (IOError, OSError) as read_err:
|
|
126
|
+
logger.warning(f"Failed to read session file: {read_err}")
|
|
127
|
+
return None
|
|
128
|
+
except (IOError, OSError) as e:
|
|
129
|
+
logger.warning(f"Failed to read session file: {e}")
|
|
130
|
+
return None
|
|
131
|
+
return None
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def save_session_data(data: dict):
|
|
135
|
+
"""Save session data to JSON file."""
|
|
136
|
+
session_file = Path(get_project_path()) / ".claude_session"
|
|
137
|
+
try:
|
|
138
|
+
session_file.write_text(json.dumps(data, indent=2))
|
|
139
|
+
except (IOError, OSError) as e:
|
|
140
|
+
logger.warning(f"Failed to save session data: {e}")
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def call_memory_agent(skill_id: str, params: dict) -> Optional[dict]:
|
|
144
|
+
"""Call the memory agent API."""
|
|
145
|
+
try:
|
|
146
|
+
response = requests.post(
|
|
147
|
+
f"{MEMORY_AGENT_URL}/a2a",
|
|
148
|
+
json={
|
|
149
|
+
"jsonrpc": "2.0",
|
|
150
|
+
"id": "problem-detector-hook",
|
|
151
|
+
"method": "tasks/send",
|
|
152
|
+
"params": {
|
|
153
|
+
"message": {"parts": [{"type": "text", "text": ""}]},
|
|
154
|
+
"metadata": {
|
|
155
|
+
"skill_id": skill_id,
|
|
156
|
+
"params": params
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
},
|
|
160
|
+
timeout=API_TIMEOUT
|
|
161
|
+
)
|
|
162
|
+
result = response.json()
|
|
163
|
+
|
|
164
|
+
# Safely extract the artifact text using safe_get
|
|
165
|
+
artifact_text = safe_get(result, "result", "artifacts", 0, "parts", 0, "text")
|
|
166
|
+
if artifact_text:
|
|
167
|
+
try:
|
|
168
|
+
return json.loads(artifact_text)
|
|
169
|
+
except json.JSONDecodeError as e:
|
|
170
|
+
logger.debug(f"Failed to parse artifact text as JSON for skill '{skill_id}': {e}")
|
|
171
|
+
return None
|
|
172
|
+
return None
|
|
173
|
+
|
|
174
|
+
except requests.RequestException as e:
|
|
175
|
+
# Silently fail - don't break Claude Code if memory agent is down
|
|
176
|
+
logger.debug(f"Memory agent request failed for skill '{skill_id}': {e}")
|
|
177
|
+
return None
|
|
178
|
+
except json.JSONDecodeError as e:
|
|
179
|
+
logger.debug(f"Failed to decode memory agent response for skill '{skill_id}': {e}")
|
|
180
|
+
return None
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
def is_simple_question(text: str) -> bool:
|
|
184
|
+
"""
|
|
185
|
+
Check if the text is a simple question that doesn't need problem-solving mode.
|
|
186
|
+
|
|
187
|
+
Returns:
|
|
188
|
+
True if this is a simple question, False otherwise
|
|
189
|
+
"""
|
|
190
|
+
for pattern in SIMPLE_QUESTION_PATTERNS:
|
|
191
|
+
if re.search(pattern, text):
|
|
192
|
+
return True
|
|
193
|
+
return False
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
def detect_problem(text: str) -> bool:
|
|
197
|
+
"""
|
|
198
|
+
Detect if the user's message describes a problem.
|
|
199
|
+
|
|
200
|
+
Args:
|
|
201
|
+
text: The user's message
|
|
202
|
+
|
|
203
|
+
Returns:
|
|
204
|
+
True if a problem is detected, False otherwise
|
|
205
|
+
"""
|
|
206
|
+
# Skip very short messages
|
|
207
|
+
if len(text) < 15:
|
|
208
|
+
return False
|
|
209
|
+
|
|
210
|
+
# Skip simple questions
|
|
211
|
+
if is_simple_question(text):
|
|
212
|
+
return False
|
|
213
|
+
|
|
214
|
+
# Check for problem patterns
|
|
215
|
+
match_count = 0
|
|
216
|
+
for pattern in PROBLEM_PATTERNS:
|
|
217
|
+
if re.search(pattern, text):
|
|
218
|
+
match_count += 1
|
|
219
|
+
|
|
220
|
+
# Require at least 1 match for problem detection
|
|
221
|
+
# Be conservative to avoid false positives
|
|
222
|
+
return match_count >= 1
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
def extract_keywords(text: str) -> List[str]:
|
|
226
|
+
"""
|
|
227
|
+
Extract relevant keywords from the problem description.
|
|
228
|
+
|
|
229
|
+
Args:
|
|
230
|
+
text: The user's message
|
|
231
|
+
|
|
232
|
+
Returns:
|
|
233
|
+
List of extracted keywords
|
|
234
|
+
"""
|
|
235
|
+
keywords = set()
|
|
236
|
+
|
|
237
|
+
for pattern in KEYWORD_PATTERNS:
|
|
238
|
+
matches = re.findall(pattern, text, re.IGNORECASE)
|
|
239
|
+
for match in matches:
|
|
240
|
+
keywords.add(match.lower())
|
|
241
|
+
|
|
242
|
+
# Also extract words that appear near "error", "problem", "issue", etc.
|
|
243
|
+
context_patterns = [
|
|
244
|
+
r"(?:error|problem|issue|bug)\s+(?:with|in|when|while)\s+(\w+)",
|
|
245
|
+
r"(\w+)\s+(?:error|problem|issue|bug)",
|
|
246
|
+
r"(?:can't|cannot|won't|doesn't)\s+(\w+)",
|
|
247
|
+
]
|
|
248
|
+
|
|
249
|
+
for pattern in context_patterns:
|
|
250
|
+
matches = re.findall(pattern, text, re.IGNORECASE)
|
|
251
|
+
for match in matches:
|
|
252
|
+
if len(match) > 2: # Skip very short words
|
|
253
|
+
keywords.add(match.lower())
|
|
254
|
+
|
|
255
|
+
return list(keywords)[:10] # Limit to 10 keywords
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
def format_reminder_output() -> str:
|
|
259
|
+
"""
|
|
260
|
+
Format the reminder message for stdout injection.
|
|
261
|
+
|
|
262
|
+
Returns:
|
|
263
|
+
Formatted reminder string
|
|
264
|
+
"""
|
|
265
|
+
return """[MEMORY HEARTBEAT - PROBLEM DETECTED]
|
|
266
|
+
Problem-solving mode activated.
|
|
267
|
+
BEFORE searching externally, check your memory:
|
|
268
|
+
memory_search_patterns("your problem description")
|
|
269
|
+
memory_search(query="...", type="error")
|
|
270
|
+
Past solutions are faster and verified to work.
|
|
271
|
+
[/MEMORY HEARTBEAT]"""
|
|
272
|
+
|
|
273
|
+
|
|
274
|
+
def main():
|
|
275
|
+
"""Main entry point for the hook."""
|
|
276
|
+
# Read hook input from stdin
|
|
277
|
+
try:
|
|
278
|
+
hook_input = json.load(sys.stdin)
|
|
279
|
+
except json.JSONDecodeError as e:
|
|
280
|
+
logger.debug(f"Failed to parse hook input JSON: {e}")
|
|
281
|
+
sys.exit(0)
|
|
282
|
+
except (IOError, OSError) as e:
|
|
283
|
+
logger.debug(f"Failed to read stdin: {e}")
|
|
284
|
+
sys.exit(0)
|
|
285
|
+
|
|
286
|
+
# Get user message from hook input
|
|
287
|
+
user_message = hook_input.get("user_prompt", "")
|
|
288
|
+
if not user_message:
|
|
289
|
+
# Try alternative format
|
|
290
|
+
session_messages = hook_input.get("session_messages", [])
|
|
291
|
+
if session_messages:
|
|
292
|
+
last_msg = session_messages[-1]
|
|
293
|
+
if last_msg.get("role") == "user":
|
|
294
|
+
user_message = last_msg.get("content", "")
|
|
295
|
+
|
|
296
|
+
if not user_message:
|
|
297
|
+
sys.exit(0)
|
|
298
|
+
|
|
299
|
+
# Detect if this is a problem description
|
|
300
|
+
if not detect_problem(user_message):
|
|
301
|
+
# Not a problem - exit silently
|
|
302
|
+
sys.exit(0)
|
|
303
|
+
|
|
304
|
+
# Extract keywords from the problem
|
|
305
|
+
keywords = extract_keywords(user_message)
|
|
306
|
+
|
|
307
|
+
logger.info(f"Problem detected with keywords: {keywords}")
|
|
308
|
+
|
|
309
|
+
# Load session data
|
|
310
|
+
session_data = load_session_data()
|
|
311
|
+
if not session_data:
|
|
312
|
+
session_data = {}
|
|
313
|
+
|
|
314
|
+
session_id = session_data.get("session_id")
|
|
315
|
+
|
|
316
|
+
# Update session state with heartbeat information
|
|
317
|
+
if "heartbeat" not in session_data:
|
|
318
|
+
session_data["heartbeat"] = {}
|
|
319
|
+
|
|
320
|
+
session_data["heartbeat"]["problem_solving_mode"] = True
|
|
321
|
+
session_data["heartbeat"]["problem_keywords"] = keywords
|
|
322
|
+
|
|
323
|
+
# Save updated session data
|
|
324
|
+
save_session_data(session_data)
|
|
325
|
+
|
|
326
|
+
# Also update memory agent state if session exists
|
|
327
|
+
if session_id:
|
|
328
|
+
call_memory_agent("state_update", {
|
|
329
|
+
"session_id": session_id,
|
|
330
|
+
"heartbeat": {
|
|
331
|
+
"problem_solving_mode": True,
|
|
332
|
+
"problem_keywords": keywords
|
|
333
|
+
}
|
|
334
|
+
})
|
|
335
|
+
|
|
336
|
+
# Output reminder to stdout for injection into Claude's context
|
|
337
|
+
print(format_reminder_output())
|
|
338
|
+
|
|
339
|
+
sys.exit(0)
|
|
340
|
+
|
|
341
|
+
|
|
342
|
+
if __name__ == "__main__":
|
|
343
|
+
main()
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Session end hook - auto-summarizes and stores session.
|
|
3
|
+
|
|
4
|
+
This hook runs when a Claude Code session ends and:
|
|
5
|
+
- Summarizes the session automatically
|
|
6
|
+
- Stores important decisions and learnings
|
|
7
|
+
- Updates project insights
|
|
8
|
+
- Syncs to CLAUDE.md if needed
|
|
9
|
+
- Appends session summary to daily log (Moltbot-inspired)
|
|
10
|
+
- Triggers MEMORY.md sync (Moltbot-inspired)
|
|
11
|
+
- Executes pre-compaction flush (Moltbot-inspired)
|
|
12
|
+
|
|
13
|
+
Configure in Claude Code settings:
|
|
14
|
+
{
|
|
15
|
+
"hooks": {
|
|
16
|
+
"SessionEnd": ["python /path/to/session_end.py"]
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
"""
|
|
20
|
+
import os
|
|
21
|
+
import sys
|
|
22
|
+
import json
|
|
23
|
+
import asyncio
|
|
24
|
+
from datetime import datetime
|
|
25
|
+
from pathlib import Path
|
|
26
|
+
from typing import Dict, Any, Optional, List
|
|
27
|
+
|
|
28
|
+
sys.path.insert(0, str(Path(__file__).parent.parent))
|
|
29
|
+
|
|
30
|
+
import httpx
|
|
31
|
+
|
|
32
|
+
MEMORY_AGENT_URL = os.getenv("MEMORY_AGENT_URL", "http://localhost:8102")
|
|
33
|
+
API_KEY = os.getenv("MEMORY_API_KEY", "")
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
async def call_memory_skill(skill_id: str, params: Dict[str, Any]) -> Optional[Dict[str, Any]]:
|
|
37
|
+
"""Call a memory agent skill."""
|
|
38
|
+
headers = {"Content-Type": "application/json"}
|
|
39
|
+
if API_KEY:
|
|
40
|
+
headers["X-Memory-Key"] = API_KEY
|
|
41
|
+
|
|
42
|
+
payload = {
|
|
43
|
+
"jsonrpc": "2.0",
|
|
44
|
+
"method": "skills/call",
|
|
45
|
+
"params": {
|
|
46
|
+
"skill_id": skill_id,
|
|
47
|
+
"params": params
|
|
48
|
+
},
|
|
49
|
+
"id": f"session-end-{datetime.now().isoformat()}"
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
try:
|
|
53
|
+
async with httpx.AsyncClient(timeout=15.0) as client:
|
|
54
|
+
response = await client.post(
|
|
55
|
+
f"{MEMORY_AGENT_URL}/a2a",
|
|
56
|
+
json=payload,
|
|
57
|
+
headers=headers
|
|
58
|
+
)
|
|
59
|
+
if response.status_code == 200:
|
|
60
|
+
data = response.json()
|
|
61
|
+
return data.get("result", {}).get("result", {})
|
|
62
|
+
except Exception:
|
|
63
|
+
pass
|
|
64
|
+
return None
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
async def end_session(session_id: str, project_path: str):
|
|
68
|
+
"""Handle session end - summarize and store."""
|
|
69
|
+
results = []
|
|
70
|
+
session_data = {
|
|
71
|
+
"decisions": [],
|
|
72
|
+
"accomplishments": [],
|
|
73
|
+
"errors_solved": [],
|
|
74
|
+
"notes": []
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
# 1. Auto-summarize the session
|
|
78
|
+
summary = await call_memory_skill("auto_summarize_session", {
|
|
79
|
+
"session_id": session_id,
|
|
80
|
+
"project_path": project_path
|
|
81
|
+
})
|
|
82
|
+
|
|
83
|
+
if summary and summary.get("success"):
|
|
84
|
+
results.append(f"Session summarized: {summary.get('summary', '')[:100]}...")
|
|
85
|
+
# Extract decisions and accomplishments from summary if available
|
|
86
|
+
if summary.get("key_decisions"):
|
|
87
|
+
session_data["decisions"] = summary["key_decisions"][:5]
|
|
88
|
+
|
|
89
|
+
# 2. Create diary entry
|
|
90
|
+
diary = await call_memory_skill("create_diary_entry", {
|
|
91
|
+
"session_id": session_id,
|
|
92
|
+
"project_path": project_path
|
|
93
|
+
})
|
|
94
|
+
|
|
95
|
+
if diary and diary.get("success"):
|
|
96
|
+
results.append(f"Diary entry created: ID {diary.get('memory_id')}")
|
|
97
|
+
|
|
98
|
+
# 3. Run insight aggregation
|
|
99
|
+
insights = await call_memory_skill("run_aggregation", {
|
|
100
|
+
"project_path": project_path
|
|
101
|
+
})
|
|
102
|
+
|
|
103
|
+
if insights and insights.get("success"):
|
|
104
|
+
new_insights = insights.get("new_insights", 0)
|
|
105
|
+
if new_insights > 0:
|
|
106
|
+
results.append(f"Generated {new_insights} new insights")
|
|
107
|
+
|
|
108
|
+
# 4. Check for CLAUDE.md suggestions
|
|
109
|
+
suggestions = await call_memory_skill("suggest_improvements", {
|
|
110
|
+
"project_path": project_path
|
|
111
|
+
})
|
|
112
|
+
|
|
113
|
+
if suggestions and suggestions.get("suggestions"):
|
|
114
|
+
results.append(f"CLAUDE.md suggestions: {len(suggestions['suggestions'])} available")
|
|
115
|
+
|
|
116
|
+
# 5. Auto-resolve any obvious anchor conflicts
|
|
117
|
+
resolved = await call_memory_skill("auto_resolve_conflicts", {
|
|
118
|
+
"project_path": project_path
|
|
119
|
+
})
|
|
120
|
+
|
|
121
|
+
if resolved and resolved.get("resolved_count", 0) > 0:
|
|
122
|
+
results.append(f"Auto-resolved {resolved['resolved_count']} conflicts")
|
|
123
|
+
|
|
124
|
+
# ============================================================
|
|
125
|
+
# MOLTBOT-INSPIRED FEATURES
|
|
126
|
+
# ============================================================
|
|
127
|
+
|
|
128
|
+
# 6. Append session summary to daily log
|
|
129
|
+
daily_log = await call_memory_skill("daily_log_append_session", {
|
|
130
|
+
"project_path": project_path,
|
|
131
|
+
"session_id": session_id,
|
|
132
|
+
"decisions": session_data["decisions"],
|
|
133
|
+
"accomplishments": session_data["accomplishments"],
|
|
134
|
+
"errors_solved": session_data["errors_solved"],
|
|
135
|
+
"notes": session_data["notes"]
|
|
136
|
+
})
|
|
137
|
+
|
|
138
|
+
if daily_log and daily_log.get("success"):
|
|
139
|
+
results.append(f"Daily log updated: {daily_log.get('file_path', 'unknown')}")
|
|
140
|
+
|
|
141
|
+
# 7. Sync MEMORY.md with high-importance items
|
|
142
|
+
memory_md = await call_memory_skill("sync_memory_md", {
|
|
143
|
+
"project_path": project_path,
|
|
144
|
+
"min_importance": 7,
|
|
145
|
+
"min_pattern_success": 3
|
|
146
|
+
})
|
|
147
|
+
|
|
148
|
+
if memory_md and memory_md.get("success"):
|
|
149
|
+
counts = memory_md.get("counts", {})
|
|
150
|
+
total_synced = sum(counts.values())
|
|
151
|
+
if total_synced > 0:
|
|
152
|
+
results.append(f"MEMORY.md synced: {total_synced} items")
|
|
153
|
+
|
|
154
|
+
# 8. Execute pre-compaction flush
|
|
155
|
+
flush = await call_memory_skill("pre_compaction_flush", {
|
|
156
|
+
"project_path": project_path,
|
|
157
|
+
"session_id": session_id
|
|
158
|
+
})
|
|
159
|
+
|
|
160
|
+
if flush and flush.get("success"):
|
|
161
|
+
results.append(f"Memory flush created: {flush.get('file_path', 'unknown')}")
|
|
162
|
+
|
|
163
|
+
return results
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
async def main():
|
|
167
|
+
session_id = os.getenv("SESSION_ID") or f"session-{datetime.now().strftime('%Y%m%d-%H%M%S')}"
|
|
168
|
+
project_path = os.getenv("PROJECT_PATH") or os.getcwd()
|
|
169
|
+
|
|
170
|
+
# Try to read from stdin
|
|
171
|
+
try:
|
|
172
|
+
if not sys.stdin.isatty():
|
|
173
|
+
data = sys.stdin.read()
|
|
174
|
+
if data:
|
|
175
|
+
hook_data = json.loads(data)
|
|
176
|
+
session_id = hook_data.get("session_id", session_id)
|
|
177
|
+
project_path = hook_data.get("project_path", project_path)
|
|
178
|
+
except:
|
|
179
|
+
pass
|
|
180
|
+
|
|
181
|
+
results = await end_session(session_id, project_path)
|
|
182
|
+
|
|
183
|
+
if results:
|
|
184
|
+
print("\n[Memory System] Session ended:")
|
|
185
|
+
for r in results:
|
|
186
|
+
print(f" - {r}")
|
|
187
|
+
else:
|
|
188
|
+
print("\n[Memory System] Session ended (no data captured)")
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
if __name__ == "__main__":
|
|
192
|
+
asyncio.run(main())
|