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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ccusage
3
- Version: 0.1.0
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 git+https://github.com/wakamex/ccusage
33
+ uv tool install ccusage
34
34
  ```
35
35
 
36
36
  Then run:
@@ -23,7 +23,7 @@ Claude Code statusline (self-caching — refreshes from API when stale, no daemo
23
23
  ## Install
24
24
 
25
25
  ```bash
26
- uv tool install git+https://github.com/wakamex/ccusage
26
+ uv tool install ccusage
27
27
  ```
28
28
 
29
29
  Then run:
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "ccusage"
3
- version = "0.1.0"
3
+ version = "0.1.4"
4
4
  description = "Claude Code usage monitor — fetches rate limits from Anthropic's API"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.12"
@@ -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 = CLAUDE_DIR / ".credentials.json"
27
- USAGE_FILE = CLAUDE_DIR / "usage-limits.json"
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": "ccusage/1.0",
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
- signal.signal(signal.SIGTERM, lambda *_: sys.exit(0))
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
@@ -1,8 +1,8 @@
1
1
  version = 1
2
- revision = 2
2
+ revision = 3
3
3
  requires-python = ">=3.12"
4
4
 
5
5
  [[package]]
6
6
  name = "ccusage"
7
- version = "0.1.0"
7
+ version = "0.1.1"
8
8
  source = { editable = "." }
File without changes
File without changes