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,208 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ User Request Logger Hook for Claude Code
4
+
5
+ This script logs user requests to the session timeline automatically.
6
+ Called via UserPromptSubmit hook - logs the request, then grounding-hook injects context.
7
+
8
+ The session file stores JSON with:
9
+ - session_id: The current session ID
10
+ - current_request_id: The event ID of the current user_request (for causal chain linking)
11
+ - request_started_at: Timestamp of when the request started
12
+ """
13
+
14
+ import os
15
+ import sys
16
+ import json
17
+ import logging
18
+ import requests
19
+ from pathlib import Path
20
+ from datetime import datetime
21
+ from typing import Any, Optional
22
+
23
+ # Configure logging to stderr (important for Claude Code hooks)
24
+ logging.basicConfig(
25
+ level=logging.INFO,
26
+ format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
27
+ stream=sys.stderr
28
+ )
29
+ logger = logging.getLogger(__name__)
30
+
31
+ # Configuration from environment
32
+ MEMORY_AGENT_URL = os.getenv("MEMORY_AGENT_URL", "http://localhost:8102")
33
+ API_TIMEOUT = int(os.getenv("API_TIMEOUT", "30"))
34
+
35
+
36
+ def safe_get(data: Any, *keys, default: Any = None) -> Any:
37
+ """
38
+ Safely navigate nested data structures (dicts and lists).
39
+
40
+ Args:
41
+ data: The data structure to navigate
42
+ *keys: Keys (str for dict) or indices (int for list) to traverse
43
+ default: Value to return if path doesn't exist
44
+
45
+ Returns:
46
+ The value at the path, or default if not found
47
+ """
48
+ for key in keys:
49
+ if data is None:
50
+ return default
51
+ if isinstance(data, dict):
52
+ data = data.get(key, default)
53
+ elif isinstance(data, list) and isinstance(key, int):
54
+ if 0 <= key < len(data):
55
+ data = data[key]
56
+ else:
57
+ return default
58
+ else:
59
+ return default
60
+ return data
61
+
62
+
63
+ def load_session_data():
64
+ """Load session data from JSON file."""
65
+ session_file = Path(os.getcwd()) / ".claude_session"
66
+ if session_file.exists():
67
+ try:
68
+ content = session_file.read_text().strip()
69
+ # Try JSON format first
70
+ return json.loads(content)
71
+ except json.JSONDecodeError as e:
72
+ logger.debug(f"JSON decode error, trying legacy format: {e}")
73
+ # Fall back to legacy plain text format (just session_id)
74
+ try:
75
+ content = session_file.read_text().strip()
76
+ return {"session_id": content}
77
+ except (IOError, OSError) as read_err:
78
+ logger.warning(f"Failed to read session file: {read_err}")
79
+ return None
80
+ except (IOError, OSError) as e:
81
+ logger.warning(f"Failed to read session file: {e}")
82
+ return None
83
+ return None
84
+
85
+
86
+ def save_session_data(data: dict):
87
+ """Save session data to JSON file."""
88
+ session_file = Path(os.getcwd()) / ".claude_session"
89
+ try:
90
+ session_file.write_text(json.dumps(data, indent=2))
91
+ except (IOError, OSError) as e:
92
+ logger.warning(f"Failed to save session data: {e}")
93
+
94
+
95
+ def get_session_id():
96
+ """Get session ID from environment or file."""
97
+ session_id = os.getenv("CLAUDE_SESSION_ID")
98
+ if session_id:
99
+ return session_id
100
+
101
+ data = load_session_data()
102
+ return data.get("session_id") if data else None
103
+
104
+
105
+ def call_memory_agent(skill_id: str, params: dict) -> Optional[dict]:
106
+ """Call the memory agent API."""
107
+ try:
108
+ response = requests.post(
109
+ f"{MEMORY_AGENT_URL}/a2a",
110
+ json={
111
+ "jsonrpc": "2.0",
112
+ "id": "log-hook",
113
+ "method": "tasks/send",
114
+ "params": {
115
+ "message": {"parts": [{"type": "text", "text": ""}]},
116
+ "metadata": {
117
+ "skill_id": skill_id,
118
+ "params": params
119
+ }
120
+ }
121
+ },
122
+ timeout=API_TIMEOUT
123
+ )
124
+ return response.json()
125
+ except requests.RequestException as e:
126
+ logger.debug(f"Memory agent request failed for skill '{skill_id}': {e}")
127
+ return None
128
+ except json.JSONDecodeError as e:
129
+ logger.debug(f"Failed to decode memory agent response for skill '{skill_id}': {e}")
130
+ return None
131
+
132
+
133
+ def main():
134
+ """Log the user's request to timeline."""
135
+ # Read hook input from stdin
136
+ try:
137
+ hook_input = json.load(sys.stdin)
138
+ except json.JSONDecodeError as e:
139
+ logger.debug(f"Failed to parse hook input JSON: {e}")
140
+ sys.exit(0)
141
+ except (IOError, OSError) as e:
142
+ logger.debug(f"Failed to read stdin: {e}")
143
+ sys.exit(0)
144
+
145
+ # Get user message from hook input
146
+ user_message = hook_input.get("user_prompt", "")
147
+ if not user_message:
148
+ # Try alternative format
149
+ session_messages = hook_input.get("session_messages", [])
150
+ if session_messages:
151
+ last_msg = session_messages[-1]
152
+ if last_msg.get("role") == "user":
153
+ user_message = last_msg.get("content", "")
154
+
155
+ if not user_message:
156
+ sys.exit(0)
157
+
158
+ # Load session data
159
+ session_data = load_session_data()
160
+ if not session_data:
161
+ sys.exit(0)
162
+
163
+ session_id = session_data.get("session_id")
164
+ if not session_id:
165
+ sys.exit(0)
166
+
167
+ # Truncate long messages
168
+ summary = user_message[:200]
169
+ if len(user_message) > 200:
170
+ summary += "..."
171
+
172
+ # Log to timeline
173
+ result = call_memory_agent("timeline_log", {
174
+ "session_id": session_id,
175
+ "event_type": "user_request",
176
+ "summary": summary,
177
+ "details": user_message if len(user_message) > 200 else None,
178
+ "project_path": os.getcwd()
179
+ })
180
+
181
+ # Update session state with current goal
182
+ call_memory_agent("state_update", {
183
+ "session_id": session_id,
184
+ "current_goal": summary
185
+ })
186
+
187
+ # Save the event_id as current_request_id for causal chain linking
188
+ if result:
189
+ # Parse result using safe_get - the memory agent returns JSON-RPC format
190
+ # Navigate: result -> artifacts[0] -> parts[0] -> text
191
+ artifact_text = safe_get(result, "result", "artifacts", 0, "parts", 0, "text")
192
+
193
+ if artifact_text:
194
+ try:
195
+ skill_result = json.loads(artifact_text)
196
+ event_id = skill_result.get("event_id")
197
+ if event_id:
198
+ session_data["current_request_id"] = event_id
199
+ session_data["request_started_at"] = datetime.now().isoformat()
200
+ save_session_data(session_data)
201
+ except json.JSONDecodeError as e:
202
+ logger.debug(f"Failed to parse skill result JSON: {e}")
203
+
204
+ sys.exit(0)
205
+
206
+
207
+ if __name__ == "__main__":
208
+ main()
@@ -0,0 +1,218 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Pre-Tool Decision Hook for Claude Code
4
+
5
+ This script captures the DECISION (why) before each tool call.
6
+ Called via PreToolUse hook - logs the reasoning before action executes.
7
+
8
+ Creates a chain: user_request → decision → action
9
+ The decision event ID is stored for PostToolUse to link the action.
10
+
11
+ Inspired by A* algorithm: selective capture of important decisions,
12
+ not blind logging of everything (Dijkstra-style).
13
+ """
14
+
15
+ import os
16
+ import sys
17
+ import json
18
+ import logging
19
+ import requests
20
+ from pathlib import Path
21
+ from typing import Optional
22
+
23
+ # Configure logging to stderr (important for Claude Code hooks)
24
+ logging.basicConfig(
25
+ level=logging.INFO,
26
+ format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
27
+ stream=sys.stderr
28
+ )
29
+ logger = logging.getLogger(__name__)
30
+
31
+ # Configuration from environment
32
+ MEMORY_AGENT_URL = os.getenv("MEMORY_AGENT_URL", "http://localhost:8102")
33
+ API_TIMEOUT = int(os.getenv("API_TIMEOUT", "30"))
34
+
35
+ # Tools worth capturing decisions for (high-signal actions)
36
+ DECISION_WORTHY_TOOLS = {
37
+ "Edit": "editing",
38
+ "Write": "creating",
39
+ "Bash": "executing",
40
+ "Task": "delegating to agent",
41
+ }
42
+
43
+ # Low-signal tools (just reading, not changing state)
44
+ READ_ONLY_TOOLS = {"Read", "Grep", "Glob", "WebFetch", "WebSearch"}
45
+
46
+
47
+ def safe_get(data, *keys, default=None):
48
+ """Safely navigate nested data structures."""
49
+ for key in keys:
50
+ if data is None:
51
+ return default
52
+ if isinstance(data, dict):
53
+ data = data.get(key, default)
54
+ elif isinstance(data, list) and isinstance(key, int):
55
+ if 0 <= key < len(data):
56
+ data = data[key]
57
+ else:
58
+ return default
59
+ else:
60
+ return default
61
+ return data
62
+
63
+
64
+ def load_session_data():
65
+ """Load session data from JSON file."""
66
+ session_file = Path(os.getcwd()) / ".claude_session"
67
+ if session_file.exists():
68
+ try:
69
+ content = session_file.read_text().strip()
70
+ return json.loads(content)
71
+ except (json.JSONDecodeError, IOError, OSError) as e:
72
+ logger.debug(f"Failed to load session data: {e}")
73
+ return None
74
+ return None
75
+
76
+
77
+ def save_session_data(data: dict):
78
+ """Save session data to JSON file."""
79
+ session_file = Path(os.getcwd()) / ".claude_session"
80
+ try:
81
+ session_file.write_text(json.dumps(data, indent=2))
82
+ except (IOError, OSError) as e:
83
+ logger.warning(f"Failed to save session data: {e}")
84
+
85
+
86
+ def call_memory_agent(skill_id: str, params: dict) -> Optional[dict]:
87
+ """Call the memory agent API."""
88
+ try:
89
+ response = requests.post(
90
+ f"{MEMORY_AGENT_URL}/a2a",
91
+ json={
92
+ "jsonrpc": "2.0",
93
+ "id": "pre-tool-hook",
94
+ "method": "tasks/send",
95
+ "params": {
96
+ "message": {"parts": [{"type": "text", "text": ""}]},
97
+ "metadata": {
98
+ "skill_id": skill_id,
99
+ "params": params
100
+ }
101
+ }
102
+ },
103
+ timeout=API_TIMEOUT
104
+ )
105
+ return response.json()
106
+ except (requests.RequestException, json.JSONDecodeError) as e:
107
+ logger.debug(f"Memory agent request failed: {e}")
108
+ return None
109
+
110
+
111
+ def generate_decision_summary(tool_name: str, tool_input: dict) -> str:
112
+ """Generate a human-readable decision summary.
113
+
114
+ This is the WHY, not the WHAT.
115
+ """
116
+ verb = DECISION_WORTHY_TOOLS.get(tool_name, "using")
117
+
118
+ if tool_name == "Edit":
119
+ file_path = tool_input.get("file_path", "unknown")
120
+ old_string = tool_input.get("old_string", "")[:50]
121
+ return f"Decided to modify {Path(file_path).name}: changing '{old_string}...'"
122
+
123
+ elif tool_name == "Write":
124
+ file_path = tool_input.get("file_path", "unknown")
125
+ return f"Decided to create/overwrite {Path(file_path).name}"
126
+
127
+ elif tool_name == "Bash":
128
+ command = tool_input.get("command", "")[:80]
129
+ description = tool_input.get("description", "")
130
+ if description:
131
+ return f"Decided to run: {description}"
132
+ return f"Decided to execute: {command}"
133
+
134
+ elif tool_name == "Task":
135
+ agent_type = tool_input.get("subagent_type", "unknown")
136
+ task_desc = tool_input.get("description", "")[:50]
137
+ return f"Decided to delegate to {agent_type} agent: {task_desc}"
138
+
139
+ return f"Decided to use {tool_name}"
140
+
141
+
142
+ def main():
143
+ """Capture the decision before tool execution."""
144
+ # Read hook input from stdin
145
+ try:
146
+ hook_input = json.load(sys.stdin)
147
+ except (json.JSONDecodeError, IOError, OSError) as e:
148
+ logger.debug(f"Failed to parse hook input: {e}")
149
+ sys.exit(0)
150
+
151
+ tool_name = hook_input.get("tool_name") or hook_input.get("tool")
152
+ if not tool_name:
153
+ sys.exit(0)
154
+
155
+ # Skip read-only tools (low signal, high noise)
156
+ # This is the A* heuristic: skip nodes that don't lead to the goal
157
+ if tool_name in READ_ONLY_TOOLS:
158
+ sys.exit(0)
159
+
160
+ # Skip if not a decision-worthy tool
161
+ if tool_name not in DECISION_WORTHY_TOOLS:
162
+ sys.exit(0)
163
+
164
+ # Load session data
165
+ session_data = load_session_data()
166
+ if not session_data:
167
+ sys.exit(0)
168
+
169
+ session_id = session_data.get("session_id")
170
+ if not session_id:
171
+ sys.exit(0)
172
+
173
+ # Get root event (user request) for causal chain
174
+ root_event_id = session_data.get("current_request_id")
175
+
176
+ tool_input = hook_input.get("tool_input") or hook_input.get("input") or {}
177
+
178
+ # Generate decision summary (the WHY)
179
+ decision_summary = generate_decision_summary(tool_name, tool_input)
180
+
181
+ # Log the decision event
182
+ log_params = {
183
+ "session_id": session_id,
184
+ "event_type": "decision",
185
+ "summary": decision_summary[:200],
186
+ "details": json.dumps({
187
+ "tool": tool_name,
188
+ "reasoning": "Pre-action decision capture"
189
+ }),
190
+ "project_path": os.getcwd()
191
+ }
192
+
193
+ # Link to causal chain
194
+ if root_event_id:
195
+ log_params["root_event_id"] = root_event_id
196
+ log_params["parent_event_id"] = root_event_id
197
+
198
+ result = call_memory_agent("timeline_log", log_params)
199
+
200
+ # Store the decision event ID so PostToolUse can link to it
201
+ if result:
202
+ artifact_text = safe_get(result, "result", "artifacts", 0, "parts", 0, "text")
203
+ if artifact_text:
204
+ try:
205
+ skill_result = json.loads(artifact_text)
206
+ decision_event_id = skill_result.get("event_id")
207
+ if decision_event_id:
208
+ session_data["current_decision_id"] = decision_event_id
209
+ session_data["pending_tool"] = tool_name
210
+ save_session_data(session_data)
211
+ except json.JSONDecodeError:
212
+ pass
213
+
214
+ sys.exit(0)
215
+
216
+
217
+ if __name__ == "__main__":
218
+ main()