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.
- package/package.json +3 -2
- 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,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()
|