|
#!/usr/bin/env python3 |
|
"""Claude Code status line: live context / token-usage meter. |
|
|
|
Claude Code pipes session JSON on stdin. As of CC v2.1.132 the payload carries |
|
a native `context_window` object with the real window size and current usage β |
|
including the correct denominator for extended-context (1M) models. We use it |
|
directly so the meter auto-detects the window instead of guessing per model. |
|
|
|
For older Claude Code (no `context_window`) we fall back to the transcript: |
|
`transcript_path` is a JSONL log where each assistant turn records |
|
`message.usage`, and context occupancy = input + cache_read + cache_creation. |
|
|
|
The same stdin payload also carries a `rate_limits` object for Claude.ai |
|
Pro/Max accounts (present after the first API response of a session): |
|
`five_hour` is the rolling session limit and `seven_day` is the weekly account |
|
limit, each with a `used_percentage`. We render those as their own meters, so |
|
the line shows both how full the window is and how much of the plan is spent β |
|
all from stdin, so still zero API cost. |
|
""" |
|
import sys, json, os |
|
|
|
# Fallback denominator only β used when the native context_window object is |
|
# absent (pre-2.1.132 Claude Code). Current Claude Code reports the true |
|
# window (200k, or 1M for extended-context models) on stdin, so no table is |
|
# consulted in normal operation. |
|
DEFAULT_LIMIT = 200_000 |
|
|
|
def c(code, s): return f"\033[{code}m{s}\033[0m" |
|
DIM, CYAN, GREEN, YELLOW, RED, BLUE, MAGENTA = "2", "36", "32", "33", "31", "34", "35" |
|
|
|
def human(n): |
|
if n >= 1_000_000: |
|
return f"{n/1_000_000:.1f}M" |
|
if n >= 1000: |
|
return f"{n/1000:.1f}k" |
|
return str(n) |
|
|
|
def bar(pct, width=10): |
|
"""A width-cell block meter for a 0-100 percentage.""" |
|
filled = min(width, int(pct / 100 * width)) |
|
return "β" * filled + "β" * (width - filled) |
|
|
|
def limit_col(pct, accent): |
|
"""Rate-limit colour: each meter keeps its own accent until it fills, then |
|
escalates to yellow/red so a nearly-spent limit stands out at a glance.""" |
|
if pct >= 80: |
|
return RED |
|
if pct >= 60: |
|
return YELLOW |
|
return accent |
|
|
|
def last_usage(path): |
|
"""Return the most recent usage dict from the transcript, or None.""" |
|
try: |
|
with open(path, "r") as f: |
|
lines = f.readlines() |
|
except Exception: |
|
return None |
|
for line in reversed(lines): |
|
line = line.strip() |
|
if not line or '"usage"' not in line: |
|
continue |
|
try: |
|
obj = json.loads(line) |
|
except Exception: |
|
continue |
|
msg = obj.get("message") or {} |
|
usage = msg.get("usage") or obj.get("usage") |
|
if isinstance(usage, dict) and usage.get("input_tokens") is not None: |
|
return usage |
|
return None |
|
|
|
def main(): |
|
try: |
|
d = json.load(sys.stdin) |
|
except Exception: |
|
print("π§ ctx ?") |
|
return |
|
|
|
parts = [] |
|
model_obj = d.get("model") or {} |
|
model = model_obj.get("display_name") or "Claude" |
|
parts.append(c(CYAN, f"βΊ {model}")) |
|
|
|
cur = (d.get("workspace") or {}).get("current_dir") or d.get("cwd") or "" |
|
if cur: |
|
parts.append(c(DIM, f"π {os.path.basename(cur.rstrip('/')) or cur}")) |
|
|
|
# Prefer Claude Code's native context_window object (v2.1.132+): it carries |
|
# the true window size β auto-detecting 1M extended-context models β and the |
|
# current occupancy, so we never divide by a guessed denominator. |
|
cw = d.get("context_window") or {} |
|
ctx = cw.get("total_input_tokens") |
|
limit = cw.get("context_window_size") |
|
out = cw.get("total_output_tokens") |
|
|
|
# Fallback for older Claude Code with no context_window object. |
|
if not limit: |
|
limit = DEFAULT_LIMIT |
|
if ctx is None: |
|
usage = last_usage(d.get("transcript_path", "")) |
|
if usage: |
|
ctx = (usage.get("input_tokens", 0) |
|
+ usage.get("cache_read_input_tokens", 0) |
|
+ usage.get("cache_creation_input_tokens", 0)) |
|
out = usage.get("output_tokens") |
|
|
|
if ctx: |
|
pct = cw.get("used_percentage") |
|
if pct is None: |
|
pct = ctx / limit * 100 |
|
col = GREEN if pct < 50 else (YELLOW if pct < 80 else RED) |
|
parts.append(c(col, f"π§ {human(ctx)}/{human(limit)} {pct:.0f}% {bar(pct)}")) |
|
if out: |
|
parts.append(c(DIM, f"β{human(out)} out")) |
|
else: |
|
# nothing to report yet (before first API call, or just after /compact) |
|
flag = d.get("exceeds_200k_tokens") |
|
parts.append(c(RED if flag else DIM, "π§ 200k+" if flag else "π§ β¦")) |
|
|
|
# Account rate limits (Claude.ai Pro/Max only; present once the session has |
|
# made an API call). five_hour is the rolling session limit, seven_day the |
|
# weekly account limit. Each is its own accent-coloured meter, and any window |
|
# may be independently absent, so we skip whatever is missing. |
|
rl = d.get("rate_limits") or {} |
|
for key, icon, label, accent in (("five_hour", "β³", "5h", BLUE), |
|
("seven_day", "π", "wk", MAGENTA)): |
|
pct = (rl.get(key) or {}).get("used_percentage") |
|
if pct is None: |
|
continue |
|
parts.append(c(limit_col(pct, accent), f"{icon} {label} {pct:.0f}% {bar(pct)}")) |
|
|
|
print(c(DIM, " Β· ").join(parts)) |
|
|
|
if __name__ == "__main__": |
|
main() |