eon-memory 1.2.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 (47) hide show
  1. package/package.json +3 -2
  2. package/templates/agents/alignment-validator.md +181 -0
  3. package/templates/agents/analytics-agent.md +93 -0
  4. package/templates/agents/code-simplifier.md +75 -0
  5. package/templates/agents/code-verifier.md +81 -0
  6. package/templates/agents/communication-agent.md +100 -0
  7. package/templates/agents/deployment-manager.md +103 -0
  8. package/templates/agents/incident-responder.md +116 -0
  9. package/templates/agents/local-llm.md +109 -0
  10. package/templates/agents/market-analyst.md +86 -0
  11. package/templates/agents/opportunity-scout.md +103 -0
  12. package/templates/agents/orchestrator.md +91 -0
  13. package/templates/agents/reflection-engine.md +157 -0
  14. package/templates/agents/research-agent.md +76 -0
  15. package/templates/agents/security-scanner.md +94 -0
  16. package/templates/agents/system-monitor.md +113 -0
  17. package/templates/agents/web-designer.md +110 -0
  18. package/templates/hooks/.omc/state/agent-replay-24ba3c54-a19a-4384-85b9-5c509ae41c2c.jsonl +1 -0
  19. package/templates/hooks/.omc/state/idle-notif-cooldown.json +3 -0
  20. package/templates/hooks/.omc/state/subagent-tracking.json +7 -0
  21. package/templates/hooks/__pycache__/agent_trigger.cpython-312.pyc +0 -0
  22. package/templates/hooks/__pycache__/cwd_context_switch.cpython-312.pyc +0 -0
  23. package/templates/hooks/__pycache__/eon_client.cpython-312.pyc +0 -0
  24. package/templates/hooks/__pycache__/eon_memory_search.cpython-312.pyc +0 -0
  25. package/templates/hooks/__pycache__/hook_utils.cpython-312.pyc +0 -0
  26. package/templates/hooks/__pycache__/memory_quality_gate.cpython-312.pyc +0 -0
  27. package/templates/hooks/__pycache__/post_code_check.cpython-312.pyc +0 -0
  28. package/templates/hooks/__pycache__/post_compact_reload.cpython-312.pyc +0 -0
  29. package/templates/hooks/__pycache__/session_end_save.cpython-312.pyc +0 -0
  30. package/templates/hooks/__pycache__/smart_permissions.cpython-312.pyc +0 -0
  31. package/templates/hooks/__pycache__/stop_failure_recovery.cpython-312.pyc +0 -0
  32. package/templates/hooks/agent_trigger.py +220 -0
  33. package/templates/hooks/cwd_context_switch.py +94 -0
  34. package/templates/hooks/eon_client.py +565 -0
  35. package/templates/hooks/eon_memory_search.py +147 -0
  36. package/templates/hooks/hook_utils.py +96 -0
  37. package/templates/hooks/memory_quality_gate.py +97 -0
  38. package/templates/hooks/post_code_check.py +179 -0
  39. package/templates/hooks/post_compact_reload.py +59 -0
  40. package/templates/hooks/session_end_save.py +91 -0
  41. package/templates/hooks/smart_permissions.py +85 -0
  42. package/templates/hooks/stop_failure_recovery.py +57 -0
  43. package/templates/skills/goal-tracker.md +42 -0
  44. package/templates/skills/health-check.md +50 -0
  45. package/templates/skills/memory-audit.md +54 -0
  46. package/templates/skills/self-improvement-loop.md +60 -0
  47. package/templates/skills/x-alignment-check.md +68 -0
