claude-code-ultimate-guide/examples/scripts/session-stats.sh
Florian BRUNIAUX 8a4d116e2e feat(docs): add LLM Handbook + Google Whitepaper integration v3.3.0
Advanced Guardrails:
- prompt-injection-detector.sh (PreToolUse)
- output-validator.sh (PostToolUse heuristics)
- claudemd-scanner.sh (SessionStart injection detection)
- output-secrets-scanner.sh (PostToolUse secrets leak prevention)

Observability & Monitoring:
- session-logger.sh (JSONL activity logging)
- session-stats.sh (cost tracking & analysis)
- guide/observability.md (full documentation)

LLM-as-a-Judge Evaluation:
- output-evaluator.md agent (Haiku)
- /validate-changes command
- pre-commit-evaluator.sh (opt-in git hook)

Google Agent Whitepaper Integration:
- Context Triage Guide (Section 2.2.4)
- CLAUDE.md Injection Warning (Section 3.1.3)
- Agent Validation Checklist (Section 4.2.4)
- MCP Security: Tool Shadowing & Confused Deputy (Section 8.6)
- Session vs Memory patterns (Section 3.3.3)

Stats: 10 new files, 8 modified, 5 new guide sections

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-14 21:00:49 +01:00

235 lines
7.8 KiB
Bash
Executable file

