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,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())