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,255 @@
1
+ #!/usr/bin/env python3
2
+ """Auto-capture hook for Claude Code.
3
+
4
+ This hook automatically captures:
5
+ - Tool executions and their outcomes
6
+ - Errors encountered during sessions
7
+ - Decisions and their rationale
8
+ - File modifications
9
+
10
+ Configure in Claude Code settings:
11
+ {
12
+ "hooks": {
13
+ "PostToolUse": ["python /path/to/auto_capture.py"],
14
+ "Notification": ["python /path/to/auto_capture.py --notification"]
15
+ }
16
+ }
17
+ """
18
+ import os
19
+ import sys
20
+ import json
21
+ import argparse
22
+ import asyncio
23
+ import logging
24
+ from datetime import datetime
25
+ from pathlib import Path
26
+ from typing import Dict, Any, Optional
27
+
28
+ # Add parent to path for imports
29
+ sys.path.insert(0, str(Path(__file__).parent.parent))
30
+
31
+ import httpx
32
+
33
+ # Configure logging to stderr (important for Claude Code hooks)
34
+ logging.basicConfig(
35
+ level=logging.INFO,
36
+ format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
37
+ stream=sys.stderr
38
+ )
39
+ logger = logging.getLogger(__name__)
40
+
41
+ MEMORY_AGENT_URL = os.getenv("MEMORY_AGENT_URL", "http://localhost:8102")
42
+ API_KEY = os.getenv("MEMORY_API_KEY", "")
43
+
44
+ # Tool categories for smart capture
45
+ ERROR_INDICATORS = ["error", "failed", "exception", "traceback", "cannot", "unable"]
46
+ DECISION_INDICATORS = ["chose", "decided", "selected", "using", "approach", "strategy"]
47
+ IMPORTANT_TOOLS = ["Write", "Edit", "Bash", "Task"]
48
+
49
+
50
+ def should_capture(tool_name: str, output: str, exit_code: Optional[int] = None) -> tuple[bool, str, int]:
51
+ """Determine if this tool execution should be captured.
52
+
53
+ Returns: (should_capture, memory_type, importance)
54
+ """
55
+ output_lower = output.lower() if output else ""
56
+
57
+ # Always capture errors
58
+ if exit_code and exit_code != 0:
59
+ return True, "error", 8
60
+
61
+ if any(indicator in output_lower for indicator in ERROR_INDICATORS):
62
+ return True, "error", 7
63
+
64
+ # Capture important tool executions
65
+ if tool_name in IMPORTANT_TOOLS:
66
+ # File writes are decisions
67
+ if tool_name in ["Write", "Edit"]:
68
+ return True, "decision", 6
69
+ # Bash commands with meaningful output
70
+ if tool_name == "Bash" and len(output) > 50:
71
+ return True, "chunk", 5
72
+ # Task tool usage
73
+ if tool_name == "Task":
74
+ return True, "decision", 6
75
+
76
+ # Skip routine operations
77
+ if tool_name in ["Read", "Glob", "Grep"] and len(output) < 500:
78
+ return False, "", 0
79
+
80
+ return False, "", 0
81
+
82
+
83
+ async def send_to_memory(
84
+ content: str,
85
+ memory_type: str,
86
+ importance: int,
87
+ metadata: Dict[str, Any],
88
+ project_path: Optional[str] = None
89
+ ):
90
+ """Send captured data to memory agent."""
91
+ headers = {"Content-Type": "application/json"}
92
+ if API_KEY:
93
+ headers["X-Memory-Key"] = API_KEY
94
+
95
+ payload = {
96
+ "jsonrpc": "2.0",
97
+ "method": "skills/call",
98
+ "params": {
99
+ "skill_id": "store_memory",
100
+ "params": {
101
+ "content": content,
102
+ "type": memory_type,
103
+ "importance": importance,
104
+ "project_path": project_path,
105
+ "tags": metadata.get("tags", []),
106
+ "metadata": metadata,
107
+ "outcome": metadata.get("outcome"),
108
+ "success": metadata.get("success", True)
109
+ }
110
+ },
111
+ "id": f"auto-capture-{datetime.now().isoformat()}"
112
+ }
113
+
114
+ try:
115
+ async with httpx.AsyncClient(timeout=5.0) as client:
116
+ response = await client.post(
117
+ f"{MEMORY_AGENT_URL}/a2a",
118
+ json=payload,
119
+ headers=headers
120
+ )
121
+ return response.status_code == 200
122
+ except httpx.RequestError as e:
123
+ # Silently fail - don't interrupt Claude's work
124
+ logger.debug(f"Memory agent request failed: {e}")
125
+ return False
126
+ except httpx.HTTPStatusError as e:
127
+ logger.debug(f"Memory agent returned error status: {e}")
128
+ return False
129
+
130
+
131
+ async def capture_tool_use(hook_data: Dict[str, Any]):
132
+ """Capture a tool execution event."""
133
+ tool_name = hook_data.get("tool_name", "Unknown")
134
+ tool_input = hook_data.get("tool_input", {})
135
+ tool_output = hook_data.get("tool_output", "")
136
+ exit_code = hook_data.get("exit_code")
137
+ session_id = hook_data.get("session_id")
138
+ project_path = hook_data.get("project_path")
139
+
140
+ should_cap, mem_type, importance = should_capture(tool_name, tool_output, exit_code)
141
+
142
+ if not should_cap:
143
+ return
144
+
145
+ # Build content summary
146
+ if mem_type == "error":
147
+ content = f"Error in {tool_name}: {tool_output[:500]}"
148
+ tags = ["error", tool_name.lower(), "auto-captured"]
149
+ success = False
150
+ else:
151
+ # Summarize the action
152
+ if tool_name == "Write":
153
+ file_path = tool_input.get("file_path", "unknown")
154
+ content = f"Created/updated file: {file_path}"
155
+ tags = ["file-change", "auto-captured"]
156
+ elif tool_name == "Edit":
157
+ file_path = tool_input.get("file_path", "unknown")
158
+ content = f"Edited file: {file_path} - {tool_input.get('old_string', '')[:100]} -> {tool_input.get('new_string', '')[:100]}"
159
+ tags = ["file-edit", "auto-captured"]
160
+ elif tool_name == "Bash":
161
+ cmd = tool_input.get("command", "")[:200]
162
+ content = f"Executed: {cmd}\nResult: {tool_output[:300]}"
163
+ tags = ["command", "auto-captured"]
164
+ elif tool_name == "Task":
165
+ content = f"Delegated task: {tool_input.get('prompt', '')[:300]}"
166
+ tags = ["delegation", "auto-captured"]
167
+ else:
168
+ content = f"{tool_name}: {str(tool_input)[:200]}\nOutput: {tool_output[:300]}"
169
+ tags = [tool_name.lower(), "auto-captured"]
170
+ success = True
171
+
172
+ metadata = {
173
+ "tool_name": tool_name,
174
+ "session_id": session_id,
175
+ "exit_code": exit_code,
176
+ "auto_captured": True,
177
+ "timestamp": datetime.now().isoformat(),
178
+ "tags": tags,
179
+ "outcome": "success" if success else "error",
180
+ "success": success
181
+ }
182
+
183
+ await send_to_memory(content, mem_type, importance, metadata, project_path)
184
+
185
+
186
+ async def capture_notification(hook_data: Dict[str, Any]):
187
+ """Capture a notification/error event."""
188
+ notification_type = hook_data.get("type", "info")
189
+ message = hook_data.get("message", "")
190
+ project_path = hook_data.get("project_path")
191
+
192
+ if notification_type == "error":
193
+ content = f"Session error: {message}"
194
+ await send_to_memory(
195
+ content,
196
+ "error",
197
+ 8,
198
+ {
199
+ "notification_type": notification_type,
200
+ "auto_captured": True,
201
+ "timestamp": datetime.now().isoformat(),
202
+ "tags": ["error", "notification", "auto-captured"],
203
+ "success": False
204
+ },
205
+ project_path
206
+ )
207
+
208
+
209
+ def read_stdin_hook_data() -> Dict[str, Any]:
210
+ """Read hook data from stdin (Claude Code passes data this way)."""
211
+ try:
212
+ if not sys.stdin.isatty():
213
+ data = sys.stdin.read()
214
+ if data:
215
+ return json.loads(data)
216
+ except json.JSONDecodeError as e:
217
+ logger.debug(f"Failed to parse hook input JSON from stdin: {e}")
218
+ except (IOError, OSError) as e:
219
+ logger.debug(f"Failed to read from stdin: {e}")
220
+ return {}
221
+
222
+
223
+ def main():
224
+ parser = argparse.ArgumentParser(description="Auto-capture hook for Claude Code")
225
+ parser.add_argument("--notification", action="store_true", help="Handle notification event")
226
+ parser.add_argument("--test", action="store_true", help="Test mode with sample data")
227
+ args = parser.parse_args()
228
+
229
+ if args.test:
230
+ # Test with sample data
231
+ test_data = {
232
+ "tool_name": "Bash",
233
+ "tool_input": {"command": "python test.py"},
234
+ "tool_output": "Error: ModuleNotFoundError: No module named 'foo'",
235
+ "exit_code": 1,
236
+ "project_path": "/test/project"
237
+ }
238
+ asyncio.run(capture_tool_use(test_data))
239
+ print("Test capture sent", file=sys.stderr)
240
+ return
241
+
242
+ hook_data = read_stdin_hook_data()
243
+
244
+ if not hook_data:
245
+ # No data from stdin, might be direct invocation
246
+ return
247
+
248
+ if args.notification:
249
+ asyncio.run(capture_notification(hook_data))
250
+ else:
251
+ asyncio.run(capture_tool_use(hook_data))
252
+
253
+
254
+ if __name__ == "__main__":
255
+ main()
@@ -0,0 +1,173 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Correction Detection Hook for Claude Code
4
+
5
+ This script detects when the user is correcting Claude.
6
+ Corrections are automatically marked as anchors (verified facts).
7
+
8
+ Patterns detected:
9
+ - "No, it's actually..."
10
+ - "That's wrong, ..."
11
+ - "Not X, Y"
12
+ - "You're mistaken..."
13
+ - "The correct X is Y"
14
+ """
15
+
16
+ import os
17
+ import sys
18
+ import json
19
+ import re
20
+ import requests
21
+ from pathlib import Path
22
+
23
+ # Configuration from environment
24
+ MEMORY_AGENT_URL = os.getenv("MEMORY_AGENT_URL", "http://localhost:8102")
25
+ API_TIMEOUT = int(os.getenv("API_TIMEOUT", "30"))
26
+
27
+ # Patterns that indicate a correction
28
+ CORRECTION_PATTERNS = [
29
+ r"^no[,.]?\s+(?:it'?s?|that'?s?|the)\s+(?:actually|really)?\s*(.+)",
30
+ r"^that'?s?\s+(?:wrong|incorrect|not right)[,.]?\s*(.+)?",
31
+ r"^not\s+(\w+)[,.]?\s+(?:it'?s?|but)\s+(.+)",
32
+ r"^you'?re?\s+(?:wrong|mistaken|incorrect)[,.]?\s*(.+)?",
33
+ r"^(?:the\s+)?correct\s+(?:\w+\s+)?(?:is|are|should be)\s+(.+)",
34
+ r"^actually[,.]?\s+(.+)",
35
+ r"^I\s+(?:meant|said|wanted)\s+(.+)",
36
+ r"^wrong[,.]?\s+(?:it'?s?|the)\s+(.+)",
37
+ ]
38
+
39
+ def load_session_data():
40
+ """Load session data from JSON file."""
41
+ session_file = Path(os.getcwd()) / ".claude_session"
42
+ if session_file.exists():
43
+ content = session_file.read_text().strip()
44
+ try:
45
+ # Try JSON format first
46
+ return json.loads(content)
47
+ except json.JSONDecodeError:
48
+ # Fall back to legacy plain text format (just session_id)
49
+ return {"session_id": content}
50
+ return None
51
+
52
+ def get_session_id():
53
+ """Get session ID from file."""
54
+ data = load_session_data()
55
+ return data.get("session_id") if data else None
56
+
57
+ def call_memory_agent(skill_id: str, params: dict) -> dict:
58
+ """Call the memory agent API."""
59
+ try:
60
+ response = requests.post(
61
+ f"{MEMORY_AGENT_URL}/a2a",
62
+ json={
63
+ "jsonrpc": "2.0",
64
+ "id": "correction-hook",
65
+ "method": "tasks/send",
66
+ "params": {
67
+ "message": {"parts": [{"type": "text", "text": ""}]},
68
+ "metadata": {
69
+ "skill_id": skill_id,
70
+ "params": params
71
+ }
72
+ }
73
+ },
74
+ timeout=API_TIMEOUT
75
+ )
76
+ return response.json()
77
+ except:
78
+ return None
79
+
80
+ def detect_correction(text: str) -> tuple[bool, str]:
81
+ """
82
+ Detect if text is a correction and extract the correction content.
83
+
84
+ Returns:
85
+ (is_correction, correction_content)
86
+ """
87
+ # Normalize text
88
+ text_lower = text.strip().lower()
89
+ text_clean = text.strip()
90
+
91
+ for pattern in CORRECTION_PATTERNS:
92
+ match = re.match(pattern, text_lower, re.IGNORECASE)
93
+ if match:
94
+ # Get the correction content
95
+ groups = match.groups()
96
+ correction = " ".join(g for g in groups if g) if groups else text_clean
97
+ return True, correction or text_clean
98
+
99
+ return False, ""
100
+
101
+ def main():
102
+ """Detect corrections and mark as anchors."""
103
+ # Read hook input from stdin
104
+ try:
105
+ hook_input = json.load(sys.stdin)
106
+ except:
107
+ sys.exit(0)
108
+
109
+ # Get user message
110
+ user_message = hook_input.get("user_prompt", "")
111
+ if not user_message:
112
+ session_messages = hook_input.get("session_messages", [])
113
+ if session_messages:
114
+ last_msg = session_messages[-1]
115
+ if last_msg.get("role") == "user":
116
+ user_message = last_msg.get("content", "")
117
+
118
+ if not user_message:
119
+ sys.exit(0)
120
+
121
+ # Check if this is a correction
122
+ is_correction, correction_content = detect_correction(user_message)
123
+
124
+ if not is_correction:
125
+ sys.exit(0)
126
+
127
+ # Load session data (includes current_request_id for causal chain)
128
+ session_data = load_session_data()
129
+ if not session_data:
130
+ sys.exit(0)
131
+
132
+ session_id = session_data.get("session_id")
133
+ if not session_id:
134
+ sys.exit(0)
135
+
136
+ # Get the current request ID for causal chain linking
137
+ root_event_id = session_data.get("current_request_id")
138
+
139
+ # Mark correction as an anchor
140
+ call_memory_agent("mark_anchor", {
141
+ "session_id": session_id,
142
+ "fact": f"USER CORRECTION: {correction_content[:200]}",
143
+ "details": f"Original message: {user_message}",
144
+ "project_path": os.getcwd()
145
+ })
146
+
147
+ # Also update the event as a clarification with causal chain linking
148
+ log_params = {
149
+ "session_id": session_id,
150
+ "event_type": "clarification",
151
+ "summary": f"User corrected: {correction_content[:150]}",
152
+ "details": user_message,
153
+ "is_anchor": True,
154
+ "confidence": 1.0,
155
+ "project_path": os.getcwd()
156
+ }
157
+
158
+ if root_event_id:
159
+ log_params["root_event_id"] = root_event_id
160
+ log_params["parent_event_id"] = root_event_id
161
+
162
+ call_memory_agent("timeline_log", log_params)
163
+
164
+ # Output a reminder for Claude
165
+ print(f"[CORRECTION DETECTED - ANCHOR CREATED]")
166
+ print(f"The user corrected you: {correction_content[:200]}")
167
+ print(f"This is now a verified fact. Do not contradict it.")
168
+ print("")
169
+
170
+ sys.exit(0)
171
+
172
+ if __name__ == "__main__":
173
+ main()