ccusage 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,18 @@
1
+ name: Publish to PyPI
2
+
3
+ on:
4
+ release:
5
+ types: [published]
6
+
7
+ permissions:
8
+ id-token: write
9
+
10
+ jobs:
11
+ publish:
12
+ runs-on: ubuntu-latest
13
+ environment: pypi
14
+ steps:
15
+ - uses: actions/checkout@v4
16
+ - uses: astral-sh/setup-uv@v4
17
+ - run: uv build
18
+ - uses: pypa/gh-action-pypi-publish@release/v1
@@ -0,0 +1,10 @@
1
+ # Python-generated files
2
+ __pycache__/
3
+ *.py[oc]
4
+ build/
5
+ dist/
6
+ wheels/
7
+ *.egg-info
8
+
9
+ # Virtual environments
10
+ .venv
@@ -0,0 +1 @@
1
+ 3.13
ccusage-0.1.0/PKG-INFO ADDED
@@ -0,0 +1,171 @@
1
+ Metadata-Version: 2.4
2
+ Name: ccusage
3
+ Version: 0.1.0
4
+ Summary: Claude Code usage monitor — fetches rate limits from Anthropic's API
5
+ Requires-Python: >=3.12
6
+ Description-Content-Type: text/markdown
7
+
8
+ # ccusage
9
+
10
+ Claude Code usage monitor. Fetches your real rate limit data from Anthropic's API and displays it in the Claude Code statusline.
11
+
12
+ ## Example output
13
+
14
+ `ccusage` command:
15
+
16
+ ```
17
+ Plan: max_5x
18
+ Session (5h) 39% resets 1h26m
19
+ Week (all) 15% resets 143h26m
20
+ Week (Sonnet) 39% resets 65h26m
21
+ Extra usage $0.00 / $1000.00
22
+ ```
23
+
24
+ Claude Code statusline (self-caching — refreshes from API when stale, no daemon needed):
25
+
26
+ ```
27
+ ~/projects/myapp [Opus 4.6] 5h:39% 7d:15% son:39% | $1.37 | max_5x | reset:1h26m
28
+ ```
29
+
30
+ ## Install
31
+
32
+ ```bash
33
+ uv tool install git+https://github.com/wakamex/ccusage
34
+ ```
35
+
36
+ Then run:
37
+
38
+ ```bash
39
+ # Check usage once
40
+ ccusage
41
+
42
+ # Run the daemon (keeps usage-limits.json updated)
43
+ ccusage daemon
44
+ ```
45
+
46
+ Configure the statusline in `~/.claude/settings.json`:
47
+
48
+ ```json
49
+ {
50
+ "statusLine": {
51
+ "type": "command",
52
+ "command": "ccusage statusline"
53
+ }
54
+ }
55
+ ```
56
+
57
+ ## Commands
58
+
59
+ | Command | Description |
60
+ |---------|-------------|
61
+ | `ccusage` | Show current usage (colored terminal output) |
62
+ | `ccusage json` | Print raw JSON |
63
+ | `ccusage daemon [-i SECS]` | Run in foreground, refresh every 5 min (customizable) |
64
+ | `ccusage statusline` | Claude Code statusline (self-caching, no daemon needed) |
65
+ | `ccusage install` | Print setup instructions |
66
+
67
+ ## How Claude Code rate limiting works
68
+
69
+ Discovered by inspecting Claude Code's bundled `cli.js` (v2.1.32).
70
+
71
+ ### Data sources
72
+
73
+ Claude Code gets rate limit data from two places:
74
+
75
+ 1. **`/api/oauth/usage` endpoint** — Called by the `/status` slash command. Returns utilization percentages and reset times for each rate limit bucket. Requires the `anthropic-beta: oauth-2025-04-20` header.
76
+
77
+ 2. **Response headers on every API call** — Every message response includes headers like:
78
+ - `anthropic-ratelimit-unified-{claim}-utilization` (0-100 float)
79
+ - `anthropic-ratelimit-unified-{claim}-reset` (unix timestamp)
80
+ - `anthropic-ratelimit-unified-status` (`allowed` / `allowed_warning` / `rejected`)
81
+ - `anthropic-ratelimit-unified-fallback` (`available` when fallback models are available)
82
+ - `anthropic-ratelimit-unified-overage-status` / `overage-reset`
83
+ - `anthropic-ratelimit-unified-representative-claim`
84
+
85
+ ### Rate limit types
86
+
87
+ | Type | Key in API response | Description |
88
+ |------|-------------------|-------------|
89
+ | `five_hour` | `five_hour` | Rolling 5-hour session window |
90
+ | `seven_day` | `seven_day` | Rolling 7-day all-models window |
91
+ | `seven_day_sonnet` | `seven_day_sonnet` | Rolling 7-day Sonnet-specific window |
92
+ | `seven_day_opus` | `seven_day_opus` | Rolling 7-day Opus-specific window |
93
+ | `overage` | `extra_usage` | Extra/overage usage (if enabled) |
94
+
95
+ ### API response format
96
+
97
+ ```
98
+ GET https://api.anthropic.com/api/oauth/usage
99
+ Authorization: Bearer <oauth_access_token>
100
+ anthropic-beta: oauth-2025-04-20
101
+ ```
102
+
103
+ ```json
104
+ {
105
+ "five_hour": {
106
+ "utilization": 35.0,
107
+ "resets_at": "2026-02-06T22:00:00+00:00"
108
+ },
109
+ "seven_day": {
110
+ "utilization": 14.0,
111
+ "resets_at": "2026-02-12T20:00:00+00:00"
112
+ },
113
+ "seven_day_sonnet": {
114
+ "utilization": 39.0,
115
+ "resets_at": "2026-02-09T14:00:00+00:00"
116
+ },
117
+ "seven_day_opus": null,
118
+ "seven_day_oauth_apps": null,
119
+ "seven_day_cowork": null,
120
+ "iguana_necktie": null,
121
+ "extra_usage": {
122
+ "is_enabled": true,
123
+ "monthly_limit": 100000,
124
+ "used_credits": 0.0,
125
+ "utilization": null
126
+ }
127
+ }
128
+ ```
129
+
130
+ ### Authentication
131
+
132
+ The OAuth token lives at `~/.claude/.credentials.json`:
133
+
134
+ ```json
135
+ {
136
+ "claudeAiOauth": {
137
+ "accessToken": "sk-ant-oat01-...",
138
+ "refreshToken": "sk-ant-ort01-...",
139
+ "expiresAt": 1770412938485,
140
+ "subscriptionType": "team",
141
+ "rateLimitTier": "default_claude_max_5x"
142
+ }
143
+ }
144
+ ```
145
+
146
+ The access token expires roughly hourly. Claude Code refreshes it automatically — as long as you have an active Claude Code session, the token stays valid for ccusage to read.
147
+
148
+ ### Warning thresholds (from cli.js)
149
+
150
+ Claude Code shows inline warnings based on these thresholds:
151
+
152
+ ```
153
+ five_hour: 90% utilization when 72% of window has passed
154
+ seven_day: 75% at 60%, 50% at 35%, 25% at 15%
155
+ ```
156
+
157
+ ### Other endpoints found in cli.js
158
+
159
+ - `/api/oauth/profile` — User profile
160
+ - `/api/oauth/account/settings` — Account settings
161
+ - `/api/claude_code/policy_limits` — Org policy limits (needs `organizationUuid`)
162
+ - `/api/organization/claude_code_first_token_date` — Org onboarding date
163
+
164
+ ### Local files Claude Code uses
165
+
166
+ | File | Written by | Contains |
167
+ |------|-----------|----------|
168
+ | `~/.claude/.credentials.json` | Claude Code | OAuth tokens, plan tier |
169
+ | `~/.claude/stats-cache.json` | Claude Code | Local usage stats (message counts, token counts per model) |
170
+ | `~/.claude/usage-limits.json` | ccusage daemon | Cached API usage data (this tool) |
171
+ | `~/.claude/statsig/` | Claude Code | Feature flags, experiment assignments |
@@ -0,0 +1,164 @@
1
+ # ccusage
2
+
3
+ Claude Code usage monitor. Fetches your real rate limit data from Anthropic's API and displays it in the Claude Code statusline.
4
+
5
+ ## Example output
6
+
7
+ `ccusage` command:
8
+
9
+ ```
10
+ Plan: max_5x
11
+ Session (5h) 39% resets 1h26m
12
+ Week (all) 15% resets 143h26m
13
+ Week (Sonnet) 39% resets 65h26m
14
+ Extra usage $0.00 / $1000.00
15
+ ```
16
+
17
+ Claude Code statusline (self-caching — refreshes from API when stale, no daemon needed):
18
+
19
+ ```
20
+ ~/projects/myapp [Opus 4.6] 5h:39% 7d:15% son:39% | $1.37 | max_5x | reset:1h26m
21
+ ```
22
+
23
+ ## Install
24
+
25
+ ```bash
26
+ uv tool install git+https://github.com/wakamex/ccusage
27
+ ```
28
+
29
+ Then run:
30
+
31
+ ```bash
32
+ # Check usage once
33
+ ccusage
34
+
35
+ # Run the daemon (keeps usage-limits.json updated)
36
+ ccusage daemon
37
+ ```
38
+
39
+ Configure the statusline in `~/.claude/settings.json`:
40
+
41
+ ```json
42
+ {
43
+ "statusLine": {
44
+ "type": "command",
45
+ "command": "ccusage statusline"
46
+ }
47
+ }
48
+ ```
49
+
50
+ ## Commands
51
+
52
+ | Command | Description |
53
+ |---------|-------------|
54
+ | `ccusage` | Show current usage (colored terminal output) |
55
+ | `ccusage json` | Print raw JSON |
56
+ | `ccusage daemon [-i SECS]` | Run in foreground, refresh every 5 min (customizable) |
57
+ | `ccusage statusline` | Claude Code statusline (self-caching, no daemon needed) |
58
+ | `ccusage install` | Print setup instructions |
59
+
60
+ ## How Claude Code rate limiting works
61
+
62
+ Discovered by inspecting Claude Code's bundled `cli.js` (v2.1.32).
63
+
64
+ ### Data sources
65
+
66
+ Claude Code gets rate limit data from two places:
67
+
68
+ 1. **`/api/oauth/usage` endpoint** — Called by the `/status` slash command. Returns utilization percentages and reset times for each rate limit bucket. Requires the `anthropic-beta: oauth-2025-04-20` header.
69
+
70
+ 2. **Response headers on every API call** — Every message response includes headers like:
71
+ - `anthropic-ratelimit-unified-{claim}-utilization` (0-100 float)
72
+ - `anthropic-ratelimit-unified-{claim}-reset` (unix timestamp)
73
+ - `anthropic-ratelimit-unified-status` (`allowed` / `allowed_warning` / `rejected`)
74
+ - `anthropic-ratelimit-unified-fallback` (`available` when fallback models are available)
75
+ - `anthropic-ratelimit-unified-overage-status` / `overage-reset`
76
+ - `anthropic-ratelimit-unified-representative-claim`
77
+
78
+ ### Rate limit types
79
+
80
+ | Type | Key in API response | Description |
81
+ |------|-------------------|-------------|
82
+ | `five_hour` | `five_hour` | Rolling 5-hour session window |
83
+ | `seven_day` | `seven_day` | Rolling 7-day all-models window |
84
+ | `seven_day_sonnet` | `seven_day_sonnet` | Rolling 7-day Sonnet-specific window |
85
+ | `seven_day_opus` | `seven_day_opus` | Rolling 7-day Opus-specific window |
86
+ | `overage` | `extra_usage` | Extra/overage usage (if enabled) |
87
+
88
+ ### API response format
89
+
90
+ ```
91
+ GET https://api.anthropic.com/api/oauth/usage
92
+ Authorization: Bearer <oauth_access_token>
93
+ anthropic-beta: oauth-2025-04-20
94
+ ```
95
+
96
+ ```json
97
+ {
98
+ "five_hour": {
99
+ "utilization": 35.0,
100
+ "resets_at": "2026-02-06T22:00:00+00:00"
101
+ },
102
+ "seven_day": {
103
+ "utilization": 14.0,
104
+ "resets_at": "2026-02-12T20:00:00+00:00"
105
+ },
106
+ "seven_day_sonnet": {
107
+ "utilization": 39.0,
108
+ "resets_at": "2026-02-09T14:00:00+00:00"
109
+ },
110
+ "seven_day_opus": null,
111
+ "seven_day_oauth_apps": null,
112
+ "seven_day_cowork": null,
113
+ "iguana_necktie": null,
114
+ "extra_usage": {
115
+ "is_enabled": true,
116
+ "monthly_limit": 100000,
117
+ "used_credits": 0.0,
118
+ "utilization": null
119
+ }
120
+ }
121
+ ```
122
+
123
+ ### Authentication
124
+
125
+ The OAuth token lives at `~/.claude/.credentials.json`:
126
+
127
+ ```json
128
+ {
129
+ "claudeAiOauth": {
130
+ "accessToken": "sk-ant-oat01-...",
131
+ "refreshToken": "sk-ant-ort01-...",
132
+ "expiresAt": 1770412938485,
133
+ "subscriptionType": "team",
134
+ "rateLimitTier": "default_claude_max_5x"
135
+ }
136
+ }
137
+ ```
138
+
139
+ The access token expires roughly hourly. Claude Code refreshes it automatically — as long as you have an active Claude Code session, the token stays valid for ccusage to read.
140
+
141
+ ### Warning thresholds (from cli.js)
142
+
143
+ Claude Code shows inline warnings based on these thresholds:
144
+
145
+ ```
146
+ five_hour: 90% utilization when 72% of window has passed
147
+ seven_day: 75% at 60%, 50% at 35%, 25% at 15%
148
+ ```
149
+
150
+ ### Other endpoints found in cli.js
151
+
152
+ - `/api/oauth/profile` — User profile
153
+ - `/api/oauth/account/settings` — Account settings
154
+ - `/api/claude_code/policy_limits` — Org policy limits (needs `organizationUuid`)
155
+ - `/api/organization/claude_code_first_token_date` — Org onboarding date
156
+
157
+ ### Local files Claude Code uses
158
+
159
+ | File | Written by | Contains |
160
+ |------|-----------|----------|
161
+ | `~/.claude/.credentials.json` | Claude Code | OAuth tokens, plan tier |
162
+ | `~/.claude/stats-cache.json` | Claude Code | Local usage stats (message counts, token counts per model) |
163
+ | `~/.claude/usage-limits.json` | ccusage daemon | Cached API usage data (this tool) |
164
+ | `~/.claude/statsig/` | Claude Code | Feature flags, experiment assignments |
@@ -0,0 +1,17 @@
1
+ [project]
2
+ name = "ccusage"
3
+ version = "0.1.0"
4
+ description = "Claude Code usage monitor — fetches rate limits from Anthropic's API"
5
+ readme = "README.md"
6
+ requires-python = ">=3.12"
7
+ dependencies = []
8
+
9
+ [tool.uv]
10
+ package = true
11
+
12
+ [build-system]
13
+ requires = ["hatchling"]
14
+ build-backend = "hatchling.build"
15
+
16
+ [project.scripts]
17
+ ccusage = "ccusage:main"
@@ -0,0 +1,347 @@
1
+ #!/usr/bin/env python3
2
+ """ccusage - Claude Code usage monitor.
3
+
4
+ Fetches rate limit data from Anthropic's /api/oauth/usage endpoint
5
+ using your Claude Code OAuth token. Zero external dependencies.
6
+
7
+ Usage:
8
+ ccusage Show current usage (colored)
9
+ ccusage status Same as above
10
+ ccusage json Print raw JSON
11
+ ccusage daemon Run in foreground, refresh every 5 min, write to ~/.claude/usage-limits.json
12
+ ccusage statusline Claude Code statusline command (reads stdin + cache)
13
+ ccusage install Print setup instructions
14
+ """
15
+
16
+ import argparse
17
+ import json
18
+ import signal
19
+ import sys
20
+ import time
21
+ import urllib.request
22
+ from datetime import datetime, timezone
23
+ from pathlib import Path
24
+
25
+ CLAUDE_DIR = Path.home() / ".claude"
26
+ CREDENTIALS_FILE = CLAUDE_DIR / ".credentials.json"
27
+ USAGE_FILE = CLAUDE_DIR / "usage-limits.json"
28
+ DAEMON_INTERVAL = 300 # 5 minutes
29
+
30
+
31
+ def get_credentials() -> dict | None:
32
+ """Read OAuth credentials from Claude Code's credentials file."""
33
+ try:
34
+ return json.loads(CREDENTIALS_FILE.read_text())
35
+ except (FileNotFoundError, json.JSONDecodeError):
36
+ return None
37
+
38
+
39
+ def get_plan(creds: dict | None = None) -> str:
40
+ """Return plan info from credentials (rateLimitTier or subscriptionType)."""
41
+ if creds is None:
42
+ creds = get_credentials()
43
+ if not creds:
44
+ return "unknown"
45
+ oauth = creds.get("claudeAiOauth", {})
46
+ tier = oauth.get("rateLimitTier") or oauth.get("subscriptionType") or "unknown"
47
+ return tier.removeprefix("default_claude_")
48
+
49
+
50
+ def fetch_usage() -> dict:
51
+ """Fetch usage from Anthropic's /api/oauth/usage endpoint.
52
+
53
+ Requires a valid (non-expired) OAuth token from ~/.claude/.credentials.json.
54
+ The key header is `anthropic-beta: oauth-2025-04-20` — without it, the
55
+ endpoint returns an auth error.
56
+
57
+ Returns the raw API response, e.g.:
58
+ {
59
+ "five_hour": {"utilization": 35.0, "resets_at": "..."},
60
+ "seven_day": {"utilization": 14.0, "resets_at": "..."},
61
+ "seven_day_sonnet": {"utilization": 39.0, "resets_at": "..."},
62
+ "seven_day_opus": null,
63
+ "extra_usage": {"is_enabled": true, "monthly_limit": 100000, ...}
64
+ }
65
+ """
66
+ creds = get_credentials()
67
+ if not creds:
68
+ raise RuntimeError("No credentials at ~/.claude/.credentials.json — run `claude` first")
69
+
70
+ oauth = creds.get("claudeAiOauth", {})
71
+ token = oauth.get("accessToken")
72
+ if not token:
73
+ raise RuntimeError("No OAuth access token in credentials")
74
+
75
+ expires_at = oauth.get("expiresAt", 0)
76
+ if time.time() * 1000 > expires_at:
77
+ raise RuntimeError("OAuth token expired — open Claude Code to refresh it")
78
+
79
+ req = urllib.request.Request(
80
+ "https://api.anthropic.com/api/oauth/usage",
81
+ headers={
82
+ "Authorization": f"Bearer {token}",
83
+ "Content-Type": "application/json",
84
+ "User-Agent": "ccusage/1.0",
85
+ "anthropic-beta": "oauth-2025-04-20",
86
+ },
87
+ )
88
+ with urllib.request.urlopen(req, timeout=10) as resp:
89
+ return json.loads(resp.read())
90
+
91
+
92
+ def build_usage_json(api_data: dict, plan: str) -> dict:
93
+ """Transform API response into our cached format."""
94
+ result = {
95
+ "plan": plan,
96
+ "source": "api",
97
+ "updated_at": datetime.now(timezone.utc).isoformat(),
98
+ }
99
+ for key, api_key in [
100
+ ("5h", "five_hour"),
101
+ ("7d", "seven_day"),
102
+ ("7d_sonnet", "seven_day_sonnet"),
103
+ ("7d_opus", "seven_day_opus"),
104
+ ]:
105
+ bucket = api_data.get(api_key)
106
+ if bucket:
107
+ result[key] = {
108
+ "pct": bucket["utilization"],
109
+ "resets_at": bucket.get("resets_at"),
110
+ }
111
+ extra = api_data.get("extra_usage")
112
+ if extra:
113
+ result["extra_usage"] = extra
114
+ return result
115
+
116
+
117
+ def write_usage_file(data: dict):
118
+ """Write usage data to ~/.claude/usage-limits.json."""
119
+ USAGE_FILE.write_text(json.dumps(data, indent=2) + "\n")
120
+
121
+
122
+ # -- CLI commands --
123
+
124
+ def cmd_status(raw_json=False):
125
+ """Fetch and display current usage."""
126
+ api_data = fetch_usage()
127
+ plan = get_plan()
128
+ data = build_usage_json(api_data, plan)
129
+
130
+ if raw_json:
131
+ print(json.dumps(data, indent=2))
132
+ return
133
+
134
+ R = "\033[0;31m"
135
+ Y = "\033[0;33m"
136
+ G = "\033[0;32m"
137
+ D = "\033[0;90m"
138
+ RST = "\033[0m"
139
+
140
+ def color_pct(pct):
141
+ p = int(pct)
142
+ c = R if p >= 70 else Y if p >= 50 else G
143
+ return f"{c}{p}%{RST}"
144
+
145
+ def fmt_reset(iso):
146
+ if not iso:
147
+ return ""
148
+ try:
149
+ reset = datetime.fromisoformat(iso)
150
+ now = datetime.now(timezone.utc)
151
+ secs = int((reset - now).total_seconds())
152
+ if secs <= 0:
153
+ return ""
154
+ m = secs // 60
155
+ if m >= 60:
156
+ return f" resets {m // 60}h{m % 60}m"
157
+ return f" resets {m}m"
158
+ except Exception:
159
+ return ""
160
+
161
+ print(f"Plan: {plan}")
162
+ for label, key in [
163
+ ("Session (5h)", "5h"),
164
+ ("Week (all)", "7d"),
165
+ ("Week (Sonnet)", "7d_sonnet"),
166
+ ("Week (Opus)", "7d_opus"),
167
+ ]:
168
+ bucket = data.get(key)
169
+ if bucket:
170
+ pct = bucket["pct"]
171
+ reset = fmt_reset(bucket.get("resets_at"))
172
+ print(f" {label:20s} {color_pct(pct)}{D}{reset}{RST}")
173
+
174
+ extra = data.get("extra_usage")
175
+ if extra and extra.get("is_enabled"):
176
+ used = extra.get("used_credits", 0) / 100
177
+ limit = extra.get("monthly_limit", 0) / 100
178
+ print(f" {'Extra usage':20s} ${used:.2f} / ${limit:.2f}")
179
+
180
+
181
+ def cmd_daemon(interval: int = DAEMON_INTERVAL):
182
+ """Run in foreground, refresh every `interval` seconds."""
183
+ signal.signal(signal.SIGINT, lambda *_: sys.exit(0))
184
+ signal.signal(signal.SIGTERM, lambda *_: sys.exit(0))
185
+
186
+ print(f"ccusage daemon started (refreshing every {interval}s)")
187
+ print(f"Writing to {USAGE_FILE}")
188
+
189
+ while True:
190
+ try:
191
+ api_data = fetch_usage()
192
+ plan = get_plan()
193
+ data = build_usage_json(api_data, plan)
194
+ write_usage_file(data)
195
+ pcts = []
196
+ for key in ("5h", "7d", "7d_sonnet"):
197
+ b = data.get(key)
198
+ if b:
199
+ pcts.append(f"{key}:{int(b['pct'])}%")
200
+ print(f"[{datetime.now().strftime('%H:%M:%S')}] {' '.join(pcts)}")
201
+ except Exception as e:
202
+ print(f"[{datetime.now().strftime('%H:%M:%S')}] Error: {e}", file=sys.stderr)
203
+
204
+ time.sleep(interval)
205
+
206
+
207
+ def _get_cached_usage(max_age: int = DAEMON_INTERVAL) -> dict:
208
+ """Read cached usage, refreshing from API if stale or missing."""
209
+ try:
210
+ usage = json.loads(USAGE_FILE.read_text())
211
+ updated = datetime.fromisoformat(usage["updated_at"])
212
+ age = (datetime.now(timezone.utc) - updated).total_seconds()
213
+ if age < max_age:
214
+ return usage
215
+ except Exception:
216
+ pass
217
+ # Cache is stale or missing — try to refresh
218
+ try:
219
+ api_data = fetch_usage()
220
+ usage = build_usage_json(api_data, get_plan())
221
+ write_usage_file(usage)
222
+ return usage
223
+ except Exception:
224
+ # Return whatever we had, even if stale
225
+ try:
226
+ return json.loads(USAGE_FILE.read_text())
227
+ except Exception:
228
+ return {}
229
+
230
+
231
+ def cmd_statusline():
232
+ """Claude Code statusline command. Reads Claude's JSON from stdin + cached usage."""
233
+ R = "\033[0;31m"
234
+ Y = "\033[0;33m"
235
+ G = "\033[0;32m"
236
+ C = "\033[0;36m"
237
+ D = "\033[0;90m"
238
+ RST = "\033[0m"
239
+
240
+ def color_pct(pct: int) -> str:
241
+ c = R if pct >= 70 else Y if pct >= 50 else G
242
+ return f"{c}{pct}%{RST}"
243
+
244
+ def fmt_reset(iso: str | None) -> str:
245
+ if not iso:
246
+ return ""
247
+ try:
248
+ reset = datetime.fromisoformat(iso)
249
+ secs = int((reset - datetime.now(timezone.utc)).total_seconds())
250
+ if secs <= 0:
251
+ return ""
252
+ m = secs // 60
253
+ if m >= 60:
254
+ return f"{m // 60}h{m % 60}m"
255
+ return f"{m}m"
256
+ except Exception:
257
+ return ""
258
+
259
+ # Read Claude Code's JSON from stdin
260
+ try:
261
+ cc = json.loads(sys.stdin.read())
262
+ except Exception:
263
+ cc = {}
264
+
265
+ model = cc.get("model", {}).get("display_name", "?")
266
+ cost = cc.get("cost", {}).get("total_cost_usd", 0)
267
+ pwd = cc.get("workspace", {}).get("current_dir", "?")
268
+ home = str(Path.home())
269
+ if pwd.startswith(home):
270
+ pwd = "~" + pwd[len(home):]
271
+
272
+ cost_fmt = f"${cost:.2f}" if cost > 0 else "$0"
273
+
274
+ # Read cached usage, refresh if stale or missing
275
+ usage = _get_cached_usage()
276
+
277
+ plan = usage.get("plan", "?")
278
+ five_h = usage.get("5h", {})
279
+ seven_d = usage.get("7d", {})
280
+ sonnet = usage.get("7d_sonnet", {})
281
+
282
+ parts = [f"{D}{pwd}{RST}", f"[{C}{model}{RST}]"]
283
+
284
+ if five_h:
285
+ parts.append(f"5h:{color_pct(int(five_h.get('pct', 0)))}")
286
+ if seven_d:
287
+ parts.append(f"7d:{color_pct(int(seven_d.get('pct', 0)))}")
288
+ if sonnet:
289
+ parts.append(f"son:{color_pct(int(sonnet.get('pct', 0)))}")
290
+
291
+ parts.append(f"| {cost_fmt} | {D}{plan}{RST}")
292
+
293
+ reset = fmt_reset(five_h.get("resets_at"))
294
+ if reset:
295
+ parts.append(f"| {D}reset:{reset}{RST}")
296
+
297
+ print(" ".join(parts))
298
+
299
+
300
+ def cmd_install():
301
+ """Print setup instructions."""
302
+ print("""ccusage setup
303
+ =============
304
+
305
+ 1. Run the daemon (in a terminal, tmux, or systemd):
306
+ ccusage daemon
307
+
308
+ 2. Configure Claude Code statusline in ~/.claude/settings.json:
309
+ {
310
+ "statusLine": {
311
+ "type": "command",
312
+ "command": "ccusage statusline"
313
+ }
314
+ }
315
+
316
+ 3. The statusline reads ~/.claude/usage-limits.json (written by the daemon)
317
+ and shows: 5h session, 7d all-models, 7d Sonnet-specific limits.
318
+ """)
319
+
320
+
321
+ def main():
322
+ parser = argparse.ArgumentParser(description="Claude Code usage monitor")
323
+ sub = parser.add_subparsers(dest="command")
324
+ sub.add_parser("status", help="Show current usage (default)")
325
+ sub.add_parser("json", help="Print raw JSON")
326
+ daemon_parser = sub.add_parser("daemon", help="Run refresh daemon")
327
+ daemon_parser.add_argument("-i", "--interval", type=int, default=DAEMON_INTERVAL,
328
+ help=f"Refresh interval in seconds (default: {DAEMON_INTERVAL})")
329
+ sub.add_parser("statusline", help="Claude Code statusline (reads stdin + cache)")
330
+ sub.add_parser("install", help="Print setup instructions")
331
+ args = parser.parse_args()
332
+
333
+ cmd = args.command or "status"
334
+ if cmd == "status":
335
+ cmd_status()
336
+ elif cmd == "json":
337
+ cmd_status(raw_json=True)
338
+ elif cmd == "daemon":
339
+ cmd_daemon(interval=args.interval)
340
+ elif cmd == "statusline":
341
+ cmd_statusline()
342
+ elif cmd == "install":
343
+ cmd_install()
344
+
345
+
346
+ if __name__ == "__main__":
347
+ main()
ccusage-0.1.0/uv.lock ADDED
@@ -0,0 +1,8 @@
1
+ version = 1
2
+ revision = 2
3
+ requires-python = ">=3.12"
4
+
5
+ [[package]]
6
+ name = "ccusage"
7
+ version = "0.1.0"
8
+ source = { editable = "." }