#!/bin/bash
# session-stats.sh - Analyze Claude Code session logs
#
# Analyzes logs created by session-logger.sh hook and outputs statistics
# about tool usage, estimated costs, and session patterns.
#
# Usage:
# ./session-stats.sh # Today's summary
# ./session-stats.sh --range week # Last 7 days
# ./session-stats.sh --range month # Last 30 days
# ./session-stats.sh --date 2026-01-14 # Specific date
# ./session-stats.sh --json # Machine-readable output
# ./session-stats.sh --project myapp # Filter by project
#
# Environment:
# CLAUDE_LOG_DIR - Log directory (default: ~/.claude/logs)
#
# Cost rates (per 1K tokens, configurable):
# CLAUDE_RATE_INPUT - Input token rate (default: 0.003 for Sonnet)
# CLAUDE_RATE_OUTPUT - Output token rate (default: 0.015 for Sonnet)
set -euo pipefail
# Colors
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
CYAN='\033[0;36m'
NC='\033[0m'
# Configuration
LOG_DIR="${CLAUDE_LOG_DIR:-$HOME/.claude/logs}"
RATE_INPUT="${CLAUDE_RATE_INPUT:-0.003}"
RATE_OUTPUT="${CLAUDE_RATE_OUTPUT:-0.015}"
# Defaults
OUTPUT_MODE="human"
DATE_RANGE="today"
SPECIFIC_DATE=""
PROJECT_FILTER=""
# Parse arguments
while [[ $# -gt 0 ]]; do
case $1 in
--json)
OUTPUT_MODE="json"
shift
;;
--range)
DATE_RANGE="$2"
shift 2
;;
--date)
SPECIFIC_DATE="$2"
DATE_RANGE="specific"
shift 2
;;
--project)
PROJECT_FILTER="$2"
shift 2
;;
--help|-h)
grep '^#' "$0" | sed 's/^# \?//'
exit 0
;;
*)
echo "Unknown option: $1"
echo "Use --help for usage information"
exit 1
;;
esac
done
# Check if log directory exists
if [[ ! -d "$LOG_DIR" ]]; then
if [[ "$OUTPUT_MODE" == "json" ]]; then
echo '{"error": "No logs found", "log_dir": "'"$LOG_DIR"'"}'
else
echo -e "${RED}No logs found in $LOG_DIR${NC}"
echo "Enable session-logger.sh hook to start collecting logs."
fi
exit 1
fi
# Determine which log files to analyze
LOG_FILES=()
case "$DATE_RANGE" in
today)
TODAY=$(date +%Y-%m-%d)
[[ -f "$LOG_DIR/activity-$TODAY.jsonl" ]] && LOG_FILES+=("$LOG_DIR/activity-$TODAY.jsonl")
;;
week)
for i in {0..6}; do
DATE=$(date -v-${i}d +%Y-%m-%d 2>/dev/null || date -d "-$i days" +%Y-%m-%d)
[[ -f "$LOG_DIR/activity-$DATE.jsonl" ]] && LOG_FILES+=("$LOG_DIR/activity-$DATE.jsonl")
done
;;
month)
for i in {0..29}; do
DATE=$(date -v-${i}d +%Y-%m-%d 2>/dev/null || date -d "-$i days" +%Y-%m-%d)
[[ -f "$LOG_DIR/activity-$DATE.jsonl" ]] && LOG_FILES+=("$LOG_DIR/activity-$DATE.jsonl")
done
;;
specific)
[[ -f "$LOG_DIR/activity-$SPECIFIC_DATE.jsonl" ]] && LOG_FILES+=("$LOG_DIR/activity-$SPECIFIC_DATE.jsonl")
;;
esac
if [[ ${#LOG_FILES[@]} -eq 0 ]]; then
if [[ "$OUTPUT_MODE" == "json" ]]; then
echo '{"error": "No logs found for specified range", "range": "'"$DATE_RANGE"'"}'
else
echo -e "${YELLOW}No logs found for range: $DATE_RANGE${NC}"
fi
exit 0
fi
# Combine and filter logs
COMBINED_LOGS=$(cat "${LOG_FILES[@]}" 2>/dev/null || true)
if [[ -n "$PROJECT_FILTER" ]]; then
COMBINED_LOGS=$(echo "$COMBINED_LOGS" | jq -c "select(.project == \"$PROJECT_FILTER\")" 2>/dev/null || true)
fi
if [[ -z "$COMBINED_LOGS" ]]; then
if [[ "$OUTPUT_MODE" == "json" ]]; then
echo '{"error": "No matching logs found"}'
else
echo -e "${YELLOW}No matching logs found${NC}"
fi
exit 0
fi
# Calculate statistics
TOTAL_OPS=$(echo "$COMBINED_LOGS" | wc -l | tr -d ' ')
TOTAL_TOKENS_IN=$(echo "$COMBINED_LOGS" | jq -s '[.[].tokens.input // 0] | add')
TOTAL_TOKENS_OUT=$(echo "$COMBINED_LOGS" | jq -s '[.[].tokens.output // 0] | add')
TOTAL_TOKENS=$((TOTAL_TOKENS_IN + TOTAL_TOKENS_OUT))
# Cost calculation
COST_INPUT=$(echo "scale=4; $TOTAL_TOKENS_IN * $RATE_INPUT / 1000" | bc)
COST_OUTPUT=$(echo "scale=4; $TOTAL_TOKENS_OUT * $RATE_OUTPUT / 1000" | bc)
COST_TOTAL=$(echo "scale=4; $COST_INPUT + $COST_OUTPUT" | bc)
# Tool breakdown
TOOL_STATS=$(echo "$COMBINED_LOGS" | jq -s 'group_by(.tool) | map({tool: .[0].tool, count: length}) | sort_by(-.count)')
# Session count
SESSION_COUNT=$(echo "$COMBINED_LOGS" | jq -s '[.[].session_id] | unique | length')
# Project breakdown
PROJECT_STATS=$(echo "$COMBINED_LOGS" | jq -s 'group_by(.project) | map({project: .[0].project, count: length}) | sort_by(-.count)')
# Most edited files
FILE_STATS=$(echo "$COMBINED_LOGS" | jq -s '[.[] | select(.file != null)] | group_by(.file) | map({file: .[0].file, count: length}) | sort_by(-.count) | .[0:10]')
# Output
if [[ "$OUTPUT_MODE" == "json" ]]; then
jq -n \
--arg range "$DATE_RANGE" \
--argjson total_ops "$TOTAL_OPS" \
--argjson sessions "$SESSION_COUNT" \
--argjson tokens_in "$TOTAL_TOKENS_IN" \
--argjson tokens_out "$TOTAL_TOKENS_OUT" \
--argjson tokens_total "$TOTAL_TOKENS" \
--arg cost_in "$COST_INPUT" \
--arg cost_out "$COST_OUTPUT" \
--arg cost_total "$COST_TOTAL" \
--argjson tools "$TOOL_STATS" \
--argjson projects "$PROJECT_STATS" \
--argjson files "$FILE_STATS" \
'{
range: $range,
summary: {
total_operations: $total_ops,
sessions: $sessions,
tokens: {
input: $tokens_in,
output: $tokens_out,
total: $tokens_total
},
estimated_cost: {
input: ($cost_in | tonumber),
output: ($cost_out | tonumber),
total: ($cost_total | tonumber),
currency: "USD"
}
},
tools: $tools,
projects: $projects,
top_files: $files
}'
else
echo ""
echo -e "${CYAN}═══════════════════════════════════════════════════════════${NC}"
echo -e "${CYAN} Claude Code Session Statistics - $DATE_RANGE${NC}"
echo -e "${CYAN}═══════════════════════════════════════════════════════════${NC}"
echo ""
echo -e "${BLUE}Summary${NC}"
echo " Total operations: $TOTAL_OPS"
echo " Sessions: $SESSION_COUNT"
echo ""
echo -e "${BLUE}Token Usage${NC}"
printf " Input tokens: %'d\n" "$TOTAL_TOKENS_IN"
printf " Output tokens: %'d\n" "$TOTAL_TOKENS_OUT"
printf " Total tokens: %'d\n" "$TOTAL_TOKENS"
echo ""
echo -e "${BLUE}Estimated Cost (Sonnet rates)${NC}"
printf " Input: \$%.4f\n" "$COST_INPUT"
printf " Output: \$%.4f\n" "$COST_OUTPUT"
printf " ${GREEN}Total: \$%.4f${NC}\n" "$COST_TOTAL"
echo ""
echo -e "${BLUE}Tools Used${NC}"
echo "$TOOL_STATS" | jq -r '.[] | " \(.tool): \(.count)"'
echo ""
echo -e "${BLUE}Projects${NC}"
echo "$PROJECT_STATS" | jq -r '.[] | " \(.project): \(.count)"'
echo ""
echo -e "${BLUE}Most Edited Files${NC}"
echo "$FILE_STATS" | jq -r '.[] | " \(.file | split("/")[-1]): \(.count)"' | head -5
echo ""
echo -e "${CYAN}═══════════════════════════════════════════════════════════${NC}"
echo -e "Logs: $LOG_DIR"
echo -e "Rate config: \$${RATE_INPUT}/1K in, \$${RATE_OUTPUT}/1K out"
echo ""
fi