mirror of
https://github.com/alleneubank/claude-code.git
synced 2026-09-14 14:06:56 +08:00
274 lines
8.1 KiB
Python
Executable File
274 lines
8.1 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
"""
|
|
Claude Code Usage Tracker
|
|
Logs usage data to SQLite database for analysis
|
|
"""
|
|
|
|
import json
|
|
import sqlite3
|
|
import sys
|
|
import os
|
|
from datetime import datetime
|
|
from pathlib import Path
|
|
|
|
# Configuration
|
|
DB_PATH = Path.home() / ".claude" / "usage-tracking.db"
|
|
LOG_PATH = Path.home() / ".claude" / "usage-logs"
|
|
|
|
def init_database():
|
|
"""Initialize SQLite database with required tables"""
|
|
conn = sqlite3.connect(DB_PATH)
|
|
cursor = conn.cursor()
|
|
|
|
# Sessions table
|
|
cursor.execute('''
|
|
CREATE TABLE IF NOT EXISTS sessions (
|
|
session_id TEXT PRIMARY KEY,
|
|
project_path TEXT,
|
|
start_time TIMESTAMP,
|
|
end_time TIMESTAMP,
|
|
duration_seconds INTEGER,
|
|
transcript_path TEXT
|
|
)
|
|
''')
|
|
|
|
# Tool usage table
|
|
cursor.execute('''
|
|
CREATE TABLE IF NOT EXISTS tool_usage (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
session_id TEXT,
|
|
timestamp TIMESTAMP,
|
|
tool_name TEXT,
|
|
tool_category TEXT,
|
|
input_size INTEGER,
|
|
output_size INTEGER,
|
|
project_path TEXT,
|
|
FOREIGN KEY (session_id) REFERENCES sessions(session_id)
|
|
)
|
|
''')
|
|
|
|
# Commands table
|
|
cursor.execute('''
|
|
CREATE TABLE IF NOT EXISTS commands (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
session_id TEXT,
|
|
timestamp TIMESTAMP,
|
|
command TEXT,
|
|
description TEXT,
|
|
project_path TEXT,
|
|
FOREIGN KEY (session_id) REFERENCES sessions(session_id)
|
|
)
|
|
''')
|
|
|
|
# Events table for general tracking
|
|
cursor.execute('''
|
|
CREATE TABLE IF NOT EXISTS events (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
session_id TEXT,
|
|
timestamp TIMESTAMP,
|
|
event_type TEXT,
|
|
event_data TEXT,
|
|
project_path TEXT,
|
|
FOREIGN KEY (session_id) REFERENCES sessions(session_id)
|
|
)
|
|
''')
|
|
|
|
conn.commit()
|
|
conn.close()
|
|
|
|
def get_project_path():
|
|
"""Get current project path"""
|
|
return os.getcwd()
|
|
|
|
def categorize_tool(tool_name):
|
|
"""Categorize tools for better analysis"""
|
|
categories = {
|
|
'Read': 'file_ops',
|
|
'Write': 'file_ops',
|
|
'Edit': 'file_ops',
|
|
'MultiEdit': 'file_ops',
|
|
'NotebookRead': 'file_ops',
|
|
'NotebookEdit': 'file_ops',
|
|
'Bash': 'shell',
|
|
'Task': 'agent',
|
|
'Glob': 'search',
|
|
'Grep': 'search',
|
|
'LS': 'search',
|
|
'WebFetch': 'web',
|
|
'WebSearch': 'web',
|
|
'TodoRead': 'planning',
|
|
'TodoWrite': 'planning',
|
|
'exit_plan_mode': 'planning'
|
|
}
|
|
|
|
# Handle MCP tools
|
|
if tool_name.startswith('mcp__'):
|
|
return 'mcp'
|
|
|
|
return categories.get(tool_name, 'other')
|
|
|
|
def log_tool_usage(data):
|
|
"""Log tool usage to database"""
|
|
conn = sqlite3.connect(DB_PATH)
|
|
cursor = conn.cursor()
|
|
|
|
tool_input = json.dumps(data.get('tool_input', {}))
|
|
tool_response = json.dumps(data.get('tool_response', {})) if 'tool_response' in data else '{}'
|
|
|
|
cursor.execute('''
|
|
INSERT INTO tool_usage
|
|
(session_id, timestamp, tool_name, tool_category, input_size, output_size, project_path)
|
|
VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
''', (
|
|
data['session_id'],
|
|
datetime.now().isoformat(),
|
|
data['tool_name'],
|
|
categorize_tool(data['tool_name']),
|
|
len(tool_input),
|
|
len(tool_response),
|
|
get_project_path()
|
|
))
|
|
|
|
# Special handling for Bash commands
|
|
if data['tool_name'] == 'Bash' and 'tool_input' in data:
|
|
command = data['tool_input'].get('command', '')
|
|
description = data['tool_input'].get('description', '')
|
|
|
|
cursor.execute('''
|
|
INSERT INTO commands
|
|
(session_id, timestamp, command, description, project_path)
|
|
VALUES (?, ?, ?, ?, ?)
|
|
''', (
|
|
data['session_id'],
|
|
datetime.now().isoformat(),
|
|
command,
|
|
description,
|
|
get_project_path()
|
|
))
|
|
|
|
conn.commit()
|
|
conn.close()
|
|
|
|
def log_session_event(data, event_type):
|
|
"""Log session start/stop events"""
|
|
conn = sqlite3.connect(DB_PATH)
|
|
cursor = conn.cursor()
|
|
|
|
if event_type == 'start':
|
|
# Check if session already exists
|
|
cursor.execute('SELECT session_id FROM sessions WHERE session_id = ?', (data['session_id'],))
|
|
if not cursor.fetchone():
|
|
cursor.execute('''
|
|
INSERT INTO sessions
|
|
(session_id, project_path, start_time, transcript_path)
|
|
VALUES (?, ?, ?, ?)
|
|
''', (
|
|
data['session_id'],
|
|
get_project_path(),
|
|
datetime.now().isoformat(),
|
|
data.get('transcript_path', '')
|
|
))
|
|
|
|
elif event_type in ['stop', 'subagent_stop']:
|
|
# Update session end time
|
|
cursor.execute('''
|
|
UPDATE sessions
|
|
SET end_time = ?,
|
|
duration_seconds = CAST((julianday(?) - julianday(start_time)) * 86400 AS INTEGER)
|
|
WHERE session_id = ?
|
|
''', (
|
|
datetime.now().isoformat(),
|
|
datetime.now().isoformat(),
|
|
data['session_id']
|
|
))
|
|
|
|
# Log as event
|
|
cursor.execute('''
|
|
INSERT INTO events
|
|
(session_id, timestamp, event_type, event_data, project_path)
|
|
VALUES (?, ?, ?, ?, ?)
|
|
''', (
|
|
data['session_id'],
|
|
datetime.now().isoformat(),
|
|
event_type,
|
|
json.dumps(data),
|
|
get_project_path()
|
|
))
|
|
|
|
conn.commit()
|
|
conn.close()
|
|
|
|
def main():
|
|
"""Main entry point for hook"""
|
|
try:
|
|
# Initialize database if needed
|
|
init_database()
|
|
|
|
# Read input data
|
|
input_data = json.load(sys.stdin)
|
|
|
|
# Determine hook type from environment or infer from data
|
|
hook_type = os.environ.get('CLAUDE_HOOK_TYPE', '')
|
|
|
|
if 'tool_name' in input_data and 'tool_input' in input_data:
|
|
if 'tool_response' in input_data:
|
|
hook_type = 'PostToolUse'
|
|
else:
|
|
hook_type = 'PreToolUse'
|
|
|
|
# Handle different hook types
|
|
if hook_type == 'PreToolUse':
|
|
# Only ensure session exists for pre-tool use, don't log usage yet
|
|
log_session_event(input_data, 'start')
|
|
|
|
elif hook_type == 'PostToolUse':
|
|
# Log tool usage only after completion to avoid duplicates
|
|
log_tool_usage(input_data)
|
|
log_session_event(input_data, 'start') # Ensure session exists
|
|
|
|
elif hook_type == 'Stop':
|
|
log_session_event(input_data, 'stop')
|
|
|
|
elif hook_type == 'SubagentStop':
|
|
log_session_event(input_data, 'subagent_stop')
|
|
|
|
elif hook_type == 'Notification':
|
|
# Log notification as event
|
|
conn = sqlite3.connect(DB_PATH)
|
|
cursor = conn.cursor()
|
|
cursor.execute('''
|
|
INSERT INTO events
|
|
(session_id, timestamp, event_type, event_data, project_path)
|
|
VALUES (?, ?, ?, ?, ?)
|
|
''', (
|
|
input_data['session_id'],
|
|
datetime.now().isoformat(),
|
|
'notification',
|
|
json.dumps(input_data),
|
|
get_project_path()
|
|
))
|
|
conn.commit()
|
|
conn.close()
|
|
|
|
# Also write to JSON log for backup
|
|
LOG_PATH.mkdir(parents=True, exist_ok=True)
|
|
log_file = LOG_PATH / f"{datetime.now().strftime('%Y-%m-%d')}.jsonl"
|
|
|
|
with open(log_file, 'a') as f:
|
|
json.dump({
|
|
'timestamp': datetime.now().isoformat(),
|
|
'hook_type': hook_type,
|
|
'project': get_project_path(),
|
|
'data': input_data
|
|
}, f)
|
|
f.write('\n')
|
|
|
|
except Exception as e:
|
|
# Don't block Claude Code operation
|
|
print(f"Usage tracking error: {e}", file=sys.stderr)
|
|
sys.exit(0) # Exit successfully to not block
|
|
|
|
sys.exit(0)
|
|
|
|
if __name__ == '__main__':
|
|
main() |