#!/usr/bin/env python3 """ Context Usage Tracker - Tracks token consumption per request. Uses UserPromptSubmit as "pre-message" hook and Stop as "post-response" hook to calculate the delta in token usage for each request. This version uses character-based estimation (no dependencies). For better accuracy, see context-tracker-tiktoken.py. Usage: Configure both hooks to use the same script: - UserPromptSubmit: saves current token count - Stop: calculates delta and reports usage """ import json import os import sys import tempfile # Configuration CONTEXT_LIMIT = 128000 # Claude's context window (adjust for your model) def get_state_file(session_id: str) -> str: """Get temp file path for storing pre-message token count, isolated by session.""" return os.path.join(tempfile.gettempdir(), f"claude-context-{session_id}.json") def count_tokens_estimate(text: str) -> int: """ Estimate token count using character-based approximation. Uses ~4 characters per token ratio, which provides ~80-90% accuracy for English text. Less accurate for code and non-English text. """ return len(text) // 4 def read_transcript(transcript_path: str) -> str: """Read and concatenate all content from transcript file.""" if not transcript_path or not os.path.exists(transcript_path): return "" content = [] with open(transcript_path, "r") as f: for line in f: try: entry = json.loads(line.strip()) # Extract text content from various message formats if "message" in entry: msg = entry["message"] if isinstance(msg.get("content"), str): content.append(msg["content"]) elif isinstance(msg.get("content"), list): for block in msg["content"]: if isinstance(block, dict) and block.get("type") == "text": content.append(block.get("text", "")) except json.JSONDecodeError: continue return "\n".join(content) def handle_user_prompt_submit(data: dict) -> None: """Pre-message hook: Save current token count before request.""" session_id = data.get("session_id", "unknown") transcript_path = data.get("transcript_path", "") transcript_content = read_transcript(transcript_path) current_tokens = count_tokens_estimate(transcript_content) # Save to temp file for later comparison state_file = get_state_file(session_id) with open(state_file, "w") as f: json.dump({"pre_tokens": current_tokens}, f) def handle_stop(data: dict) -> None: """Post-response hook: Calculate and report token delta.""" session_id = data.get("session_id", "unknown") transcript_path = data.get("transcript_path", "") transcript_content = read_transcript(transcript_path) current_tokens = count_tokens_estimate(transcript_content) # Load pre-message count state_file = get_state_file(session_id) pre_tokens = 0 if os.path.exists(state_file): try: with open(state_file, "r") as f: state = json.load(f) pre_tokens = state.get("pre_tokens", 0) except (json.JSONDecodeError, IOError): pass # Calculate delta delta_tokens = current_tokens - pre_tokens remaining = CONTEXT_LIMIT - current_tokens percentage = (current_tokens / CONTEXT_LIMIT) * 100 # Report usage (stderr so it doesn't interfere with hook output) print( f"Context (estimated): ~{current_tokens:,} tokens " f"({percentage:.1f}% used, ~{remaining:,} remaining)", file=sys.stderr, ) if delta_tokens > 0: print(f"This request: ~{delta_tokens:,} tokens", file=sys.stderr) def main(): data = json.load(sys.stdin) event = data.get("hook_event_name", "") if event == "UserPromptSubmit": handle_user_prompt_submit(data) elif event == "Stop": handle_stop(data) sys.exit(0) if __name__ == "__main__": main()