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.
Files changed (49) hide show
  1. package/package.json +11 -3
  2. package/src/init.js +197 -12
  3. package/src/tier_map.json +97 -0
  4. package/templates/agents/alignment-validator.md +181 -0
  5. package/templates/agents/analytics-agent.md +93 -0
  6. package/templates/agents/code-simplifier.md +75 -0
  7. package/templates/agents/code-verifier.md +81 -0
  8. package/templates/agents/communication-agent.md +100 -0
  9. package/templates/agents/deployment-manager.md +103 -0
  10. package/templates/agents/incident-responder.md +116 -0
  11. package/templates/agents/local-llm.md +109 -0
  12. package/templates/agents/market-analyst.md +86 -0
  13. package/templates/agents/opportunity-scout.md +103 -0
  14. package/templates/agents/orchestrator.md +91 -0
  15. package/templates/agents/reflection-engine.md +157 -0
  16. package/templates/agents/research-agent.md +76 -0
  17. package/templates/agents/security-scanner.md +94 -0
  18. package/templates/agents/system-monitor.md +113 -0
  19. package/templates/agents/web-designer.md +110 -0
  20. package/templates/hooks/.omc/state/agent-replay-24ba3c54-a19a-4384-85b9-5c509ae41c2c.jsonl +1 -0
  21. package/templates/hooks/.omc/state/idle-notif-cooldown.json +3 -0
  22. package/templates/hooks/.omc/state/subagent-tracking.json +7 -0
  23. package/templates/hooks/__pycache__/agent_trigger.cpython-312.pyc +0 -0
  24. package/templates/hooks/__pycache__/cwd_context_switch.cpython-312.pyc +0 -0
  25. package/templates/hooks/__pycache__/eon_client.cpython-312.pyc +0 -0
  26. package/templates/hooks/__pycache__/eon_memory_search.cpython-312.pyc +0 -0
  27. package/templates/hooks/__pycache__/hook_utils.cpython-312.pyc +0 -0
  28. package/templates/hooks/__pycache__/memory_quality_gate.cpython-312.pyc +0 -0
  29. package/templates/hooks/__pycache__/post_code_check.cpython-312.pyc +0 -0
  30. package/templates/hooks/__pycache__/post_compact_reload.cpython-312.pyc +0 -0
  31. package/templates/hooks/__pycache__/session_end_save.cpython-312.pyc +0 -0
  32. package/templates/hooks/__pycache__/smart_permissions.cpython-312.pyc +0 -0
  33. package/templates/hooks/__pycache__/stop_failure_recovery.cpython-312.pyc +0 -0
  34. package/templates/hooks/agent_trigger.py +220 -0
  35. package/templates/hooks/cwd_context_switch.py +94 -0
  36. package/templates/hooks/eon_client.py +565 -0
  37. package/templates/hooks/eon_memory_search.py +147 -0
  38. package/templates/hooks/hook_utils.py +96 -0
  39. package/templates/hooks/memory_quality_gate.py +97 -0
  40. package/templates/hooks/post_code_check.py +179 -0
  41. package/templates/hooks/post_compact_reload.py +59 -0
  42. package/templates/hooks/session_end_save.py +91 -0
  43. package/templates/hooks/smart_permissions.py +85 -0
  44. package/templates/hooks/stop_failure_recovery.py +57 -0
  45. package/templates/skills/goal-tracker.md +42 -0
  46. package/templates/skills/health-check.md +50 -0
  47. package/templates/skills/memory-audit.md +54 -0
  48. package/templates/skills/self-improvement-loop.md +60 -0
  49. 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()