Created
March 31, 2026 05:33
-
-
Save ani03sha/f880463e3abb72dcacc82f2bf05cb4d0 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/usr/bin/env python3 | |
| """ | |
| Analyze per-skill token usage from a Claude Code session JSONL file. | |
| Usage: | |
| python3 skill_token_usage.py <path_to_session.jsonl> | |
| If no path given, uses the most recent session file found. | |
| """ | |
| import json | |
| import sys | |
| import os | |
| import glob | |
| from collections import defaultdict | |
| def find_latest_session(): | |
| pattern = os.path.expanduser("~/.claude/projects/**/*.jsonl") | |
| files = [f for f in glob.glob(pattern, recursive=True) if "/subagents/" not in f] | |
| if not files: | |
| return None | |
| return max(files, key=os.path.getmtime) | |
| def parse_session(path): | |
| records = [] | |
| with open(path) as f: | |
| for line in f: | |
| line = line.strip() | |
| if not line: | |
| continue | |
| try: | |
| records.append(json.loads(line)) | |
| except json.JSONDecodeError: | |
| continue | |
| return records | |
| def get_usage(record): | |
| msg = record.get("message", {}) | |
| usage = msg.get("usage", {}) | |
| return { | |
| "input": usage.get("input_tokens", 0), | |
| "output": usage.get("output_tokens", 0), | |
| "cache_read": usage.get("cache_read_input_tokens", 0), | |
| "cache_create": usage.get("cache_creation_input_tokens", 0), | |
| } | |
| def total_tokens(u): | |
| return u["input"] + u["output"] + u["cache_read"] + u["cache_create"] | |
| def add_usage(a, b): | |
| return {k: a[k] + b[k] for k in a} | |
| def main(): | |
| path = sys.argv[1] if len(sys.argv) > 1 else find_latest_session() | |
| if not path: | |
| print("No session file found.") | |
| sys.exit(1) | |
| print(f"Analyzing: {path}\n") | |
| records = parse_session(path) | |
| # Build UUID -> record map for assistant messages | |
| uuid_to_record = {r["uuid"]: r for r in records if "uuid" in r} | |
| # Track current skill context as we scan user messages in order | |
| skill_usage = defaultdict(lambda: {"input": 0, "output": 0, "cache_read": 0, "cache_create": 0}) | |
| session_total = {"input": 0, "output": 0, "cache_read": 0, "cache_create": 0} | |
| current_skill = None | |
| # Collect (uuid, skill_or_None) for all records in order | |
| # Strategy: find user messages that invoke a skill, then attribute | |
| # subsequent assistant messages to that skill until next user turn | |
| # Pass 1: build ordered list of (uuid, type, skill_name or None) | |
| ordered = [] | |
| for r in records: | |
| rtype = r.get("type") | |
| if rtype == "user": | |
| content = r.get("message", {}).get("content", "") | |
| skill = None | |
| if isinstance(content, str) and "<command-name>" in content: | |
| # Extract skill name from <command-name>/skill-name</command-name> | |
| start = content.find("<command-name>") + len("<command-name>") | |
| end = content.find("</command-name>") | |
| if end > start: | |
| skill = content[start:end].lstrip("/") | |
| ordered.append(("user", r, skill)) | |
| elif rtype == "assistant": | |
| ordered.append(("assistant", r, None)) | |
| # Pass 2: accumulate tokens, attributing to active skill | |
| for kind, r, skill in ordered: | |
| if kind == "user": | |
| if skill: | |
| current_skill = skill | |
| elif kind == "assistant": | |
| u = get_usage(r) | |
| if any(u.values()): | |
| session_total = add_usage(session_total, u) | |
| key = current_skill if current_skill else "(no skill / manual)" | |
| skill_usage[key] = add_usage(skill_usage[key], u) | |
| # Print results | |
| print(f"{'Skill':<25} {'Input':>10} {'Output':>10} {'Cache Read':>15} {'Cache Create':>14} {'Total':>15}") | |
| print("-" * 95) | |
| for skill, u in sorted(skill_usage.items(), key=lambda x: -total_tokens(x[1])): | |
| print(f"{skill:<25} {u['input']:>10,} {u['output']:>10,} {u['cache_read']:>15,} {u['cache_create']:>14,} {total_tokens(u):>15,}") | |
| print("-" * 95) | |
| print(f"{'SESSION TOTAL':<25} {session_total['input']:>10,} {session_total['output']:>10,} {session_total['cache_read']:>15,} {session_total['cache_create']:>14,} {total_tokens(session_total):>15,}") | |
| if __name__ == "__main__": | |
| main() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment