From ec2eaccf6edc065bc0aa7759ff48c41a992076e1 Mon Sep 17 00:00:00 2001 From: Weston Johnson Date: Wed, 18 Feb 2026 21:48:06 -0700 Subject: [PATCH 1/3] Add redline skill: live rate-limit awareness for Claude + OpenAI --- skills/wgj/redline/SKILL.md | 99 +++++++++++++++++++++++++++++++++++++ 1 file changed, 99 insertions(+) create mode 100644 skills/wgj/redline/SKILL.md diff --git a/skills/wgj/redline/SKILL.md b/skills/wgj/redline/SKILL.md new file mode 100644 index 0000000000..c443ee6b0f --- /dev/null +++ b/skills/wgj/redline/SKILL.md @@ -0,0 +1,99 @@ +--- +name: redline +description: Live rate-limit awareness for Claude.ai (Max/Pro) and OpenAI (Plus/Pro/Codex). Never hit the red line again — your agent checks remaining budget every heartbeat and automatically throttles from full-send to conservation mode. Two CLI scripts + a 4-tier pacing strategy (GREEN/YELLOW/ORANGE/RED) that keeps you running at maximum token efficiency 24/7. +metadata: + openclaw: + requires: + bins: [python3] +--- + +# Usage Pacing + +Check live rate-limit usage for **Claude.ai** and **OpenAI/Codex** plans, then apply pacing tiers to avoid hitting limits. + +## Scripts + +### `claude-usage` + +Reads the Claude Code OAuth token from macOS Keychain and calls the Anthropic usage API. + +```bash +# Human-readable output with color bars +scripts/claude-usage + +# JSON output (for programmatic use) +scripts/claude-usage --json +``` + +**Requirements:** +- macOS with `security` CLI (Keychain access) +- Claude Code OAuth token in Keychain (run `claude login` to set up) +- Token needs `user:profile` scope (standard Claude Code login provides this) + +**Token location:** macOS Keychain, service `Claude Code-credentials`, account = your macOS username. + +### `openai-usage` + +Reads the OpenAI OAuth token from OpenClaw's auth-profiles and calls the ChatGPT usage API. + +```bash +# Human-readable output with color bars +scripts/openai-usage + +# JSON output +scripts/openai-usage --json +``` + +**Requirements:** +- OpenClaw with an authenticated `openai-codex` profile (run `openclaw auth openai-codex`) +- Auth profiles at `~/.openclaw/agents/main/agent/auth-profiles.json` + +## Pacing Tiers + +Wire both scripts into your heartbeat to automatically pace work based on remaining budget: + +| Tier | Remaining | Behavior | +|------|-----------|----------| +| 🟢 GREEN | >50% | Normal operations | +| 🟡 YELLOW | 25-50% | Skip sub-agents, defer non-urgent research | +| 🟠 ORANGE | 10-25% | Essential replies only, no proactive checks | +| 🔴 RED | <10% | Critical only, warn user | + +### Heartbeat integration + +Add to your `HEARTBEAT.md`: + +```markdown +## Usage pacing (every heartbeat) +- Run `scripts/claude-usage --json` and `scripts/openai-usage --json` to check rate limits. +- Store readings in memory/heartbeat-state.json under "usage.claude" and "usage.openai". +- Apply pacing tiers: + - GREEN (>50% left): normal ops + - YELLOW (25-50%): skip sub-agents, defer non-urgent research + - ORANGE (10-25%): essential replies only, no proactive checks + - RED (<10%): critical only, warn user +- If entering YELLOW or worse, mention it briefly when next messaging. +``` + +### JSON output format + +**Claude (`--json`):** +```json +{ + "five_hour": {"utilization": 39.0, "resets_at": "2026-02-18T04:00:00Z"}, + "seven_day": {"utilization": 12.0, "resets_at": "2026-02-24T03:00:00Z"}, + "extra_usage": {"is_enabled": true, "used_credits": 5044, "monthly_limit": 5000} +} +``` + +**OpenAI (`--json`):** +```json +{ + "plan_type": "plus", + "rate_limit": { + "primary_window": {"used_percent": 0, "limit_window_seconds": 10800, "reset_at": 1771556400}, + "secondary_window": {"used_percent": 34, "limit_window_seconds": 86400, "reset_at": 1771556400} + }, + "credits": {"balance": "882.99"} +} +``` From 2f51e2eec4c60467c7d79c9e95c6799438b511e6 Mon Sep 17 00:00:00 2001 From: Weston Johnson Date: Wed, 18 Feb 2026 21:48:13 -0700 Subject: [PATCH 2/3] Add claude-usage script --- skills/wgj/redline/scripts/claude-usage | 161 ++++++++++++++++++++++++ 1 file changed, 161 insertions(+) create mode 100644 skills/wgj/redline/scripts/claude-usage diff --git a/skills/wgj/redline/scripts/claude-usage b/skills/wgj/redline/scripts/claude-usage new file mode 100644 index 0000000000..9e636a06b7 --- /dev/null +++ b/skills/wgj/redline/scripts/claude-usage @@ -0,0 +1,161 @@ +#!/usr/bin/env python3 +"""Check Claude.ai live usage limits (Max/Pro plan).""" + +import json, subprocess, sys, urllib.request, urllib.error +from datetime import datetime, timezone + +KEYCHAIN_SERVICE = "Claude Code-credentials" +KEYCHAIN_ACCOUNT = "wgj-ai" +TOKEN_URL = "https://platform.claude.com/v1/oauth/token" +CLIENT_ID = "9d1c250a-e61b-44d9-88ed-5944d1962f5e" + +def _read_keychain(): + raw = subprocess.check_output([ + "security", "find-generic-password", + "-s", KEYCHAIN_SERVICE, "-a", KEYCHAIN_ACCOUNT, "-w" + ], text=True).strip() + return json.loads(raw) + +def _write_keychain(data): + payload = json.dumps(data) + hex_payload = payload.encode("utf-8").hex() + # Delete then add (security doesn't have a reliable update) + subprocess.run([ + "security", "delete-generic-password", + "-s", KEYCHAIN_SERVICE, "-a", KEYCHAIN_ACCOUNT + ], capture_output=True) + subprocess.check_call([ + "security", "add-generic-password", + "-s", KEYCHAIN_SERVICE, "-a", KEYCHAIN_ACCOUNT, + "-X", hex_payload + ]) + +def _refresh_token(data): + """Refresh the OAuth token using the refresh_token grant.""" + oauth = data["claudeAiOauth"] + refresh_token = oauth.get("refreshToken") + if not refresh_token: + raise RuntimeError("No refresh token available") + + body = json.dumps({ + "grant_type": "refresh_token", + "refresh_token": refresh_token, + "client_id": CLIENT_ID, + }).encode() + + req = urllib.request.Request(TOKEN_URL, data=body, + headers={"Content-Type": "application/json", "User-Agent": "Claude-Code/1.0"}, + method="POST") + resp = json.loads(urllib.request.urlopen(req).read().decode()) + + # Update the stored credentials + oauth["accessToken"] = resp["access_token"] + if "refresh_token" in resp: + oauth["refreshToken"] = resp["refresh_token"] + # expires_in is seconds; store as ms timestamp + expires_in = resp.get("expires_in", 3600) + oauth["expiresAt"] = int(datetime.now(timezone.utc).timestamp() * 1000) + (expires_in * 1000) + + _write_keychain(data) + return oauth["accessToken"] + +def get_token(): + data = _read_keychain() + oauth = data["claudeAiOauth"] + tier = oauth.get("rateLimitTier", "unknown") + + # Check if token is expired or about to expire (within 5 min) + expires_at = oauth.get("expiresAt", 0) + now_ms = int(datetime.now(timezone.utc).timestamp() * 1000) + if now_ms >= (expires_at - 300000): # 5 min buffer + try: + new_token = _refresh_token(data) + return new_token, tier + except Exception as e: + if now_ms >= expires_at: + raise RuntimeError( + f"OAuth token expired and refresh failed: {e}. " + "Run any `claude` command to re-authenticate." + ) + # Token not yet expired, just close to it — use existing + pass + + return oauth["accessToken"], tier + +def fetch_usage(token): + req = urllib.request.Request("https://api.anthropic.com/api/oauth/usage", + headers={"Authorization": f"Bearer {token}", "anthropic-beta": "oauth-2025-04-20"}) + return json.loads(urllib.request.urlopen(req).read().decode()) + +def fmt_reset(iso_str): + if not iso_str: return "" + dt = datetime.fromisoformat(iso_str) + now = datetime.now(timezone.utc) + delta = dt - now + h, m = divmod(int(delta.total_seconds()) // 60, 60) + local = dt.astimezone().strftime("%I:%M %p %b %d") + return f"{local} ({h}h{m:02d}m)" + +def bar(pct, width=20): + used = pct / 100 + filled = int(used * width) + remaining = 100 - pct + if remaining > 60: color = "\033[32m" # green + elif remaining > 30: color = "\033[33m" # yellow + elif remaining > 10: color = "\033[38;5;208m" # orange + else: color = "\033[31m" # red + reset = "\033[0m" + return f"{color}{'█' * filled}{'░' * (width - filled)}{reset} {remaining:.0f}% left" + +def main(): + if "--refresh" in sys.argv: + try: + data = _read_keychain() + new_token = _refresh_token(data) + print(f"Token refreshed successfully. New token prefix: {new_token[:15]}...") + except Exception as e: + print(f"Refresh failed: {e}", file=sys.stderr) + sys.exit(1) + return + + json_mode = "--json" in sys.argv + try: + token, tier = get_token() + data = fetch_usage(token) + except Exception as e: + print(f"Error: {e}", file=sys.stderr) + sys.exit(1) + + if json_mode: + print(json.dumps(data, indent=2)) + return + + print(f"\n Claude Usage ({tier})") + print(f" {'─' * 40}") + + for key, label in [("five_hour", "5-Hour"), ("seven_day", "Weekly")]: + bucket = data.get(key) + if not bucket: continue + pct = bucket["utilization"] + reset = fmt_reset(bucket.get("resets_at")) + print(f" {label:>8} {bar(pct)} resets {reset}") + + for key, label in [("seven_day_opus", "Opus"), ("seven_day_sonnet", "Sonnet")]: + bucket = data.get(key) + if not bucket: continue + pct = bucket["utilization"] + if pct > 0: + reset = fmt_reset(bucket.get("resets_at")) + print(f" {label:>8} {bar(pct)} resets {reset}") + + extra = data.get("extra_usage") + if extra and extra.get("is_enabled"): + used = extra.get("used_credits", 0) + limit = extra.get("monthly_limit", 0) + pct = extra.get("utilization", 0) + print(f" {'Credits':>8} {bar(pct)} ${used:.0f}/${limit} used") + + print() + +if __name__ == "__main__": + main() From fde8f965f07a6b5873cdfe33fa503ba8b868195e Mon Sep 17 00:00:00 2001 From: Weston Johnson Date: Wed, 18 Feb 2026 21:48:14 -0700 Subject: [PATCH 3/3] Add openai-usage script --- skills/wgj/redline/scripts/openai-usage | 102 ++++++++++++++++++++++++ 1 file changed, 102 insertions(+) create mode 100644 skills/wgj/redline/scripts/openai-usage diff --git a/skills/wgj/redline/scripts/openai-usage b/skills/wgj/redline/scripts/openai-usage new file mode 100644 index 0000000000..ea7c29b1e5 --- /dev/null +++ b/skills/wgj/redline/scripts/openai-usage @@ -0,0 +1,102 @@ +#!/usr/bin/env python3 +"""Check OpenAI/Codex live usage (rate limits, plan type, credits). + +Reads OAuth token from OpenClaw's auth-profiles.json and calls +chatgpt.com/backend-api/wham/usage (same endpoint OpenClaw uses internally). +""" + +import json, sys, argparse, urllib.request, math, os +from datetime import datetime, timezone + +AUTH_PROFILES = os.path.expanduser( + "~/.openclaw/agents/main/agent/auth-profiles.json" +) + +BAR_WIDTH = 30 +COLORS = { + "green": "\033[92m", + "yellow": "\033[93m", + "red": "\033[91m", + "dim": "\033[90m", + "reset": "\033[0m", + "bold": "\033[1m", +} + + +def get_token(): + with open(AUTH_PROFILES) as f: + data = json.load(f) + prof = data.get("profiles", {}).get("openai-codex:default") + if not prof: + sys.exit("No openai-codex:default profile found in auth-profiles.json") + return prof["access"], prof.get("accountId") + + +def fetch_usage(token, account_id): + url = "https://chatgpt.com/backend-api/wham/usage" + req = urllib.request.Request(url, method="GET") + req.add_header("Authorization", f"Bearer {token}") + req.add_header("User-Agent", "CodexBar") + req.add_header("Accept", "application/json") + if account_id: + req.add_header("ChatGPT-Account-Id", account_id) + with urllib.request.urlopen(req, timeout=15) as resp: + return json.loads(resp.read()) + + +def bar(pct, width=BAR_WIDTH): + filled = round(pct / 100 * width) + empty = width - filled + if pct < 60: + color = COLORS["green"] + elif pct < 85: + color = COLORS["yellow"] + else: + color = COLORS["red"] + return f"{color}{'█' * filled}{COLORS['dim']}{'░' * empty}{COLORS['reset']}" + + +def format_reset(ts): + if not ts: + return "" + dt = datetime.fromtimestamp(ts, tz=timezone.utc).astimezone() + return dt.strftime("%-I:%M %p %Z") + + +def main(): + parser = argparse.ArgumentParser(description="Check OpenAI/Codex usage limits") + parser.add_argument("--json", action="store_true", help="JSON output") + args = parser.parse_args() + + token, account_id = get_token() + data = fetch_usage(token, account_id) + + if args.json: + print(json.dumps(data, indent=2)) + return + + plan = data.get("plan_type", "unknown") + print(f"{COLORS['bold']}OpenAI Usage{COLORS['reset']} plan: {plan}") + + rl = data.get("rate_limit", {}) + for key, label in [("primary_window", None), ("secondary_window", None)]: + w = rl.get(key) + if not w: + continue + secs = w.get("limit_window_seconds", 10800) + hours = round(secs / 3600) + wlabel = f"Day" if hours >= 24 else f"{hours}h" + pct = w.get("used_percent", 0) + remaining = 100 - pct + reset = format_reset(w.get("reset_at")) + reset_str = f" resets {reset}" if reset else "" + print(f" {wlabel:>4} {bar(pct)} {remaining:.0f}% left{reset_str}") + + credits = data.get("credits", {}) + if credits.get("balance") is not None: + bal = float(credits["balance"]) + print(f" Credits: ${bal:.2f}") + + +if __name__ == "__main__": + main()