@@ -0,0 +1,147 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ EON Memory Search Hook - Semantic Context Injection
4
+ ======================================================
5
+ Searches EON Memory on every user prompt and injects relevant
6
+ context into the conversation.
7
+
8
+ Uses eon_client.py for API calls to the EON backend.
9
+
10
+ Hook Type: UserPromptSubmit
11
+ Version: 1.0.0
12
+ """
13
+
14
+ import json
15
+ import sys
16
+ from pathlib import Path
17
+
18
+ # Add hooks directory for eon_client import
19
+ HOOKS_DIR = str(Path(__file__).parent)
20
+ if HOOKS_DIR not in sys.path:
21
+ sys.path.insert(0, HOOKS_DIR)
22
+
23
+ # Common greetings to skip (no search needed)
24
+ SKIP_MESSAGES = {
25
+ "hi", "hello", "hey", "ok", "okay", "yes", "no",
26
+ "thanks", "thank you", "bye", "quit", "exit",
27
+ }
28
+
29
+ # Minimum message length for search
30
+ MIN_LENGTH = 15
31
+
32
+ # Maximum results to inject
33
+ MAX_RESULTS = 5
34
+
35
+
36
+ def extract_keywords(text: str) -> str:
37
+ """Extract meaningful keywords from text for search."""
38
+ # Remove common stop words
39
+ stop_words = {
40
+ "the", "a", "an", "is", "are", "was", "were", "be", "been",
41
+ "being", "have", "has", "had", "do", "does", "did", "will",
42
+ "would", "could", "should", "may", "might", "can", "to",
43
+ "of", "in", "for", "on", "with", "at", "by", "from", "as",
44
+ "into", "through", "during", "before", "after", "and", "but",
45
+ "or", "not", "no", "so", "if", "then", "than", "too", "very",
46
+ "just", "about", "up", "out", "how", "what", "which", "who",
47
+ "when", "where", "why", "all", "each", "every", "both", "few",
48
+ "more", "most", "other", "some", "such", "only", "own", "same",
49
+ "that", "this", "it", "i", "me", "my", "we", "our", "you",
50
+ "your", "he", "she", "they", "them", "please", "help",
51
+ }
52
+
53
+ words = text.lower().split()
54
+ keywords = [w for w in words if w not in stop_words and len(w) > 2]
55
+ return " ".join(keywords[:10])
56
+
57
+
58
+ def format_results(results: list) -> str:
59
+ """Format search results as context string."""
60
+ if not results:
61
+ return ""
62
+
63
+ lines = [
64
+ "============================================================",
65
+ "EON MEMORY CONTEXT",
66
+ "============================================================",
67
+ "",
68
+ ]
69
+
70
+ for r in results:
71
+ memory_id = r.get("memory_id", "?")
72
+ title = r.get("title", "Untitled")
73
+ preview = r.get("content_preview", "")[:80]
74
+ project = r.get("project_id", "")
75
+ similarity = r.get("similarity", 0)
76
+
77
+ lines.append(f"[#{memory_id}] {title}")
78
+ if project:
79
+ lines.append(f" Project: {project} | Relevance: {similarity:.2f}")
80
+ if preview:
81
+ lines.append(f" Preview: {preview}")
82
+ lines.append("")
83
+
84
+ lines.extend([
85
+ "============================================================",
86
+ "Use this context to inform your response.",
87
+ "============================================================",
88
+ ])
89
+
90
+ return "\n".join(lines)
91
+
92
+
93
+ def main():
94
+ try:
95
+ input_data = json.load(sys.stdin)
96
+ except Exception:
97
+ sys.exit(0)
98
+
99
+ prompt = input_data.get("prompt", "")
100
+
101
+ # Skip short messages and greetings
102
+ stripped = prompt.strip().lower().rstrip("!.,?")
103
+ if not prompt or len(prompt) < MIN_LENGTH or stripped in SKIP_MESSAGES:
104
+ sys.exit(0)
105
+
106
+ # Try to search via eon_client
107
+ try:
108
+ from eon_client import get_client
109
+
110
+ client = get_client()
111
+ if not client:
112
+ sys.exit(0)
113
+
114
+ # Extract keywords for better search
115
+ query = extract_keywords(prompt)
116
+ if not query:
117
+ query = prompt[:100]
118
+
119
+ results = client.search(query, n_results=MAX_RESULTS)
120
+
121
+ if not results:
122
+ sys.exit(0)
123
+
124
+ # Convert Memory objects to dicts for formatting
125
+ result_dicts = [dict(r) for r in results]
126
+ context = format_results(result_dicts)
127
+
128
+ if not context:
129
+ sys.exit(0)
130
+
131
+ output = {
132
+ "hookSpecificOutput": {
133
+ "hookEventName": "UserPromptSubmit",
134
+ "additionalContext": context,
135
+ }
136
+ }
137
+
138
+ print(json.dumps(output))
139
+
140
+ except Exception:
141
+ pass # Fail silently - don't block the user
142
+
143
+ sys.exit(0)
144
+
145
+
146
+ if __name__ == "__main__":
147
+ main()
@@ -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()