eon-memory 1.1.0 → 1.2.1
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/package.json +11 -3
- package/src/init.js +197 -12
- package/src/tier_map.json +97 -0
- package/templates/agents/alignment-validator.md +181 -0
- package/templates/agents/analytics-agent.md +93 -0
- package/templates/agents/code-simplifier.md +75 -0
- package/templates/agents/code-verifier.md +81 -0
- package/templates/agents/communication-agent.md +100 -0
- package/templates/agents/deployment-manager.md +103 -0
- package/templates/agents/incident-responder.md +116 -0
- package/templates/agents/local-llm.md +109 -0
- package/templates/agents/market-analyst.md +86 -0
- package/templates/agents/opportunity-scout.md +103 -0
- package/templates/agents/orchestrator.md +91 -0
- package/templates/agents/reflection-engine.md +157 -0
- package/templates/agents/research-agent.md +76 -0
- package/templates/agents/security-scanner.md +94 -0
- package/templates/agents/system-monitor.md +113 -0
- package/templates/agents/web-designer.md +110 -0
- package/templates/hooks/.omc/state/agent-replay-24ba3c54-a19a-4384-85b9-5c509ae41c2c.jsonl +1 -0
- package/templates/hooks/.omc/state/idle-notif-cooldown.json +3 -0
- package/templates/hooks/.omc/state/subagent-tracking.json +7 -0
- package/templates/hooks/__pycache__/agent_trigger.cpython-312.pyc +0 -0
- package/templates/hooks/__pycache__/cwd_context_switch.cpython-312.pyc +0 -0
- package/templates/hooks/__pycache__/eon_client.cpython-312.pyc +0 -0
- package/templates/hooks/__pycache__/eon_memory_search.cpython-312.pyc +0 -0
- package/templates/hooks/__pycache__/hook_utils.cpython-312.pyc +0 -0
- package/templates/hooks/__pycache__/memory_quality_gate.cpython-312.pyc +0 -0
- package/templates/hooks/__pycache__/post_code_check.cpython-312.pyc +0 -0
- package/templates/hooks/__pycache__/post_compact_reload.cpython-312.pyc +0 -0
- package/templates/hooks/__pycache__/session_end_save.cpython-312.pyc +0 -0
- package/templates/hooks/__pycache__/smart_permissions.cpython-312.pyc +0 -0
- package/templates/hooks/__pycache__/stop_failure_recovery.cpython-312.pyc +0 -0
- package/templates/hooks/agent_trigger.py +220 -0
- package/templates/hooks/cwd_context_switch.py +94 -0
- package/templates/hooks/eon_client.py +565 -0
- package/templates/hooks/eon_memory_search.py +147 -0
- package/templates/hooks/hook_utils.py +96 -0
- package/templates/hooks/memory_quality_gate.py +97 -0
- package/templates/hooks/post_code_check.py +179 -0
- package/templates/hooks/post_compact_reload.py +59 -0
- package/templates/hooks/session_end_save.py +91 -0
- package/templates/hooks/smart_permissions.py +85 -0
- package/templates/hooks/stop_failure_recovery.py +57 -0
- package/templates/skills/goal-tracker.md +42 -0
- package/templates/skills/health-check.md +50 -0
- package/templates/skills/memory-audit.md +54 -0
- package/templates/skills/self-improvement-loop.md +60 -0
- package/templates/skills/x-alignment-check.md +68 -0
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Shared Utilities for EON Hooks
|
|
4
|
+
================================
|
|
5
|
+
Common functions for post_code_check.py and session_end_save.py.
|
|
6
|
+
|
|
7
|
+
- Project detection from file paths
|
|
8
|
+
- Tracking file I/O (JSONL)
|
|
9
|
+
- Change summary generation from tool input
|
|
10
|
+
|
|
11
|
+
Version: 1.0.0
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
import json
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
from datetime import datetime
|
|
17
|
+
|
|
18
|
+
TRACKING_FILE = Path("/tmp/eon_session_changes.jsonl")
|
|
19
|
+
BATCH_THRESHOLD = 5 # Write memory after 5 changes
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def detect_project(files):
|
|
23
|
+
"""Detect project from file paths."""
|
|
24
|
+
for f in files:
|
|
25
|
+
fl = f.lower()
|
|
26
|
+
# User can customize these patterns for their own projects
|
|
27
|
+
if any(kw in fl for kw in ["frontend", "web", "ui", "app"]):
|
|
28
|
+
return "frontend"
|
|
29
|
+
if any(kw in fl for kw in ["api", "backend", "server"]):
|
|
30
|
+
return "backend"
|
|
31
|
+
if any(kw in fl for kw in ["test", "spec", "e2e"]):
|
|
32
|
+
return "testing"
|
|
33
|
+
if any(kw in fl for kw in ["infra", "deploy", "docker", "k8s"]):
|
|
34
|
+
return "infrastructure"
|
|
35
|
+
return "default"
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def read_tracking_file():
|
|
39
|
+
"""Read all entries from the tracking file."""
|
|
40
|
+
if not TRACKING_FILE.exists():
|
|
41
|
+
return []
|
|
42
|
+
try:
|
|
43
|
+
lines = TRACKING_FILE.read_text().strip().split('\n')
|
|
44
|
+
return [json.loads(l) for l in lines if l.strip()]
|
|
45
|
+
except Exception:
|
|
46
|
+
return []
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def append_tracking(entry):
|
|
50
|
+
"""Append an entry to the tracking file."""
|
|
51
|
+
try:
|
|
52
|
+
with open(TRACKING_FILE, "a") as f:
|
|
53
|
+
f.write(json.dumps(entry) + "\n")
|
|
54
|
+
except Exception:
|
|
55
|
+
pass
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def get_changes_since_last_memory():
|
|
59
|
+
"""Get all changes since the last batch memory was written."""
|
|
60
|
+
entries = read_tracking_file()
|
|
61
|
+
changes = []
|
|
62
|
+
for entry in reversed(entries):
|
|
63
|
+
if entry.get("_batch_memory"):
|
|
64
|
+
break
|
|
65
|
+
if entry.get("file"):
|
|
66
|
+
changes.append(entry)
|
|
67
|
+
return list(reversed(changes))
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def generate_change_summary(tool_name, tool_input):
|
|
71
|
+
"""Generate a human-readable summary of a code change."""
|
|
72
|
+
file_path = tool_input.get("file_path", "unknown")
|
|
73
|
+
filename = Path(file_path).name
|
|
74
|
+
|
|
75
|
+
if tool_name == "Write":
|
|
76
|
+
return f"{filename}: file written"
|
|
77
|
+
elif tool_name == "Edit":
|
|
78
|
+
old = tool_input.get("old_string", "")
|
|
79
|
+
new = tool_input.get("new_string", "")
|
|
80
|
+
old_lines = len(old.strip().split('\n')) if old else 0
|
|
81
|
+
new_lines = len(new.strip().split('\n')) if new else 0
|
|
82
|
+
return f"{filename}: {old_lines}L replaced with {new_lines}L"
|
|
83
|
+
elif tool_name == "NotebookEdit":
|
|
84
|
+
return f"{filename}: notebook cell edited"
|
|
85
|
+
return f"{filename}: modified via {tool_name}"
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def count_changed_lines(tool_name, tool_input):
|
|
89
|
+
"""Count approximate lines changed."""
|
|
90
|
+
if tool_name == "Edit":
|
|
91
|
+
new = tool_input.get("new_string", "")
|
|
92
|
+
return len(new.strip().split('\n')) if new else 0
|
|
93
|
+
elif tool_name == "Write":
|
|
94
|
+
content = tool_input.get("content", "")
|
|
95
|
+
return len(content.strip().split('\n')) if content else 0
|
|
96
|
+
return 0
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Memory Quality Gate Hook
|
|
4
|
+
==========================
|
|
5
|
+
Validates memory quality after eon_create MCP tool calls.
|
|
6
|
+
Warns if the memory is missing key quality indicators.
|
|
7
|
+
|
|
8
|
+
Hook Type: PostToolUse
|
|
9
|
+
Matcher: mcp__eon-memory__eon_create
|
|
10
|
+
|
|
11
|
+
Version: 1.0.0
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
import json
|
|
15
|
+
import sys
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def check_quality(tool_input: dict) -> list:
|
|
19
|
+
"""Check memory quality and return warnings."""
|
|
20
|
+
warnings = []
|
|
21
|
+
|
|
22
|
+
title = tool_input.get("title", "")
|
|
23
|
+
content = tool_input.get("content", "")
|
|
24
|
+
project_id = tool_input.get("project_id", "")
|
|
25
|
+
|
|
26
|
+
# Title checks
|
|
27
|
+
if len(title) < 10:
|
|
28
|
+
warnings.append("Title is very short - be more descriptive")
|
|
29
|
+
|
|
30
|
+
# Content checks
|
|
31
|
+
if len(content) < 50:
|
|
32
|
+
warnings.append("Content is brief - add more detail for future reference")
|
|
33
|
+
|
|
34
|
+
# Check for WHY (problem context)
|
|
35
|
+
why_indicators = ["because", "reason", "why", "problem", "issue", "caused by",
|
|
36
|
+
"root cause", "due to", "in order to"]
|
|
37
|
+
has_why = any(ind in content.lower() for ind in why_indicators)
|
|
38
|
+
if not has_why and len(content) > 100:
|
|
39
|
+
warnings.append("Missing WHY - explain the reason/problem, not just what was done")
|
|
40
|
+
|
|
41
|
+
# Check for HOW (solution details)
|
|
42
|
+
how_indicators = ["fix", "solution", "changed", "updated", "added", "removed",
|
|
43
|
+
"modified", "implemented", "configured"]
|
|
44
|
+
has_how = any(ind in content.lower() for ind in how_indicators)
|
|
45
|
+
if not has_how and len(content) > 100:
|
|
46
|
+
warnings.append("Missing HOW - describe what was changed/fixed")
|
|
47
|
+
|
|
48
|
+
# Project check
|
|
49
|
+
if not project_id or project_id == "default":
|
|
50
|
+
warnings.append("No project_id set - organize memories by project")
|
|
51
|
+
|
|
52
|
+
return warnings
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def main():
|
|
56
|
+
try:
|
|
57
|
+
input_data = json.load(sys.stdin)
|
|
58
|
+
except Exception:
|
|
59
|
+
sys.exit(0)
|
|
60
|
+
|
|
61
|
+
tool_name = input_data.get("tool_name", "")
|
|
62
|
+
|
|
63
|
+
# Only trigger on eon_create
|
|
64
|
+
if "eon_create" not in tool_name and "eon-memory" not in tool_name:
|
|
65
|
+
sys.exit(0)
|
|
66
|
+
|
|
67
|
+
tool_input = input_data.get("tool_input", {})
|
|
68
|
+
warnings = check_quality(tool_input)
|
|
69
|
+
|
|
70
|
+
if not warnings:
|
|
71
|
+
sys.exit(0)
|
|
72
|
+
|
|
73
|
+
context = "\n".join([
|
|
74
|
+
"",
|
|
75
|
+
"============================================================",
|
|
76
|
+
"MEMORY QUALITY CHECK",
|
|
77
|
+
"============================================================",
|
|
78
|
+
"",
|
|
79
|
+
] + [f" - {w}" for w in warnings] + [
|
|
80
|
+
"",
|
|
81
|
+
"Tip: High-quality memories include PROBLEM (why) + FIX (how) + context",
|
|
82
|
+
"============================================================",
|
|
83
|
+
])
|
|
84
|
+
|
|
85
|
+
output = {
|
|
86
|
+
"hookSpecificOutput": {
|
|
87
|
+
"hookEventName": "PostToolUse",
|
|
88
|
+
"additionalContext": context,
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
print(json.dumps(output))
|
|
93
|
+
sys.exit(0)
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
if __name__ == "__main__":
|
|
97
|
+
main()
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Post-Code Check Hook - Change Tracking + Reminders
|
|
4
|
+
=====================================================
|
|
5
|
+
Runs after Edit/Write/NotebookEdit operations.
|
|
6
|
+
|
|
7
|
+
1. Tracks changes with rich data (summary, line count)
|
|
8
|
+
2. Reminds about code-verifier, security-scanner, code-simplifier
|
|
9
|
+
3. Tracks verification state (A10 principle)
|
|
10
|
+
|
|
11
|
+
Hook Type: PostToolUse
|
|
12
|
+
Matcher: Edit|Write|NotebookEdit|Bash
|
|
13
|
+
|
|
14
|
+
Version: 1.0.0
|
|
15
|
+
"""
|
|
16
|
+
import json
|
|
17
|
+
import sys
|
|
18
|
+
import os
|
|
19
|
+
from pathlib import Path
|
|
20
|
+
from datetime import datetime
|
|
21
|
+
|
|
22
|
+
# Add hooks directory to import path
|
|
23
|
+
HOOKS_DIR = str(Path(__file__).parent)
|
|
24
|
+
if HOOKS_DIR not in sys.path:
|
|
25
|
+
sys.path.insert(0, HOOKS_DIR)
|
|
26
|
+
|
|
27
|
+
try:
|
|
28
|
+
from hook_utils import (
|
|
29
|
+
TRACKING_FILE, generate_change_summary,
|
|
30
|
+
count_changed_lines, append_tracking,
|
|
31
|
+
)
|
|
32
|
+
HAS_UTILS = True
|
|
33
|
+
except ImportError:
|
|
34
|
+
HAS_UTILS = False
|
|
35
|
+
TRACKING_FILE = Path("/tmp/eon_session_changes.jsonl")
|
|
36
|
+
|
|
37
|
+
# Tools that change code
|
|
38
|
+
CODE_CHANGING_TOOLS = ['Edit', 'Write', 'NotebookEdit']
|
|
39
|
+
|
|
40
|
+
# Skip paths (no tracking for these)
|
|
41
|
+
SKIP_PATTERNS = [
|
|
42
|
+
'/logs/', '/tmp/', '/cache/', '.log', '.jsonl', '__pycache__',
|
|
43
|
+
'/node_modules/', '.deprecated', '.backup', '/backups/',
|
|
44
|
+
]
|
|
45
|
+
|
|
46
|
+
# Verification state file
|
|
47
|
+
VERIFICATION_STATE = Path("/tmp/eon_verification_state.json")
|
|
48
|
+
|
|
49
|
+
# Patterns that count as verification (in Bash commands)
|
|
50
|
+
VERIFY_PATTERNS = [
|
|
51
|
+
"py_compile", "pytest", "python -m pytest",
|
|
52
|
+
"tsc --noEmit", "npx tsc",
|
|
53
|
+
'python -c "from', "python -c 'from",
|
|
54
|
+
'python3 -c "from', "python3 -c 'from",
|
|
55
|
+
"npm test", "npm run test",
|
|
56
|
+
"cargo test", "go test",
|
|
57
|
+
]
|
|
58
|
+
|
|
59
|
+
# File extensions that require verification
|
|
60
|
+
CODE_EXTENSIONS = {'.py', '.ts', '.tsx', '.js', '.jsx', '.rs', '.go', '.java'}
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def track_change(file_path, tool_name, tool_input):
|
|
64
|
+
"""Track a code change."""
|
|
65
|
+
if HAS_UTILS:
|
|
66
|
+
summary = generate_change_summary(tool_name, tool_input)
|
|
67
|
+
lines = count_changed_lines(tool_name, tool_input)
|
|
68
|
+
append_tracking({
|
|
69
|
+
"file": file_path,
|
|
70
|
+
"tool": tool_name,
|
|
71
|
+
"ts": datetime.now().isoformat(),
|
|
72
|
+
"summary": summary,
|
|
73
|
+
"lines": lines,
|
|
74
|
+
})
|
|
75
|
+
else:
|
|
76
|
+
try:
|
|
77
|
+
entry = json.dumps({
|
|
78
|
+
"file": file_path,
|
|
79
|
+
"tool": tool_name,
|
|
80
|
+
"timestamp": datetime.now().isoformat(),
|
|
81
|
+
})
|
|
82
|
+
with open(TRACKING_FILE, "a") as f:
|
|
83
|
+
f.write(entry + "\n")
|
|
84
|
+
except Exception:
|
|
85
|
+
pass
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def main():
|
|
89
|
+
try:
|
|
90
|
+
input_data = json.load(sys.stdin)
|
|
91
|
+
except Exception:
|
|
92
|
+
sys.exit(0)
|
|
93
|
+
|
|
94
|
+
tool_name = input_data.get("tool_name", "")
|
|
95
|
+
tool_input = input_data.get("tool_input", {})
|
|
96
|
+
|
|
97
|
+
reminders = []
|
|
98
|
+
|
|
99
|
+
# 1. After code changes: track + set verification state
|
|
100
|
+
if tool_name in CODE_CHANGING_TOOLS:
|
|
101
|
+
file_path = tool_input.get("file_path", "")
|
|
102
|
+
|
|
103
|
+
if file_path and not any(p in file_path for p in SKIP_PATTERNS):
|
|
104
|
+
track_change(file_path, tool_name, tool_input)
|
|
105
|
+
|
|
106
|
+
# Set verification state to false when code file is changed
|
|
107
|
+
ext = Path(file_path).suffix
|
|
108
|
+
if ext in CODE_EXTENSIONS:
|
|
109
|
+
try:
|
|
110
|
+
state = {}
|
|
111
|
+
if VERIFICATION_STATE.exists():
|
|
112
|
+
state = json.loads(VERIFICATION_STATE.read_text())
|
|
113
|
+
files = state.get("files", [])
|
|
114
|
+
if file_path not in files:
|
|
115
|
+
files.append(file_path)
|
|
116
|
+
VERIFICATION_STATE.write_text(json.dumps({
|
|
117
|
+
"verified": False,
|
|
118
|
+
"files": files,
|
|
119
|
+
"last_change": datetime.now().isoformat(),
|
|
120
|
+
}))
|
|
121
|
+
except Exception:
|
|
122
|
+
pass
|
|
123
|
+
|
|
124
|
+
# Reminder for verification
|
|
125
|
+
if file_path and Path(file_path).suffix in CODE_EXTENSIONS:
|
|
126
|
+
reminders.append(
|
|
127
|
+
"Reminder: Verify changes before marking complete "
|
|
128
|
+
f"(file: {os.path.basename(file_path)})"
|
|
129
|
+
)
|
|
130
|
+
|
|
131
|
+
# Large changes -> suggest simplifier
|
|
132
|
+
new_string = tool_input.get("new_string", "")
|
|
133
|
+
if len(new_string) > 500:
|
|
134
|
+
reminders.append("Note: Large code change - consider code-simplifier")
|
|
135
|
+
|
|
136
|
+
# 2. After Bash commands: deployment reminder + verification detection
|
|
137
|
+
if tool_name == "Bash":
|
|
138
|
+
command = tool_input.get("command", "")
|
|
139
|
+
if any(kw in command.lower() for kw in ['git push', 'docker', 'deploy', 'systemctl']):
|
|
140
|
+
reminders.append("Post-deployment: consider running system-monitor")
|
|
141
|
+
|
|
142
|
+
# Detect verification commands -> set state to true
|
|
143
|
+
if any(vp in command for vp in VERIFY_PATTERNS):
|
|
144
|
+
try:
|
|
145
|
+
VERIFICATION_STATE.write_text(json.dumps({
|
|
146
|
+
"verified": True,
|
|
147
|
+
"verified_at": datetime.now().isoformat(),
|
|
148
|
+
}))
|
|
149
|
+
reminders.append("Verification detected - changes verified")
|
|
150
|
+
except Exception:
|
|
151
|
+
pass
|
|
152
|
+
|
|
153
|
+
if not reminders:
|
|
154
|
+
sys.exit(0)
|
|
155
|
+
|
|
156
|
+
context = "\n".join([
|
|
157
|
+
"",
|
|
158
|
+
"============================================================",
|
|
159
|
+
"POST-CODE CHECK",
|
|
160
|
+
"============================================================",
|
|
161
|
+
"",
|
|
162
|
+
] + reminders + [
|
|
163
|
+
"",
|
|
164
|
+
"============================================================",
|
|
165
|
+
])
|
|
166
|
+
|
|
167
|
+
output = {
|
|
168
|
+
"hookSpecificOutput": {
|
|
169
|
+
"hookEventName": "PostToolUse",
|
|
170
|
+
"additionalContext": context,
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
print(json.dumps(output))
|
|
175
|
+
sys.exit(0)
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
if __name__ == "__main__":
|
|
179
|
+
main()
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Post-Compact Reload Hook
|
|
4
|
+
===========================
|
|
5
|
+
Reloads session state after Claude Code compacts the conversation.
|
|
6
|
+
Ensures memory context is not lost during long sessions.
|
|
7
|
+
|
|
8
|
+
Hook Type: PostCompact
|
|
9
|
+
Version: 1.0.0
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
import json
|
|
13
|
+
import sys
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
|
|
16
|
+
# Add hooks directory
|
|
17
|
+
HOOKS_DIR = str(Path(__file__).parent)
|
|
18
|
+
if HOOKS_DIR not in sys.path:
|
|
19
|
+
sys.path.insert(0, HOOKS_DIR)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def main():
|
|
23
|
+
context_parts = ["Session context reloaded after compaction."]
|
|
24
|
+
|
|
25
|
+
# Try to reload context from EON
|
|
26
|
+
try:
|
|
27
|
+
from eon_client import get_client
|
|
28
|
+
|
|
29
|
+
client = get_client()
|
|
30
|
+
if client:
|
|
31
|
+
ctx = client.get_context()
|
|
32
|
+
recent = ctx.recent_memories
|
|
33
|
+
stats = ctx.stats
|
|
34
|
+
|
|
35
|
+
if recent:
|
|
36
|
+
context_parts.append(f"Active memories: {stats.get('total_memories', '?')}")
|
|
37
|
+
context_parts.append("Recent context:")
|
|
38
|
+
for m in recent[:3]:
|
|
39
|
+
title = m.get("title", "Untitled")
|
|
40
|
+
project = m.get("project_id", "")
|
|
41
|
+
context_parts.append(f" - [{project}] {title}")
|
|
42
|
+
except Exception:
|
|
43
|
+
pass
|
|
44
|
+
|
|
45
|
+
context = "\n".join(context_parts)
|
|
46
|
+
|
|
47
|
+
output = {
|
|
48
|
+
"hookSpecificOutput": {
|
|
49
|
+
"hookEventName": "PostCompact",
|
|
50
|
+
"additionalContext": f"\n[EON Reload] {context}\n",
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
print(json.dumps(output))
|
|
55
|
+
sys.exit(0)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
if __name__ == "__main__":
|
|
59
|
+
main()
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Session End Save Hook
|
|
4
|
+
=======================
|
|
5
|
+
Saves session state when Claude Code session ends.
|
|
6
|
+
Persists summary, decisions, and blockers via EON API.
|
|
7
|
+
|
|
8
|
+
Uses eon_client.py for API calls.
|
|
9
|
+
|
|
10
|
+
Hook Type: Stop
|
|
11
|
+
Version: 1.0.0
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
import json
|
|
15
|
+
import sys
|
|
16
|
+
from pathlib import Path
|
|
17
|
+
from datetime import datetime
|
|
18
|
+
|
|
19
|
+
# Add hooks directory for imports
|
|
20
|
+
HOOKS_DIR = str(Path(__file__).parent)
|
|
21
|
+
if HOOKS_DIR not in sys.path:
|
|
22
|
+
sys.path.insert(0, HOOKS_DIR)
|
|
23
|
+
|
|
24
|
+
TRACKING_FILE = Path("/tmp/eon_session_changes.jsonl")
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def collect_session_data() -> dict:
|
|
28
|
+
"""Collect session data from tracking file."""
|
|
29
|
+
changes = []
|
|
30
|
+
if TRACKING_FILE.exists():
|
|
31
|
+
try:
|
|
32
|
+
lines = TRACKING_FILE.read_text().strip().split('\n')
|
|
33
|
+
for line in lines:
|
|
34
|
+
if line.strip():
|
|
35
|
+
entry = json.loads(line)
|
|
36
|
+
if entry.get("file"):
|
|
37
|
+
changes.append(entry)
|
|
38
|
+
except Exception:
|
|
39
|
+
pass
|
|
40
|
+
|
|
41
|
+
# Deduplicate files
|
|
42
|
+
files_changed = list(set(c.get("file", "") for c in changes if c.get("file")))
|
|
43
|
+
|
|
44
|
+
return {
|
|
45
|
+
"total_changes": len(changes),
|
|
46
|
+
"files_changed": files_changed[:20], # Cap at 20
|
|
47
|
+
"summaries": [c.get("summary", "") for c in changes[-10:] if c.get("summary")],
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def main():
|
|
52
|
+
session_data = collect_session_data()
|
|
53
|
+
|
|
54
|
+
if session_data["total_changes"] == 0:
|
|
55
|
+
# No changes tracked - nothing to save
|
|
56
|
+
sys.exit(0)
|
|
57
|
+
|
|
58
|
+
# Build summary
|
|
59
|
+
summary_parts = [
|
|
60
|
+
f"Session ended at {datetime.now().isoformat()[:16]}",
|
|
61
|
+
f"Changes: {session_data['total_changes']} operations",
|
|
62
|
+
f"Files: {len(session_data['files_changed'])} modified",
|
|
63
|
+
]
|
|
64
|
+
|
|
65
|
+
if session_data["summaries"]:
|
|
66
|
+
summary_parts.append("Recent: " + "; ".join(session_data["summaries"][-5:]))
|
|
67
|
+
|
|
68
|
+
summary = " | ".join(summary_parts)
|
|
69
|
+
|
|
70
|
+
# Save via EON API
|
|
71
|
+
try:
|
|
72
|
+
from eon_client import get_client
|
|
73
|
+
|
|
74
|
+
client = get_client()
|
|
75
|
+
if client:
|
|
76
|
+
client.save_session(summary=summary)
|
|
77
|
+
except Exception:
|
|
78
|
+
pass # Fail silently on session end
|
|
79
|
+
|
|
80
|
+
# Clean up tracking file
|
|
81
|
+
try:
|
|
82
|
+
if TRACKING_FILE.exists():
|
|
83
|
+
TRACKING_FILE.unlink()
|
|
84
|
+
except Exception:
|
|
85
|
+
pass
|
|
86
|
+
|
|
87
|
+
sys.exit(0)
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
if __name__ == "__main__":
|
|
91
|
+
main()
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Smart Permissions Hook
|
|
4
|
+
========================
|
|
5
|
+
Dynamic permission management for Claude Code tools.
|
|
6
|
+
Read operations are always allowed; write/destructive operations
|
|
7
|
+
require explicit context-aware approval.
|
|
8
|
+
|
|
9
|
+
Hook Type: PreToolUse
|
|
10
|
+
Version: 1.0.0
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
import json
|
|
14
|
+
import sys
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
|
|
17
|
+
# Tools that are always allowed (read-only)
|
|
18
|
+
ALWAYS_ALLOW = {
|
|
19
|
+
"Read", "Glob", "Grep", "WebFetch", "WebSearch",
|
|
20
|
+
"Agent", "Skill", "ToolSearch",
|
|
21
|
+
"TaskCreate", "TaskUpdate", "TaskGet", "TaskList",
|
|
22
|
+
"SendMessage", "EnterPlanMode", "ExitPlanMode",
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
# MCP tools that are always allowed
|
|
26
|
+
ALWAYS_ALLOW_MCP_PATTERNS = [
|
|
27
|
+
"eon_search", "eon_list", "eon_get", "eon_health",
|
|
28
|
+
"eon_stats", "eon_similar", "eon_projects",
|
|
29
|
+
"eon_goals_list", "eon_session_start",
|
|
30
|
+
]
|
|
31
|
+
|
|
32
|
+
# Bash commands that are always allowed (read-only)
|
|
33
|
+
SAFE_BASH_PATTERNS = [
|
|
34
|
+
"ls ", "pwd", "echo ", "cat ", "head ", "tail ",
|
|
35
|
+
"git status", "git log", "git diff", "git branch",
|
|
36
|
+
"python -m py_compile", "python3 -m py_compile",
|
|
37
|
+
"pytest", "npm test", "tsc --noEmit",
|
|
38
|
+
"docker ps", "docker logs",
|
|
39
|
+
"curl ", "wget ",
|
|
40
|
+
]
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def main():
|
|
44
|
+
try:
|
|
45
|
+
input_data = json.load(sys.stdin)
|
|
46
|
+
except Exception:
|
|
47
|
+
sys.exit(0)
|
|
48
|
+
|
|
49
|
+
tool_name = input_data.get("tool_name", "")
|
|
50
|
+
tool_input = input_data.get("tool_input", {})
|
|
51
|
+
|
|
52
|
+
# Always-allow tools
|
|
53
|
+
if tool_name in ALWAYS_ALLOW:
|
|
54
|
+
sys.exit(0)
|
|
55
|
+
|
|
56
|
+
# MCP tools - check patterns
|
|
57
|
+
if tool_name.startswith("mcp__"):
|
|
58
|
+
for pattern in ALWAYS_ALLOW_MCP_PATTERNS:
|
|
59
|
+
if pattern in tool_name:
|
|
60
|
+
sys.exit(0)
|
|
61
|
+
|
|
62
|
+
# Bash - check for safe commands
|
|
63
|
+
if tool_name == "Bash":
|
|
64
|
+
command = tool_input.get("command", "")
|
|
65
|
+
for pattern in SAFE_BASH_PATTERNS:
|
|
66
|
+
if command.strip().startswith(pattern) or pattern in command:
|
|
67
|
+
sys.exit(0)
|
|
68
|
+
|
|
69
|
+
# Everything else: provide context but don't block
|
|
70
|
+
# (Claude Code's built-in permission system handles blocking)
|
|
71
|
+
context = f"Permission check: {tool_name} requires approval"
|
|
72
|
+
|
|
73
|
+
output = {
|
|
74
|
+
"hookSpecificOutput": {
|
|
75
|
+
"hookEventName": "PreToolUse",
|
|
76
|
+
"additionalContext": context,
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
print(json.dumps(output))
|
|
81
|
+
sys.exit(0)
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
if __name__ == "__main__":
|
|
85
|
+
main()
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Stop Failure Recovery Hook
|
|
4
|
+
============================
|
|
5
|
+
Recovery when a Claude Code session fails to stop cleanly.
|
|
6
|
+
Attempts to save session state before exiting.
|
|
7
|
+
|
|
8
|
+
Hook Type: StopFailure
|
|
9
|
+
Version: 1.0.0
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
import json
|
|
13
|
+
import sys
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
from datetime import datetime
|
|
16
|
+
|
|
17
|
+
# Add hooks directory
|
|
18
|
+
HOOKS_DIR = str(Path(__file__).parent)
|
|
19
|
+
if HOOKS_DIR not in sys.path:
|
|
20
|
+
sys.path.insert(0, HOOKS_DIR)
|
|
21
|
+
|
|
22
|
+
STATE_FILE = Path("/tmp/eon_stop_failure.json")
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def main():
|
|
26
|
+
# Record the failure
|
|
27
|
+
try:
|
|
28
|
+
STATE_FILE.write_text(json.dumps({
|
|
29
|
+
"failure_at": datetime.now().isoformat(),
|
|
30
|
+
"recovered": False,
|
|
31
|
+
}))
|
|
32
|
+
except Exception:
|
|
33
|
+
pass
|
|
34
|
+
|
|
35
|
+
# Try to save session via API
|
|
36
|
+
try:
|
|
37
|
+
from eon_client import get_client
|
|
38
|
+
|
|
39
|
+
client = get_client()
|
|
40
|
+
if client:
|
|
41
|
+
client.save_session(
|
|
42
|
+
summary="Session ended with StopFailure - auto-recovery attempted"
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
# Mark as recovered
|
|
46
|
+
STATE_FILE.write_text(json.dumps({
|
|
47
|
+
"failure_at": datetime.now().isoformat(),
|
|
48
|
+
"recovered": True,
|
|
49
|
+
}))
|
|
50
|
+
except Exception:
|
|
51
|
+
pass # Best effort
|
|
52
|
+
|
|
53
|
+
sys.exit(0)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
if __name__ == "__main__":
|
|
57
|
+
main()
|