ccusage 0.1.0__tar.gz → 0.1.4__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.
- {ccusage-0.1.0 → ccusage-0.1.4}/PKG-INFO +2 -2
- {ccusage-0.1.0 → ccusage-0.1.4}/README.md +1 -1
- {ccusage-0.1.0 → ccusage-0.1.4}/pyproject.toml +1 -1
- {ccusage-0.1.0 → ccusage-0.1.4}/src/ccusage/__init__.py +68 -16
- {ccusage-0.1.0 → ccusage-0.1.4}/uv.lock +2 -2
- {ccusage-0.1.0 → ccusage-0.1.4}/.github/workflows/publish.yml +0 -0
- {ccusage-0.1.0 → ccusage-0.1.4}/.gitignore +0 -0
- {ccusage-0.1.0 → ccusage-0.1.4}/.python-version +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: ccusage
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.4
|
|
4
4
|
Summary: Claude Code usage monitor — fetches rate limits from Anthropic's API
|
|
5
5
|
Requires-Python: >=3.12
|
|
6
6
|
Description-Content-Type: text/markdown
|
|
@@ -30,7 +30,7 @@ Claude Code statusline (self-caching — refreshes from API when stale, no daemo
|
|
|
30
30
|
## Install
|
|
31
31
|
|
|
32
32
|
```bash
|
|
33
|
-
uv tool install
|
|
33
|
+
uv tool install ccusage
|
|
34
34
|
```
|
|
35
35
|
|
|
36
36
|
Then run:
|
|
@@ -16,15 +16,58 @@ Usage:
|
|
|
16
16
|
import argparse
|
|
17
17
|
import json
|
|
18
18
|
import signal
|
|
19
|
+
import subprocess
|
|
19
20
|
import sys
|
|
20
21
|
import time
|
|
21
22
|
import urllib.request
|
|
22
23
|
from datetime import datetime, timezone
|
|
23
24
|
from pathlib import Path
|
|
24
25
|
|
|
26
|
+
_TTY = sys.stdout.isatty()
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _resolve_claude_path(relative: str) -> Path:
|
|
30
|
+
"""Return the first existing path for a file inside ~/.claude/.
|
|
31
|
+
|
|
32
|
+
On Windows, if the native path doesn't exist, also checks WSL distros
|
|
33
|
+
so the tool works from a Windows terminal against a WSL-based Claude Code
|
|
34
|
+
installation.
|
|
35
|
+
|
|
36
|
+
Args:
|
|
37
|
+
relative: path relative to the .claude directory, e.g. ".credentials.json"
|
|
38
|
+
"""
|
|
39
|
+
native = Path.home() / ".claude" / relative
|
|
40
|
+
if native.exists() or sys.platform != "win32":
|
|
41
|
+
return native
|
|
42
|
+
|
|
43
|
+
# Windows: try WSL paths
|
|
44
|
+
try:
|
|
45
|
+
out = subprocess.run(
|
|
46
|
+
["wsl", "-l", "-q"],
|
|
47
|
+
capture_output=True, timeout=5,
|
|
48
|
+
)
|
|
49
|
+
decoded = out.stdout.decode("utf-16-le", errors="ignore")
|
|
50
|
+
distros = [d.strip() for d in decoded.splitlines() if d.strip()]
|
|
51
|
+
except Exception:
|
|
52
|
+
return native
|
|
53
|
+
|
|
54
|
+
for distro in distros:
|
|
55
|
+
wsl_base = Path(f"//wsl$/{distro}/home")
|
|
56
|
+
try:
|
|
57
|
+
users = [p.name for p in wsl_base.iterdir() if p.is_dir()]
|
|
58
|
+
except OSError:
|
|
59
|
+
continue
|
|
60
|
+
for user in users:
|
|
61
|
+
candidate = wsl_base / user / ".claude" / relative
|
|
62
|
+
if candidate.exists():
|
|
63
|
+
return candidate
|
|
64
|
+
|
|
65
|
+
return native
|
|
66
|
+
|
|
67
|
+
|
|
25
68
|
CLAUDE_DIR = Path.home() / ".claude"
|
|
26
|
-
CREDENTIALS_FILE =
|
|
27
|
-
USAGE_FILE =
|
|
69
|
+
CREDENTIALS_FILE = _resolve_claude_path(".credentials.json")
|
|
70
|
+
USAGE_FILE = _resolve_claude_path("usage-limits.json")
|
|
28
71
|
DAEMON_INTERVAL = 300 # 5 minutes
|
|
29
72
|
|
|
30
73
|
|
|
@@ -81,7 +124,7 @@ def fetch_usage() -> dict:
|
|
|
81
124
|
headers={
|
|
82
125
|
"Authorization": f"Bearer {token}",
|
|
83
126
|
"Content-Type": "application/json",
|
|
84
|
-
"User-Agent": "
|
|
127
|
+
"User-Agent": "claude-code/2.1.71",
|
|
85
128
|
"anthropic-beta": "oauth-2025-04-20",
|
|
86
129
|
},
|
|
87
130
|
)
|
|
@@ -131,11 +174,11 @@ def cmd_status(raw_json=False):
|
|
|
131
174
|
print(json.dumps(data, indent=2))
|
|
132
175
|
return
|
|
133
176
|
|
|
134
|
-
R = "\033[0;31m"
|
|
135
|
-
Y = "\033[0;33m"
|
|
136
|
-
G = "\033[0;32m"
|
|
137
|
-
D = "\033[0;90m"
|
|
138
|
-
RST = "\033[0m"
|
|
177
|
+
R = "\033[0;31m" if _TTY else ""
|
|
178
|
+
Y = "\033[0;33m" if _TTY else ""
|
|
179
|
+
G = "\033[0;32m" if _TTY else ""
|
|
180
|
+
D = "\033[0;90m" if _TTY else ""
|
|
181
|
+
RST = "\033[0m" if _TTY else ""
|
|
139
182
|
|
|
140
183
|
def color_pct(pct):
|
|
141
184
|
p = int(pct)
|
|
@@ -181,27 +224,36 @@ def cmd_status(raw_json=False):
|
|
|
181
224
|
def cmd_daemon(interval: int = DAEMON_INTERVAL):
|
|
182
225
|
"""Run in foreground, refresh every `interval` seconds."""
|
|
183
226
|
signal.signal(signal.SIGINT, lambda *_: sys.exit(0))
|
|
184
|
-
|
|
227
|
+
if hasattr(signal, "SIGTERM"):
|
|
228
|
+
signal.signal(signal.SIGTERM, lambda *_: sys.exit(0))
|
|
185
229
|
|
|
186
230
|
print(f"ccusage daemon started (refreshing every {interval}s)")
|
|
187
231
|
print(f"Writing to {USAGE_FILE}")
|
|
188
232
|
|
|
233
|
+
backoff = 0
|
|
189
234
|
while True:
|
|
190
235
|
try:
|
|
191
236
|
api_data = fetch_usage()
|
|
192
237
|
plan = get_plan()
|
|
193
238
|
data = build_usage_json(api_data, plan)
|
|
194
239
|
write_usage_file(data)
|
|
240
|
+
backoff = 0
|
|
195
241
|
pcts = []
|
|
196
242
|
for key in ("5h", "7d", "7d_sonnet"):
|
|
197
243
|
b = data.get(key)
|
|
198
244
|
if b:
|
|
199
245
|
pcts.append(f"{key}:{int(b['pct'])}%")
|
|
200
246
|
print(f"[{datetime.now().strftime('%H:%M:%S')}] {' '.join(pcts)}")
|
|
247
|
+
except urllib.error.HTTPError as e:
|
|
248
|
+
if e.code == 429:
|
|
249
|
+
backoff = min((backoff or interval) * 2, 3600)
|
|
250
|
+
print(f"[{datetime.now().strftime('%H:%M:%S')}] 429 — backing off {backoff}s", file=sys.stderr)
|
|
251
|
+
else:
|
|
252
|
+
print(f"[{datetime.now().strftime('%H:%M:%S')}] Error: {e}", file=sys.stderr)
|
|
201
253
|
except Exception as e:
|
|
202
254
|
print(f"[{datetime.now().strftime('%H:%M:%S')}] Error: {e}", file=sys.stderr)
|
|
203
255
|
|
|
204
|
-
time.sleep(interval)
|
|
256
|
+
time.sleep(backoff or interval)
|
|
205
257
|
|
|
206
258
|
|
|
207
259
|
def _get_cached_usage(max_age: int = DAEMON_INTERVAL) -> dict:
|
|
@@ -230,12 +282,12 @@ def _get_cached_usage(max_age: int = DAEMON_INTERVAL) -> dict:
|
|
|
230
282
|
|
|
231
283
|
def cmd_statusline():
|
|
232
284
|
"""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"
|
|
285
|
+
R = "\033[0;31m" if _TTY else ""
|
|
286
|
+
Y = "\033[0;33m" if _TTY else ""
|
|
287
|
+
G = "\033[0;32m" if _TTY else ""
|
|
288
|
+
C = "\033[0;36m" if _TTY else ""
|
|
289
|
+
D = "\033[0;90m" if _TTY else ""
|
|
290
|
+
RST = "\033[0m" if _TTY else ""
|
|
239
291
|
|
|
240
292
|
def color_pct(pct: int) -> str:
|
|
241
293
|
c = R if pct >= 70 else Y if pct >= 50 else G
|
|
File without changes
|
|
File without changes
|
|
File without changes
|