Initial commit

This commit is contained in:
Zhongwei Li
2025-11-30 08:57:25 +08:00
commit 37ed95ddbf
10 changed files with 1187 additions and 0 deletions

77
hooks/hooks.json Normal file
View File

@@ -0,0 +1,77 @@
{
"hooks": {
"UserPromptSubmit": [
{
"matcher": "",
"hooks": [
{
"type": "command",
"command": "${CLAUDE_PLUGIN_ROOT}/hooks/log_ndp_events.py --event-type UserPromptSubmit"
}
]
}
],
"PreToolUse": [
{
"matcher": "ndp",
"hooks": [
{
"type": "command",
"command": "${CLAUDE_PLUGIN_ROOT}/hooks/log_ndp_events.py --event-type PreToolUse"
}
]
},
{
"matcher": "*",
"hooks": [
{
"type": "command",
"command": "echo \"$(date +%s.%N),$(ps -o %cpu= -p $$),$(ps -o rss= -p $$),$CLAUDE_TOOL_NAME,start\" >> ~/.claude/performance.csv"
}
]
}
],
"PostToolUse": [
{
"matcher": "ndp",
"hooks": [
{
"type": "command",
"command": "${CLAUDE_PLUGIN_ROOT}/hooks/log_ndp_events.py --event-type PostToolUse"
}
]
},
{
"matcher": "*",
"hooks": [
{
"type": "command",
"command": "echo \"$(date +%s.%N),$(ps -o %cpu= -p $$),$(ps -o rss= -p $$),$CLAUDE_TOOL_NAME,end\" >> ~/.claude/performance.csv; if [[ $(wc -l < ~/.claude/performance.csv) -gt 1000 ]]; then tail -n 500 ~/.claude/performance.csv > ~/.claude/performance.csv.tmp && mv ~/.claude/performance.csv.tmp ~/.claude/performance.csv; fi"
}
]
}
],
"SessionStart": [
{
"matcher": "",
"hooks": [
{
"type": "command",
"command": "${CLAUDE_PLUGIN_ROOT}/hooks/log_ndp_events.py --event-type SessionStart"
}
]
}
],
"Stop": [
{
"matcher": "",
"hooks": [
{
"type": "command",
"command": "${CLAUDE_PLUGIN_ROOT}/hooks/log_ndp_events.py --event-type Stop"
}
]
}
]
}
}

159
hooks/log_ndp_events.py Executable file
View File

@@ -0,0 +1,159 @@
#!/usr/bin/env -S uv run --python 3.10 --script
# /// script
# requires-python = ">=3.10"
# ///
"""
NDP Plugin Event Logger
Logs Claude Code events related to NDP plugin usage to a local file.
Enhanced to capture tool names, user input, and agent responses.
"""
import json
import sys
import os
import argparse
from datetime import datetime
from pathlib import Path
def get_log_file_path():
"""Get the log file path within plugin directory"""
# Get plugin root directory
plugin_root = Path(__file__).parent.parent
logs_dir = plugin_root / "logs"
# Create logs directory if it doesn't exist
logs_dir.mkdir(exist_ok=True)
return logs_dir / "ndp_events.log"
def extract_enhanced_data(event_type: str, event_data: dict) -> dict:
"""Extract enhanced information from event data"""
enhanced = {
"timestamp": datetime.now().isoformat(),
"event_type": event_type,
"session_id": event_data.get("session_id", "unknown"),
}
# Extract tool information for PreToolUse and PostToolUse
if event_type in ["PreToolUse", "PostToolUse"]:
tool_data = event_data.get('tool', {})
if tool_data:
enhanced['tool_name'] = tool_data.get('name', 'unknown')
enhanced['tool_input'] = tool_data.get('input', {})
# For PostToolUse, capture tool results
if event_type == "PostToolUse":
if 'result' in event_data:
enhanced['tool_result'] = event_data['result']
if 'output' in event_data:
enhanced['tool_output'] = event_data['output']
if 'error' in event_data:
enhanced['tool_error'] = event_data['error']
# Extract user input for UserPromptSubmit
if event_type == "UserPromptSubmit":
if 'text' in event_data:
enhanced['user_prompt'] = event_data['text']
if 'messages' in event_data:
enhanced['conversation_messages'] = event_data['messages']
# For PostToolUse, extract agent response from transcript
if event_type == "PostToolUse" and 'transcript_path' in event_data:
transcript_path = event_data['transcript_path']
if os.path.exists(transcript_path):
try:
# Read last few messages to capture recent agent response
recent_chat = []
with open(transcript_path, 'r') as f:
lines = f.readlines()
# Get last 5 messages to capture context
for line in lines[-5:]:
line = line.strip()
if line:
try:
msg = json.loads(line)
recent_chat.append(msg)
except json.JSONDecodeError:
pass
enhanced['recent_chat'] = recent_chat
# Extract the latest agent response
for msg in reversed(recent_chat):
if msg.get('role') == 'assistant':
enhanced['latest_agent_response'] = msg.get('content', [])
break
except Exception as e:
enhanced['transcript_read_error'] = str(e)
# For Stop event, optionally include full chat if requested
if event_type == "Stop" and 'transcript_path' in event_data:
transcript_path = event_data['transcript_path']
if os.path.exists(transcript_path):
try:
chat_data = []
with open(transcript_path, 'r') as f:
for line in f:
line = line.strip()
if line:
try:
chat_data.append(json.loads(line))
except json.JSONDecodeError:
pass
# Add summary statistics
enhanced['chat_summary'] = {
'total_messages': len(chat_data),
'user_messages': sum(1 for msg in chat_data if msg.get('role') == 'user'),
'assistant_messages': sum(1 for msg in chat_data if msg.get('role') == 'assistant'),
}
# Optionally include last few messages
enhanced['last_5_messages'] = chat_data[-5:] if chat_data else []
except Exception as e:
enhanced['chat_read_error'] = str(e)
# Include raw event data for completeness
enhanced['raw_data'] = event_data
return enhanced
def log_event(event_type: str, event_data: dict):
"""Log event to file with enhanced data extraction"""
try:
log_file = get_log_file_path()
# Prepare enhanced log entry
log_entry = extract_enhanced_data(event_type, event_data)
# Append to log file (one JSON object per line)
with open(log_file, "a") as f:
f.write(json.dumps(log_entry) + "\n")
return True
except Exception as e:
# Fail silently to not block Claude Code
print(f"Warning: Failed to log event: {e}", file=sys.stderr)
return False
def main():
parser = argparse.ArgumentParser(description='Log NDP plugin events with enhanced data capture')
parser.add_argument('--event-type', required=True, help='Type of event')
args = parser.parse_args()
try:
# Read event data from stdin
event_data = json.load(sys.stdin)
except json.JSONDecodeError:
event_data = {}
# Log the event with enhanced data
log_event(args.event_type, event_data)
# Always exit successfully to not block Claude Code
sys.exit(0)
if __name__ == '__main__':
main()