claude-memory-agent 2.0.1 → 2.2.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/README.md +206 -206
- package/agent_card.py +186 -0
- package/bin/cli.js +327 -185
- package/bin/lib/banner.js +39 -0
- package/bin/lib/environment.js +166 -0
- package/bin/lib/installer.js +291 -0
- package/bin/lib/models.js +95 -0
- package/bin/lib/steps/advanced.js +101 -0
- package/bin/lib/steps/confirm.js +87 -0
- package/bin/lib/steps/model.js +57 -0
- package/bin/lib/steps/provider.js +65 -0
- package/bin/lib/steps/scope.js +59 -0
- package/bin/lib/steps/server.js +74 -0
- package/bin/lib/ui.js +75 -0
- package/bin/onboarding.js +164 -0
- package/bin/postinstall.js +35 -270
- package/config.py +103 -4
- package/dashboard.html +4902 -2689
- package/hooks/extract_memories.py +439 -0
- package/hooks/grounding-hook.py +422 -348
- package/hooks/pre_compact_hook.py +76 -0
- package/hooks/session_end.py +293 -192
- package/hooks/session_end_hook.py +149 -0
- package/hooks/session_start.py +227 -227
- package/hooks/stop_hook.py +372 -0
- package/install.py +972 -902
- package/main.py +5240 -2859
- package/mcp_server.py +451 -0
- package/package.json +58 -47
- package/requirements.txt +12 -8
- package/services/__init__.py +50 -50
- package/services/adaptive_ranker.py +272 -0
- package/services/agent_catalog.json +153 -0
- package/services/agent_registry.py +245 -730
- package/services/claude_md_sync.py +320 -4
- package/services/consolidation.py +417 -0
- package/services/curator.py +1606 -0
- package/services/database.py +4118 -2485
- package/services/embedding_pipeline.py +262 -0
- package/services/embeddings.py +493 -85
- package/services/memory_decay.py +408 -0
- package/services/native_memory_paths.py +86 -0
- package/services/native_memory_sync.py +496 -0
- package/services/response_manager.py +183 -0
- package/services/terminal_ui.py +199 -0
- package/services/tier_manager.py +235 -0
- package/services/websocket.py +26 -6
- package/skills/__init__.py +21 -1
- package/skills/confidence_tracker.py +441 -0
- package/skills/context.py +675 -0
- package/skills/curator.py +348 -0
- package/skills/search.py +444 -213
- package/skills/session_review.py +605 -0
- package/skills/store.py +484 -179
- package/terminal_dashboard.py +474 -0
- package/update_system.py +829 -817
- 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/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/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/test_automation.py +0 -221
- package/test_complete.py +0 -338
- package/test_full.py +0 -322
- package/verify_db.py +0 -134
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
SessionEnd hook for Claude Code.
|
|
4
|
+
|
|
5
|
+
Called when a Claude Code session ends. Performs final memory extraction
|
|
6
|
+
from the conversation transcript and cleans up the cursor file for
|
|
7
|
+
this session.
|
|
8
|
+
|
|
9
|
+
This script:
|
|
10
|
+
1. Reads hook JSON from stdin (session_id, transcript_path, etc.)
|
|
11
|
+
2. Runs final extraction via extract_memories.py (with is_session_end=True)
|
|
12
|
+
3. Optionally invokes the existing session_end.py for full session wrapup
|
|
13
|
+
4. Cleans up cursor state for this session
|
|
14
|
+
5. Exits 0 on success OR failure (never blocks session teardown)
|
|
15
|
+
|
|
16
|
+
Timing budget: < 5 seconds total.
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
import sys
|
|
20
|
+
import json
|
|
21
|
+
import os
|
|
22
|
+
import time
|
|
23
|
+
|
|
24
|
+
# Ensure the hooks directory is on the path
|
|
25
|
+
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def main():
|
|
29
|
+
start = time.time()
|
|
30
|
+
|
|
31
|
+
try:
|
|
32
|
+
# Read hook data from stdin
|
|
33
|
+
hook_data = {}
|
|
34
|
+
if not sys.stdin.isatty():
|
|
35
|
+
raw = sys.stdin.read()
|
|
36
|
+
if raw.strip():
|
|
37
|
+
hook_data = json.loads(raw)
|
|
38
|
+
|
|
39
|
+
# Ensure hook_event_name is set
|
|
40
|
+
if "hook_event_name" not in hook_data:
|
|
41
|
+
hook_data["hook_event_name"] = "SessionEnd"
|
|
42
|
+
|
|
43
|
+
session_id = hook_data.get("session_id", "")
|
|
44
|
+
transcript_path = hook_data.get("transcript_path", "")
|
|
45
|
+
project_path = hook_data.get("cwd") or hook_data.get("project_path", "")
|
|
46
|
+
|
|
47
|
+
# ---------------------------------------------------------------
|
|
48
|
+
# Step 1: Extract memories from transcript (final pass)
|
|
49
|
+
# ---------------------------------------------------------------
|
|
50
|
+
if transcript_path:
|
|
51
|
+
from extract_memories import run_extraction
|
|
52
|
+
|
|
53
|
+
results = run_extraction(
|
|
54
|
+
session_id=session_id,
|
|
55
|
+
transcript_path=transcript_path,
|
|
56
|
+
project_path=project_path,
|
|
57
|
+
is_session_end=True, # This will clean up the cursor after extraction
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
elapsed_extract = round(time.time() - start, 2)
|
|
61
|
+
print(
|
|
62
|
+
f"[SessionEnd] Extraction complete: "
|
|
63
|
+
f"extracted={results['extracted']} stored={results['stored']} "
|
|
64
|
+
f"errors={results['errors']} time={elapsed_extract}s",
|
|
65
|
+
file=sys.stderr,
|
|
66
|
+
)
|
|
67
|
+
else:
|
|
68
|
+
print("[SessionEnd] No transcript_path provided, skipping extraction.", file=sys.stderr)
|
|
69
|
+
# Still clean up cursor if session_id is present
|
|
70
|
+
if session_id:
|
|
71
|
+
try:
|
|
72
|
+
from extract_memories import cleanup_cursor
|
|
73
|
+
cleanup_cursor(session_id)
|
|
74
|
+
except ImportError:
|
|
75
|
+
pass
|
|
76
|
+
|
|
77
|
+
# ---------------------------------------------------------------
|
|
78
|
+
# Step 2: Trigger the existing session_end.py wrapup logic
|
|
79
|
+
# (summarization, daily log, MEMORY.md sync, flush)
|
|
80
|
+
# Only if we have time left in our budget
|
|
81
|
+
# ---------------------------------------------------------------
|
|
82
|
+
remaining = 5.0 - (time.time() - start)
|
|
83
|
+
if remaining > 1.0 and session_id:
|
|
84
|
+
try:
|
|
85
|
+
_trigger_session_wrapup(session_id, project_path, timeout=remaining - 0.5)
|
|
86
|
+
except Exception as e:
|
|
87
|
+
print(f"[SessionEnd] Session wrapup failed (non-fatal): {e}", file=sys.stderr)
|
|
88
|
+
|
|
89
|
+
elapsed_total = round(time.time() - start, 2)
|
|
90
|
+
print(f"[SessionEnd] Complete. Total time: {elapsed_total}s", file=sys.stderr)
|
|
91
|
+
|
|
92
|
+
except Exception as e:
|
|
93
|
+
elapsed = round(time.time() - start, 2)
|
|
94
|
+
print(f"[SessionEnd] Error (non-fatal): {e} [{elapsed}s]", file=sys.stderr)
|
|
95
|
+
|
|
96
|
+
# Always exit 0 - never block session end
|
|
97
|
+
sys.exit(0)
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def _trigger_session_wrapup(session_id: str, project_path: str, timeout: float = 3.0):
|
|
101
|
+
"""
|
|
102
|
+
Trigger the existing session_end.py summarization via the memory agent API.
|
|
103
|
+
This calls key skills: daily_log_append_session, sync_memory_md, pre_compaction_flush.
|
|
104
|
+
Uses a single lightweight API call rather than the full async pipeline.
|
|
105
|
+
"""
|
|
106
|
+
import urllib.request
|
|
107
|
+
import urllib.error
|
|
108
|
+
|
|
109
|
+
memory_agent_url = os.getenv("MEMORY_AGENT_URL", "http://localhost:8102")
|
|
110
|
+
api_key = os.getenv("MEMORY_API_KEY", "")
|
|
111
|
+
|
|
112
|
+
# Call the pre_compaction_flush skill as a lightweight session wrapup
|
|
113
|
+
payload = {
|
|
114
|
+
"jsonrpc": "2.0",
|
|
115
|
+
"method": "tasks/send",
|
|
116
|
+
"params": {
|
|
117
|
+
"message": {"parts": [{"type": "text", "text": ""}]},
|
|
118
|
+
"metadata": {
|
|
119
|
+
"skill_id": "pre_compaction_flush",
|
|
120
|
+
"params": {
|
|
121
|
+
"project_path": project_path,
|
|
122
|
+
"session_id": session_id,
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
},
|
|
126
|
+
"id": f"session-end-flush-{session_id}"
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
headers = {"Content-Type": "application/json"}
|
|
130
|
+
if api_key:
|
|
131
|
+
headers["X-Memory-Key"] = api_key
|
|
132
|
+
|
|
133
|
+
try:
|
|
134
|
+
data = json.dumps(payload).encode("utf-8")
|
|
135
|
+
req = urllib.request.Request(
|
|
136
|
+
f"{memory_agent_url}/a2a",
|
|
137
|
+
data=data,
|
|
138
|
+
headers=headers,
|
|
139
|
+
method="POST"
|
|
140
|
+
)
|
|
141
|
+
with urllib.request.urlopen(req, timeout=min(timeout, 3.0)) as resp:
|
|
142
|
+
if resp.status == 200:
|
|
143
|
+
print(f"[SessionEnd] Flush triggered successfully.", file=sys.stderr)
|
|
144
|
+
except (urllib.error.URLError, urllib.error.HTTPError, OSError, TimeoutError) as e:
|
|
145
|
+
print(f"[SessionEnd] Flush API call failed: {e}", file=sys.stderr)
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
if __name__ == "__main__":
|
|
149
|
+
main()
|
package/hooks/session_start.py
CHANGED
|
@@ -1,227 +1,227 @@
|
|
|
1
|
-
#!/usr/bin/env python3
|
|
2
|
-
"""Session start hook - auto-loads relevant context.
|
|
3
|
-
|
|
4
|
-
This hook runs when a Claude Code session starts and:
|
|
5
|
-
- Loads project info and preferences
|
|
6
|
-
- Retrieves recent decisions and patterns
|
|
7
|
-
- Gets unresolved items from previous sessions
|
|
8
|
-
- Injects relevant context into the session
|
|
9
|
-
- Loads daily logs (Moltbot-inspired)
|
|
10
|
-
- Loads MEMORY.md core facts (Moltbot-inspired)
|
|
11
|
-
|
|
12
|
-
Configure in Claude Code settings:
|
|
13
|
-
{
|
|
14
|
-
"hooks": {
|
|
15
|
-
"SessionStart": ["python /path/to/session_start.py"]
|
|
16
|
-
}
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
Output is printed to stdout and injected into Claude's context.
|
|
20
|
-
"""
|
|
21
|
-
import os
|
|
22
|
-
import sys
|
|
23
|
-
import json
|
|
24
|
-
import asyncio
|
|
25
|
-
from datetime import datetime
|
|
26
|
-
from pathlib import Path
|
|
27
|
-
from typing import Dict, Any, List, Optional
|
|
28
|
-
|
|
29
|
-
sys.path.insert(0, str(Path(__file__).parent.parent))
|
|
30
|
-
|
|
31
|
-
import httpx
|
|
32
|
-
|
|
33
|
-
MEMORY_AGENT_URL = os.getenv("MEMORY_AGENT_URL", "http://localhost:8102")
|
|
34
|
-
API_KEY = os.getenv("MEMORY_API_KEY", "")
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
async def call_memory_skill(skill_id: str, params: Dict[str, Any]) -> Optional[Dict[str, Any]]:
|
|
38
|
-
"""Call a memory agent skill."""
|
|
39
|
-
headers = {"Content-Type": "application/json"}
|
|
40
|
-
if API_KEY:
|
|
41
|
-
headers["X-Memory-Key"] = API_KEY
|
|
42
|
-
|
|
43
|
-
payload = {
|
|
44
|
-
"jsonrpc": "2.0",
|
|
45
|
-
"method": "skills/call",
|
|
46
|
-
"params": {
|
|
47
|
-
"skill_id": skill_id,
|
|
48
|
-
"params": params
|
|
49
|
-
},
|
|
50
|
-
"id": f"session-start-{datetime.now().isoformat()}"
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
try:
|
|
54
|
-
async with httpx.AsyncClient(timeout=10.0) as client:
|
|
55
|
-
response = await client.post(
|
|
56
|
-
f"{MEMORY_AGENT_URL}/a2a",
|
|
57
|
-
json=payload,
|
|
58
|
-
headers=headers
|
|
59
|
-
)
|
|
60
|
-
if response.status_code == 200:
|
|
61
|
-
data = response.json()
|
|
62
|
-
return data.get("result", {}).get("result", {})
|
|
63
|
-
except Exception as e:
|
|
64
|
-
pass
|
|
65
|
-
return None
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
async def load_session_context(project_path: str) -> str:
|
|
69
|
-
"""Load all relevant context for a session start."""
|
|
70
|
-
context_parts = []
|
|
71
|
-
|
|
72
|
-
# ============================================================
|
|
73
|
-
# MOLTBOT-INSPIRED: Load MEMORY.md first (core facts)
|
|
74
|
-
# ============================================================
|
|
75
|
-
memory_md = await call_memory_skill("read_memory_md", {
|
|
76
|
-
"project_path": project_path
|
|
77
|
-
})
|
|
78
|
-
|
|
79
|
-
if memory_md and memory_md.get("exists"):
|
|
80
|
-
context_parts.append("## Core Facts (from MEMORY.md)")
|
|
81
|
-
# Include the summary or first part of content
|
|
82
|
-
content = memory_md.get("content", "")
|
|
83
|
-
# Truncate if too long
|
|
84
|
-
if len(content) > 2000:
|
|
85
|
-
content = content[:2000] + "\n...(truncated)"
|
|
86
|
-
context_parts.append(content)
|
|
87
|
-
|
|
88
|
-
# ============================================================
|
|
89
|
-
# MOLTBOT-INSPIRED: Load recent daily logs
|
|
90
|
-
# ============================================================
|
|
91
|
-
daily_logs = await call_memory_skill("daily_log_read", {
|
|
92
|
-
"project_path": project_path,
|
|
93
|
-
"days": 2,
|
|
94
|
-
"max_chars": 3000
|
|
95
|
-
})
|
|
96
|
-
|
|
97
|
-
if daily_logs and daily_logs.get("logs"):
|
|
98
|
-
context_parts.append("\n## Recent Activity (from Daily Logs)")
|
|
99
|
-
for log in daily_logs["logs"]:
|
|
100
|
-
log_date = log.get("date", "Unknown")
|
|
101
|
-
log_content = log.get("content", "")
|
|
102
|
-
# Show just the highlights, not full content
|
|
103
|
-
if len(log_content) > 1500:
|
|
104
|
-
log_content = log_content[:1500] + "\n...(truncated)"
|
|
105
|
-
context_parts.append(f"\n### {log_date}")
|
|
106
|
-
context_parts.append(log_content)
|
|
107
|
-
|
|
108
|
-
# ============================================================
|
|
109
|
-
# ORIGINAL MEMORY SYSTEM CONTEXT
|
|
110
|
-
# ============================================================
|
|
111
|
-
|
|
112
|
-
# 1. Get project info
|
|
113
|
-
project_info = await call_memory_skill("get_project_context", {
|
|
114
|
-
"project_path": project_path,
|
|
115
|
-
"limit": 5
|
|
116
|
-
})
|
|
117
|
-
|
|
118
|
-
if project_info and project_info.get("project"):
|
|
119
|
-
proj = project_info["project"]
|
|
120
|
-
context_parts.append(f"\n## Project: {proj.get('name', project_path)}")
|
|
121
|
-
if proj.get("tech_stack"):
|
|
122
|
-
context_parts.append(f"Tech Stack: {', '.join(proj['tech_stack'])}")
|
|
123
|
-
if proj.get("conventions"):
|
|
124
|
-
context_parts.append(f"Conventions: {json.dumps(proj['conventions'], indent=2)}")
|
|
125
|
-
|
|
126
|
-
# 2. Get recent decisions
|
|
127
|
-
decisions = await call_memory_skill("semantic_search", {
|
|
128
|
-
"query": "decision architecture approach",
|
|
129
|
-
"project_path": project_path,
|
|
130
|
-
"type": "decision",
|
|
131
|
-
"limit": 5
|
|
132
|
-
})
|
|
133
|
-
|
|
134
|
-
if decisions and decisions.get("results"):
|
|
135
|
-
context_parts.append("\n## Recent Decisions")
|
|
136
|
-
for d in decisions["results"][:3]:
|
|
137
|
-
context_parts.append(f"- {d['content'][:150]}")
|
|
138
|
-
|
|
139
|
-
# 3. Get recent errors (to avoid repeating)
|
|
140
|
-
errors = await call_memory_skill("semantic_search", {
|
|
141
|
-
"query": "error bug fix problem",
|
|
142
|
-
"project_path": project_path,
|
|
143
|
-
"type": "error",
|
|
144
|
-
"success_only": True, # Only get solved errors
|
|
145
|
-
"limit": 5
|
|
146
|
-
})
|
|
147
|
-
|
|
148
|
-
if errors and errors.get("results"):
|
|
149
|
-
context_parts.append("\n## Past Errors & Solutions")
|
|
150
|
-
for e in errors["results"][:3]:
|
|
151
|
-
context_parts.append(f"- {e['content'][:150]}")
|
|
152
|
-
|
|
153
|
-
# 4. Get session handoff (unresolved items)
|
|
154
|
-
handoff = await call_memory_skill("get_session_handoff", {
|
|
155
|
-
"project_path": project_path,
|
|
156
|
-
"include_last_n_sessions": 2
|
|
157
|
-
})
|
|
158
|
-
|
|
159
|
-
if handoff:
|
|
160
|
-
if handoff.get("unresolved_questions"):
|
|
161
|
-
context_parts.append("\n## Unresolved from Previous Sessions")
|
|
162
|
-
for q in handoff["unresolved_questions"][:3]:
|
|
163
|
-
context_parts.append(f"- {q}")
|
|
164
|
-
|
|
165
|
-
if handoff.get("recent_summaries"):
|
|
166
|
-
context_parts.append("\n## Recent Session Summaries")
|
|
167
|
-
for s in handoff["recent_summaries"][:2]:
|
|
168
|
-
context_parts.append(f"- {s.get('summary', '')[:200]}")
|
|
169
|
-
|
|
170
|
-
# 5. Get relevant patterns
|
|
171
|
-
patterns = await call_memory_skill("search_patterns", {
|
|
172
|
-
"query": "common patterns solutions",
|
|
173
|
-
"limit": 3
|
|
174
|
-
})
|
|
175
|
-
|
|
176
|
-
if patterns and patterns.get("patterns"):
|
|
177
|
-
context_parts.append("\n## Useful Patterns")
|
|
178
|
-
for p in patterns["patterns"][:2]:
|
|
179
|
-
context_parts.append(f"- **{p['name']}**: {p['solution'][:100]}")
|
|
180
|
-
|
|
181
|
-
# 6. Check for anchor conflicts
|
|
182
|
-
conflicts = await call_memory_skill("get_unresolved_conflicts", {
|
|
183
|
-
"project_path": project_path,
|
|
184
|
-
"limit": 3
|
|
185
|
-
})
|
|
186
|
-
|
|
187
|
-
if conflicts and conflicts.get("conflicts"):
|
|
188
|
-
context_parts.append("\n## Unresolved Fact Conflicts")
|
|
189
|
-
for c in conflicts["conflicts"]:
|
|
190
|
-
context_parts.append(f"- {c.get('anchor1_summary', '')} vs {c.get('anchor2_summary', '')}")
|
|
191
|
-
|
|
192
|
-
if context_parts:
|
|
193
|
-
return "\n".join(context_parts)
|
|
194
|
-
return ""
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
async def main():
|
|
198
|
-
# Get project path from environment or current directory
|
|
199
|
-
project_path = os.getenv("PROJECT_PATH") or os.getcwd()
|
|
200
|
-
|
|
201
|
-
# Try to read from stdin if available
|
|
202
|
-
try:
|
|
203
|
-
if not sys.stdin.isatty():
|
|
204
|
-
data = sys.stdin.read()
|
|
205
|
-
if data:
|
|
206
|
-
hook_data = json.loads(data)
|
|
207
|
-
project_path = hook_data.get("project_path", project_path)
|
|
208
|
-
except:
|
|
209
|
-
pass
|
|
210
|
-
|
|
211
|
-
context = await load_session_context(project_path)
|
|
212
|
-
|
|
213
|
-
if context:
|
|
214
|
-
# Output context for Claude to see
|
|
215
|
-
print("\n<memory-context>")
|
|
216
|
-
print("# Loaded from Memory System")
|
|
217
|
-
print(context)
|
|
218
|
-
print("</memory-context>\n")
|
|
219
|
-
else:
|
|
220
|
-
print("\n<memory-context>")
|
|
221
|
-
print("# No prior context found for this project")
|
|
222
|
-
print("Starting fresh session.")
|
|
223
|
-
print("</memory-context>\n")
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
if __name__ == "__main__":
|
|
227
|
-
asyncio.run(main())
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Session start hook - auto-loads relevant context.
|
|
3
|
+
|
|
4
|
+
This hook runs when a Claude Code session starts and:
|
|
5
|
+
- Loads project info and preferences
|
|
6
|
+
- Retrieves recent decisions and patterns
|
|
7
|
+
- Gets unresolved items from previous sessions
|
|
8
|
+
- Injects relevant context into the session
|
|
9
|
+
- Loads daily logs (Moltbot-inspired)
|
|
10
|
+
- Loads MEMORY.md core facts (Moltbot-inspired)
|
|
11
|
+
|
|
12
|
+
Configure in Claude Code settings:
|
|
13
|
+
{
|
|
14
|
+
"hooks": {
|
|
15
|
+
"SessionStart": ["python /path/to/session_start.py"]
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
Output is printed to stdout and injected into Claude's context.
|
|
20
|
+
"""
|
|
21
|
+
import os
|
|
22
|
+
import sys
|
|
23
|
+
import json
|
|
24
|
+
import asyncio
|
|
25
|
+
from datetime import datetime
|
|
26
|
+
from pathlib import Path
|
|
27
|
+
from typing import Dict, Any, List, Optional
|
|
28
|
+
|
|
29
|
+
sys.path.insert(0, str(Path(__file__).parent.parent))
|
|
30
|
+
|
|
31
|
+
import httpx
|
|
32
|
+
|
|
33
|
+
MEMORY_AGENT_URL = os.getenv("MEMORY_AGENT_URL", "http://localhost:8102")
|
|
34
|
+
API_KEY = os.getenv("MEMORY_API_KEY", "")
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
async def call_memory_skill(skill_id: str, params: Dict[str, Any]) -> Optional[Dict[str, Any]]:
|
|
38
|
+
"""Call a memory agent skill."""
|
|
39
|
+
headers = {"Content-Type": "application/json"}
|
|
40
|
+
if API_KEY:
|
|
41
|
+
headers["X-Memory-Key"] = API_KEY
|
|
42
|
+
|
|
43
|
+
payload = {
|
|
44
|
+
"jsonrpc": "2.0",
|
|
45
|
+
"method": "skills/call",
|
|
46
|
+
"params": {
|
|
47
|
+
"skill_id": skill_id,
|
|
48
|
+
"params": params
|
|
49
|
+
},
|
|
50
|
+
"id": f"session-start-{datetime.now().isoformat()}"
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
try:
|
|
54
|
+
async with httpx.AsyncClient(timeout=10.0) as client:
|
|
55
|
+
response = await client.post(
|
|
56
|
+
f"{MEMORY_AGENT_URL}/a2a",
|
|
57
|
+
json=payload,
|
|
58
|
+
headers=headers
|
|
59
|
+
)
|
|
60
|
+
if response.status_code == 200:
|
|
61
|
+
data = response.json()
|
|
62
|
+
return data.get("result", {}).get("result", {})
|
|
63
|
+
except Exception as e:
|
|
64
|
+
pass
|
|
65
|
+
return None
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
async def load_session_context(project_path: str) -> str:
|
|
69
|
+
"""Load all relevant context for a session start."""
|
|
70
|
+
context_parts = []
|
|
71
|
+
|
|
72
|
+
# ============================================================
|
|
73
|
+
# MOLTBOT-INSPIRED: Load MEMORY.md first (core facts)
|
|
74
|
+
# ============================================================
|
|
75
|
+
memory_md = await call_memory_skill("read_memory_md", {
|
|
76
|
+
"project_path": project_path
|
|
77
|
+
})
|
|
78
|
+
|
|
79
|
+
if memory_md and memory_md.get("exists"):
|
|
80
|
+
context_parts.append("## Core Facts (from MEMORY.md)")
|
|
81
|
+
# Include the summary or first part of content
|
|
82
|
+
content = memory_md.get("content", "")
|
|
83
|
+
# Truncate if too long
|
|
84
|
+
if len(content) > 2000:
|
|
85
|
+
content = content[:2000] + "\n...(truncated)"
|
|
86
|
+
context_parts.append(content)
|
|
87
|
+
|
|
88
|
+
# ============================================================
|
|
89
|
+
# MOLTBOT-INSPIRED: Load recent daily logs
|
|
90
|
+
# ============================================================
|
|
91
|
+
daily_logs = await call_memory_skill("daily_log_read", {
|
|
92
|
+
"project_path": project_path,
|
|
93
|
+
"days": 2,
|
|
94
|
+
"max_chars": 3000
|
|
95
|
+
})
|
|
96
|
+
|
|
97
|
+
if daily_logs and daily_logs.get("logs"):
|
|
98
|
+
context_parts.append("\n## Recent Activity (from Daily Logs)")
|
|
99
|
+
for log in daily_logs["logs"]:
|
|
100
|
+
log_date = log.get("date", "Unknown")
|
|
101
|
+
log_content = log.get("content", "")
|
|
102
|
+
# Show just the highlights, not full content
|
|
103
|
+
if len(log_content) > 1500:
|
|
104
|
+
log_content = log_content[:1500] + "\n...(truncated)"
|
|
105
|
+
context_parts.append(f"\n### {log_date}")
|
|
106
|
+
context_parts.append(log_content)
|
|
107
|
+
|
|
108
|
+
# ============================================================
|
|
109
|
+
# ORIGINAL MEMORY SYSTEM CONTEXT
|
|
110
|
+
# ============================================================
|
|
111
|
+
|
|
112
|
+
# 1. Get project info
|
|
113
|
+
project_info = await call_memory_skill("get_project_context", {
|
|
114
|
+
"project_path": project_path,
|
|
115
|
+
"limit": 5
|
|
116
|
+
})
|
|
117
|
+
|
|
118
|
+
if project_info and project_info.get("project"):
|
|
119
|
+
proj = project_info["project"]
|
|
120
|
+
context_parts.append(f"\n## Project: {proj.get('name', project_path)}")
|
|
121
|
+
if proj.get("tech_stack"):
|
|
122
|
+
context_parts.append(f"Tech Stack: {', '.join(proj['tech_stack'])}")
|
|
123
|
+
if proj.get("conventions"):
|
|
124
|
+
context_parts.append(f"Conventions: {json.dumps(proj['conventions'], indent=2)}")
|
|
125
|
+
|
|
126
|
+
# 2. Get recent decisions
|
|
127
|
+
decisions = await call_memory_skill("semantic_search", {
|
|
128
|
+
"query": "decision architecture approach",
|
|
129
|
+
"project_path": project_path,
|
|
130
|
+
"type": "decision",
|
|
131
|
+
"limit": 5
|
|
132
|
+
})
|
|
133
|
+
|
|
134
|
+
if decisions and decisions.get("results"):
|
|
135
|
+
context_parts.append("\n## Recent Decisions")
|
|
136
|
+
for d in decisions["results"][:3]:
|
|
137
|
+
context_parts.append(f"- {d['content'][:150]}")
|
|
138
|
+
|
|
139
|
+
# 3. Get recent errors (to avoid repeating)
|
|
140
|
+
errors = await call_memory_skill("semantic_search", {
|
|
141
|
+
"query": "error bug fix problem",
|
|
142
|
+
"project_path": project_path,
|
|
143
|
+
"type": "error",
|
|
144
|
+
"success_only": True, # Only get solved errors
|
|
145
|
+
"limit": 5
|
|
146
|
+
})
|
|
147
|
+
|
|
148
|
+
if errors and errors.get("results"):
|
|
149
|
+
context_parts.append("\n## Past Errors & Solutions")
|
|
150
|
+
for e in errors["results"][:3]:
|
|
151
|
+
context_parts.append(f"- {e['content'][:150]}")
|
|
152
|
+
|
|
153
|
+
# 4. Get session handoff (unresolved items)
|
|
154
|
+
handoff = await call_memory_skill("get_session_handoff", {
|
|
155
|
+
"project_path": project_path,
|
|
156
|
+
"include_last_n_sessions": 2
|
|
157
|
+
})
|
|
158
|
+
|
|
159
|
+
if handoff:
|
|
160
|
+
if handoff.get("unresolved_questions"):
|
|
161
|
+
context_parts.append("\n## Unresolved from Previous Sessions")
|
|
162
|
+
for q in handoff["unresolved_questions"][:3]:
|
|
163
|
+
context_parts.append(f"- {q}")
|
|
164
|
+
|
|
165
|
+
if handoff.get("recent_summaries"):
|
|
166
|
+
context_parts.append("\n## Recent Session Summaries")
|
|
167
|
+
for s in handoff["recent_summaries"][:2]:
|
|
168
|
+
context_parts.append(f"- {s.get('summary', '')[:200]}")
|
|
169
|
+
|
|
170
|
+
# 5. Get relevant patterns
|
|
171
|
+
patterns = await call_memory_skill("search_patterns", {
|
|
172
|
+
"query": "common patterns solutions",
|
|
173
|
+
"limit": 3
|
|
174
|
+
})
|
|
175
|
+
|
|
176
|
+
if patterns and patterns.get("patterns"):
|
|
177
|
+
context_parts.append("\n## Useful Patterns")
|
|
178
|
+
for p in patterns["patterns"][:2]:
|
|
179
|
+
context_parts.append(f"- **{p['name']}**: {p['solution'][:100]}")
|
|
180
|
+
|
|
181
|
+
# 6. Check for anchor conflicts
|
|
182
|
+
conflicts = await call_memory_skill("get_unresolved_conflicts", {
|
|
183
|
+
"project_path": project_path,
|
|
184
|
+
"limit": 3
|
|
185
|
+
})
|
|
186
|
+
|
|
187
|
+
if conflicts and conflicts.get("conflicts"):
|
|
188
|
+
context_parts.append("\n## Unresolved Fact Conflicts")
|
|
189
|
+
for c in conflicts["conflicts"]:
|
|
190
|
+
context_parts.append(f"- {c.get('anchor1_summary', '')} vs {c.get('anchor2_summary', '')}")
|
|
191
|
+
|
|
192
|
+
if context_parts:
|
|
193
|
+
return "\n".join(context_parts)
|
|
194
|
+
return ""
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
async def main():
|
|
198
|
+
# Get project path from environment or current directory
|
|
199
|
+
project_path = os.getenv("PROJECT_PATH") or os.getcwd()
|
|
200
|
+
|
|
201
|
+
# Try to read from stdin if available
|
|
202
|
+
try:
|
|
203
|
+
if not sys.stdin.isatty():
|
|
204
|
+
data = sys.stdin.read()
|
|
205
|
+
if data:
|
|
206
|
+
hook_data = json.loads(data)
|
|
207
|
+
project_path = hook_data.get("project_path", project_path)
|
|
208
|
+
except:
|
|
209
|
+
pass
|
|
210
|
+
|
|
211
|
+
context = await load_session_context(project_path)
|
|
212
|
+
|
|
213
|
+
if context:
|
|
214
|
+
# Output context for Claude to see
|
|
215
|
+
print("\n<memory-context>")
|
|
216
|
+
print("# Loaded from Memory System")
|
|
217
|
+
print(context)
|
|
218
|
+
print("</memory-context>\n")
|
|
219
|
+
else:
|
|
220
|
+
print("\n<memory-context>")
|
|
221
|
+
print("# No prior context found for this project")
|
|
222
|
+
print("Starting fresh session.")
|
|
223
|
+
print("</memory-context>\n")
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
if __name__ == "__main__":
|
|
227
|
+
asyncio.run(main())
|