|
#!/usr/bin/env python3 |
|
|
|
# xbar plugin for Claude API usage monitoring |
|
# <xbar.title>Claude Usage</xbar.title> |
|
# <xbar.version>1.0</xbar.version> |
|
# <xbar.author>mohemohe</xbar.author> |
|
# <xbar.author.github>mohemohe</xbar.author.github> |
|
# <xbar.desc>Displays Claude API usage status with 5-hour and weekly limits</xbar.desc> |
|
# <xbar.dependencies>python3,flaresolverr</xbar.dependencies> |
|
|
|
import json |
|
import os |
|
import sys |
|
import traceback |
|
import urllib.error |
|
import urllib.request |
|
from datetime import datetime, timedelta, timezone |
|
|
|
# Configuration |
|
CLAUDE_ORGANIZATION = "CHANGEME" # Set your organization ID |
|
CLAUDE_INITIAL_SESSION_KEY = "sk-ant-sid01-CHANGEME" # Set your initial session key if no file exists |
|
CLAUDE_USAGE_API_URL = ( |
|
f"https://claude.ai/api/organizations/{CLAUDE_ORGANIZATION}/usage" |
|
) |
|
|
|
# FlareSolverr configuration |
|
FLARESOLVERR_HOST = "172.0.0.1" # FlareSolverr IP address |
|
FLARESOLVERR_PORT = "8191" # FlareSolverr port |
|
FLARESOLVERR_URL = f"http://{FLARESOLVERR_HOST}:{FLARESOLVERR_PORT}/v1" |
|
|
|
LOG_FILE = "/tmp/claude-usage.log" # Log file for debugging |
|
SESSION_KEY_FILE = os.path.expanduser("~/.claude/sessionKey") |
|
TZ = timezone(timedelta(hours=9)) |
|
|
|
|
|
def get_session_key(): |
|
"""Get session key from file or environment variable""" |
|
if os.path.exists(SESSION_KEY_FILE): |
|
try: |
|
with open(SESSION_KEY_FILE, "r") as f: |
|
return f.read().strip() |
|
except Exception: |
|
pass |
|
|
|
# Fallback to environment variable |
|
if CLAUDE_INITIAL_SESSION_KEY: |
|
return CLAUDE_INITIAL_SESSION_KEY |
|
|
|
return None |
|
|
|
|
|
def save_session_key(session_key): |
|
"""Save session key to file""" |
|
try: |
|
os.makedirs(os.path.dirname(SESSION_KEY_FILE), exist_ok=True) |
|
with open(SESSION_KEY_FILE, "w") as f: |
|
f.write(session_key) |
|
except Exception as e: |
|
print(f"Error saving session key: {e}", file=sys.stderr) |
|
|
|
|
|
def format_datetime(iso_string): |
|
"""Format ISO datetime string to readable format""" |
|
if not iso_string: |
|
return "N/A" |
|
|
|
try: |
|
dt = datetime.fromisoformat(iso_string) |
|
dt_tz = dt.astimezone(TZ) |
|
return dt_tz.strftime("%Y/%m/%d %H:%M") |
|
except Exception: |
|
return iso_string |
|
|
|
|
|
def make_api_request(): |
|
"""Make API request to Claude usage endpoint using FlareSolverr""" |
|
session_key = get_session_key() |
|
if not session_key: |
|
raise Exception("No session key available") |
|
|
|
# Create FlareSolverr request payload |
|
flaresolverr_payload = { |
|
"cmd": "request.get", |
|
"url": CLAUDE_USAGE_API_URL, |
|
"maxTimeout": 60000, |
|
"cookies": [ |
|
{"name": "sessionKey", "value": session_key, "domain": "claude.ai"} |
|
], |
|
"headers": { |
|
"accept": "*/*", |
|
"accept-encoding": "gzip, deflate, br, zstd", |
|
"accept-language": "ja,en-US;q=0.9,en;q=0.8", |
|
"anthropic-client-platform": "web_claude_ai", |
|
"anthropic-client-version": "1.0.0", |
|
"origin": "https://claude.ai", |
|
"referer": "https://claude.ai/settings/usage", |
|
"sec-fetch-dest": "empty", |
|
"sec-fetch-mode": "cors", |
|
"sec-fetch-site": "same-origin", |
|
"user-agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/142.0.0.0 Safari/537.36", |
|
}, |
|
} |
|
|
|
# Convert payload to JSON |
|
payload_data = json.dumps(flaresolverr_payload).encode("utf-8") |
|
|
|
# Create request to FlareSolverr |
|
headers = {"Content-Type": "application/json"} |
|
|
|
request = urllib.request.Request( |
|
FLARESOLVERR_URL, data=payload_data, headers=headers |
|
) |
|
|
|
response_data = None |
|
try: |
|
with urllib.request.urlopen(request) as response: |
|
response_data = response.read().decode() |
|
flaresolverr_response = json.loads(response_data) |
|
|
|
# Check if FlareSolverr request was successful |
|
if flaresolverr_response.get("status") != "ok": |
|
raise Exception( |
|
f"FlareSolverr error: {flaresolverr_response.get('message', 'Unknown error')}" |
|
) |
|
|
|
# Extract the actual response from FlareSolverr |
|
solution = flaresolverr_response.get("solution", {}) |
|
actual_response = solution.get("response", "") |
|
|
|
# Extract JSON from HTML response |
|
# The response is embedded in HTML as: <html>...<pre>{JSON}</pre>...</html> |
|
import re |
|
|
|
json_match = re.search(r"<pre>(\{.*?\})</pre>", actual_response, re.DOTALL) |
|
if json_match: |
|
json_text = json_match.group(1) |
|
# Parse the extracted JSON |
|
data = json.loads(json_text) |
|
else: |
|
# If no JSON found in HTML, try parsing the response directly |
|
data = json.loads(actual_response) |
|
|
|
# Check for new session key in response cookies |
|
cookies = solution.get("cookies", []) |
|
for cookie in cookies: |
|
if cookie.get("name") == "sessionKey": |
|
save_session_key(cookie.get("value")) |
|
break |
|
|
|
return data |
|
except urllib.error.HTTPError as e: |
|
raise Exception(f"HTTP Error {e.code}: {e.reason}, {response_data}") |
|
except json.JSONDecodeError as e: |
|
# Log the full FlareSolverr response for debugging |
|
try: |
|
with open(LOG_FILE, "w") as f: |
|
f.write(f"FlareSolverr Response:\n{response_data}\n\n") |
|
f.write(f"JSON Decode Error: {str(e)}\n") |
|
f.write(f"Traceback:\n{traceback.format_exc()}") |
|
except Exception as log_error: |
|
print(f"Failed to write log: {log_error}", file=sys.stderr) |
|
|
|
raise Exception(f"JSON decode error: {str(e)}, Response: {response_data}") |
|
except Exception as e: |
|
# Log the full FlareSolverr response for debugging |
|
try: |
|
with open(LOG_FILE, "w") as f: |
|
f.write(f"FlareSolverr Response:\n{response_data}\n\n") |
|
f.write(f"Error: {str(e)}\n") |
|
f.write(f"Traceback:\n{traceback.format_exc()}") |
|
except Exception as log_error: |
|
print(f"Failed to write log: {log_error}", file=sys.stderr) |
|
|
|
raise Exception(f"Request failed: {str(e)}") |
|
|
|
|
|
def display_usage(usage_data): |
|
"""Display usage data in xbar format""" |
|
try: |
|
# Get 5-hour utilization |
|
five_hour = usage_data.get("five_hour", {}) |
|
five_hour_util = five_hour.get("utilization", 0) |
|
|
|
# Display main status bar item |
|
percentage = int(five_hour_util) |
|
color = ( |
|
"color=#26FF4E" |
|
if percentage < 70 |
|
else "color=#FFC107" |
|
if percentage < 90 |
|
else "color=#FF5722" |
|
) |
|
print(f"✳️ {percentage}% | {color}") |
|
|
|
print("---") # Separator |
|
|
|
# 5-hour usage details |
|
five_hour_resets = format_datetime(five_hour.get("resets_at")) |
|
print(f"🕔 5-Hour Usage: {percentage}%") |
|
print(f"-- Resets at: {five_hour_resets}") |
|
|
|
# Weekly usage for all models |
|
seven_day = usage_data.get("seven_day", {}) |
|
if seven_day: |
|
weekly_util = int(seven_day.get("utilization", 0)) |
|
weekly_resets = format_datetime(seven_day.get("resets_at")) |
|
print(f"📅 Weekly All Models: {weekly_util}%") |
|
print(f"-- Resets at: {weekly_resets}") |
|
|
|
# Weekly Opus usage |
|
seven_day_opus = usage_data.get("seven_day_opus", {}) |
|
if seven_day_opus: |
|
opus_util = int(seven_day_opus.get("utilization", 0)) |
|
opus_resets = format_datetime(seven_day_opus.get("resets_at")) |
|
print(f"🎼 Weekly Opus: {opus_util}%") |
|
print(f"-- Resets at: {opus_resets}") |
|
|
|
# Other models if available |
|
seven_day_sonnet = usage_data.get("seven_day_sonnet") |
|
if seven_day_sonnet: |
|
sonnet_util = int(seven_day_sonnet.get("utilization", 0)) |
|
print(f"📝 Weekly Sonnet: {sonnet_util}%") |
|
|
|
# Extra usage if available |
|
extra_usage = usage_data.get("extra_usage") |
|
if extra_usage: |
|
extra_util = int(extra_usage.get("utilization", 0)) |
|
print(f"🔥 Extra Usage: {extra_util}%") |
|
|
|
print("---") |
|
print("🔄 Refresh | refresh=true") |
|
|
|
except Exception as e: |
|
print(f"❌ Error displaying data: {str(e)}", file=sys.stderr) |
|
|
|
|
|
def main(): |
|
"""Main function""" |
|
try: |
|
usage_data = make_api_request() |
|
display_usage(usage_data) |
|
except Exception as e: |
|
print(f"✳️ Claude: Error | color=red") |
|
print("---") |
|
print(f"❌ {str(e)}") |
|
print("🔄 Refresh | refresh=true") |
|
|
|
# Show session key status for debugging |
|
session_key = get_session_key() |
|
if not session_key: |
|
print("---") |
|
print("⚠️ No session key configured") |
|
print("-- Set CLAUDE_INITIAL_SESSION_KEY or create ~/.claude/sessionKey") |
|
|
|
|
|
if __name__ == "__main__": |
|
main() |