#!/usr/bin/env python3 """Extrage token usage real din transcripturile Claude Code (~/.claude/projects/**/*.jsonl). READ-ONLY: nu scrie, nu mută, nu șterge nimic. Emite JSON agregat pe stdout. Poate fi rulat remote fără a atinge discul: ssh host 'python3 -' < acest_fisier Deduplicare: cheia (message.id, requestId) — aceeași cerere API poate apărea în mai multe fișiere (resume, fork de sesiune, backup-uri). """ import json, os, sys, glob, argparse, hashlib def main(): ap = argparse.ArgumentParser() ap.add_argument("--root", default=os.path.expanduser("~/.claude/projects")) ap.add_argument("--machine", default=os.uname().nodename) a = ap.parse_args() rows = {} # (session, date, model) -> tokens sessions = {} # session -> meta seen = set() # chei de deduplicare dup = 0 total_assistant = 0 files = sorted(glob.glob(os.path.join(a.root, "**", "*.jsonl"), recursive=True)) bad_lines = 0 for fp in files: project = os.path.basename(os.path.dirname(fp)) try: fh = open(fp, "r", encoding="utf-8", errors="replace") except OSError: continue with fh: for line in fh: line = line.strip() if not line or not line.startswith("{"): continue try: d = json.loads(line) except Exception: bad_lines += 1 continue t = d.get("type") sid = d.get("sessionId") if not sid: continue ts = d.get("timestamp") or "" s = sessions.setdefault(sid, { "machine": a.machine, "project": project, "first_ts": ts, "last_ts": ts, "user_msgs": 0, "assistant_msgs": 0, "models": set(), "version": d.get("version"), "entrypoint": d.get("entrypoint"), "cwd": d.get("cwd"), "files": set(), }) s["files"].add(os.path.basename(fp)) if ts: if not s["first_ts"] or ts < s["first_ts"]: s["first_ts"] = ts if ts > s["last_ts"]: s["last_ts"] = ts if t == "user" and not d.get("isSidechain"): s["user_msgs"] += 1 if t != "assistant": continue msg = d.get("message") or {} u = msg.get("usage") if not isinstance(u, dict): continue total_assistant += 1 # cheie de deduplicare mid = msg.get("id"); rid = d.get("requestId") if mid or rid: key = f"{mid}|{rid}" else: key = "h:" + hashlib.sha1((sid + ts + str(u.get("output_tokens"))).encode()).hexdigest() if key in seen: dup += 1 continue seen.add(key) model = msg.get("model") or "unknown" if model.startswith("<") or model == "unknown": continue # synthetic / mesaje locale fără cost s["assistant_msgs"] += 1 s["models"].add(model) date = ts[:10] if ts else "unknown" k = (sid, date, model) r = rows.setdefault(k, {"input": 0, "output": 0, "cache_creation": 0, "cache_read": 0, "thinking": 0, "turns": 0, "web_search": 0, "web_fetch": 0}) r["input"] += u.get("input_tokens") or 0 r["output"] += u.get("output_tokens") or 0 r["cache_creation"] += u.get("cache_creation_input_tokens") or 0 r["cache_read"] += u.get("cache_read_input_tokens") or 0 otd = u.get("output_tokens_details") or {} r["thinking"] += otd.get("thinking_tokens") or 0 stu = u.get("server_tool_use") or {} r["web_search"] += stu.get("web_search_requests") or 0 r["web_fetch"] += stu.get("web_fetch_requests") or 0 r["turns"] += 1 out = { "machine": a.machine, "root": a.root, "files_scanned": len(files), "assistant_records_with_usage": total_assistant, "duplicates_removed": dup, "malformed_lines": bad_lines, "rows": [{"session": k[0], "date": k[1], "model": k[2], **v} for k, v in rows.items()], "sessions": {sid: {**{kk: vv for kk, vv in m.items() if kk not in ("models", "files")}, "models": sorted(m["models"]), "files": len(m["files"])} for sid, m in sessions.items()}, } json.dump(out, sys.stdout) main()