Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
99 changes: 99 additions & 0 deletions skills/wgj/redline/SKILL.md
Original file line number Diff line number Diff line change
@@ -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"}
}
```
161 changes: 161 additions & 0 deletions skills/wgj/redline/scripts/claude-usage
Original file line number Diff line number Diff line change
@@ -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()
102 changes: 102 additions & 0 deletions skills/wgj/redline/scripts/openai-usage
Original file line number Diff line number Diff line change
@@ -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()