/ scripts / session-metrics.py
session-metrics.py
  1  #!/usr/bin/env python3
  2  """Extract token counts and costs from Claude Code session JSONL.
  3  
  4  Usage: python3 scripts/session-metrics.py [session_jsonl_path]
  5  
  6  If no path given, looks for the most recent session in ~/.claude/sessions/.
  7  Outputs a summary suitable for pasting into ops/metrics.md.
  8  """
  9  
 10  import json
 11  import sys
 12  import os
 13  from pathlib import Path
 14  from datetime import datetime
 15  
 16  
 17  def find_latest_session():
 18      """Find the most recently modified session JSONL file."""
 19      # Claude Code stores session JSONL in project dirs
 20      claude_dir = Path.home() / ".claude" / "projects"
 21      if not claude_dir.exists():
 22          # Fallback to sessions dir
 23          claude_dir = Path.home() / ".claude" / "sessions"
 24      if not claude_dir.exists():
 25          print(f"Claude directory not found: {claude_dir}", file=sys.stderr)
 26          sys.exit(1)
 27      jsonl_files = list(claude_dir.rglob("*.jsonl"))
 28      # Exclude subagent files
 29      jsonl_files = [f for f in jsonl_files if "subagents" not in str(f)]
 30      if not jsonl_files:
 31          print("No session files found", file=sys.stderr)
 32          sys.exit(1)
 33      return max(jsonl_files, key=lambda f: f.stat().st_mtime)
 34  
 35  
 36  def parse_session(path):
 37      """Parse a session JSONL file and extract metrics."""
 38      total_input = 0
 39      total_output = 0
 40      total_cache_read = 0
 41      total_cache_write = 0
 42      messages = 0
 43      tool_uses = 0
 44      first_ts = None
 45      last_ts = None
 46  
 47      with open(path) as f:
 48          for line in f:
 49              line = line.strip()
 50              if not line:
 51                  continue
 52              try:
 53                  entry = json.loads(line)
 54              except json.JSONDecodeError:
 55                  continue
 56  
 57              # Extract timestamp
 58              ts = entry.get("timestamp") or entry.get("ts")
 59              if ts:
 60                  if first_ts is None:
 61                      first_ts = ts
 62                  last_ts = ts
 63  
 64              # Claude Code JSONL: usage lives in entry.message.usage
 65              msg = entry.get("message", {})
 66              if isinstance(msg, dict):
 67                  usage = msg.get("usage", {})
 68              else:
 69                  usage = entry.get("usage", {})
 70              if usage:
 71                  total_input += usage.get("input_tokens", 0)
 72                  total_output += usage.get("output_tokens", 0)
 73                  total_cache_read += usage.get("cache_read_input_tokens", 0)
 74                  total_cache_write += usage.get("cache_creation_input_tokens", 0)
 75  
 76              # Count messages and tool uses
 77              entry_type = entry.get("type")
 78              if entry_type == "assistant":
 79                  messages += 1
 80                  content = msg.get("content", []) if isinstance(msg, dict) else []
 81                  if isinstance(content, list):
 82                      tool_uses += sum(1 for c in content if isinstance(c, dict) and c.get("type") == "tool_use")
 83  
 84      return {
 85          "input_tokens": total_input,
 86          "output_tokens": total_output,
 87          "cache_read": total_cache_read,
 88          "cache_write": total_cache_write,
 89          "total_tokens": total_input + total_output,
 90          "messages": messages,
 91          "tool_uses": tool_uses,
 92          "first_ts": first_ts,
 93          "last_ts": last_ts,
 94      }
 95  
 96  
 97  def estimate_cost(metrics):
 98      """Estimate cost using Claude Opus pricing (approximate)."""
 99      # Opus pricing: $15/M input, $75/M output, $1.50/M cache read, $3.75/M cache write
100      input_cost = (metrics["input_tokens"] / 1_000_000) * 15.0
101      output_cost = (metrics["output_tokens"] / 1_000_000) * 75.0
102      cache_read_cost = (metrics["cache_read"] / 1_000_000) * 1.50
103      cache_write_cost = (metrics["cache_write"] / 1_000_000) * 3.75
104      return input_cost + output_cost + cache_read_cost + cache_write_cost
105  
106  
107  def main():
108      if len(sys.argv) > 1:
109          path = Path(sys.argv[1])
110      else:
111          path = find_latest_session()
112          print(f"Using session: {path}", file=sys.stderr)
113  
114      if not path.exists():
115          print(f"File not found: {path}", file=sys.stderr)
116          sys.exit(1)
117  
118      m = parse_session(path)
119      cost = estimate_cost(m)
120  
121      print(f"### Session Token Summary")
122      print(f"")
123      print(f"| Metric | Value |")
124      print(f"|--------|-------|")
125      print(f"| Input tokens | {m['input_tokens']:,} |")
126      print(f"| Output tokens | {m['output_tokens']:,} |")
127      print(f"| Cache read | {m['cache_read']:,} |")
128      print(f"| Cache write | {m['cache_write']:,} |")
129      print(f"| Total tokens | {m['total_tokens']:,} |")
130      print(f"| Assistant messages | {m['messages']} |")
131      print(f"| Tool uses | {m['tool_uses']} |")
132      print(f"| Estimated cost | ${cost:.2f} |")
133      if m["first_ts"] and m["last_ts"]:
134          print(f"| First timestamp | {m['first_ts']} |")
135          print(f"| Last timestamp | {m['last_ts']} |")
136  
137  
138  if __name__ == "__main__":
139      main()