tokenmaxxing 0.1.3__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.
- {tokenmaxxing-0.1.3 → tokenmaxxing-0.1.4}/PKG-INFO +1 -1
- {tokenmaxxing-0.1.3 → tokenmaxxing-0.1.4}/pyproject.toml +1 -1
- {tokenmaxxing-0.1.3 → tokenmaxxing-0.1.4}/src/tokenmaxxing/__init__.py +1 -1
- {tokenmaxxing-0.1.3 → tokenmaxxing-0.1.4}/src/tokenmaxxing/app.py +49 -3
- {tokenmaxxing-0.1.3 → tokenmaxxing-0.1.4}/.gitignore +0 -0
- {tokenmaxxing-0.1.3 → tokenmaxxing-0.1.4}/LICENSE +0 -0
- {tokenmaxxing-0.1.3 → tokenmaxxing-0.1.4}/README.md +0 -0
- {tokenmaxxing-0.1.3 → tokenmaxxing-0.1.4}/src/tokenmaxxing/__main__.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: tokenmaxxing
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.4
|
|
4
4
|
Summary: Menu bar app showing your live Claude Code session and weekly usage as a colored progress bar.
|
|
5
5
|
Project-URL: Homepage, https://github.com/alvations/tokenmaxxing
|
|
6
6
|
Project-URL: Repository, https://github.com/alvations/tokenmaxxing
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "tokenmaxxing"
|
|
7
|
-
version = "0.1.
|
|
7
|
+
version = "0.1.4"
|
|
8
8
|
description = "Menu bar app showing your live Claude Code session and weekly usage as a colored progress bar."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.9"
|
|
@@ -387,6 +387,46 @@ def _compute_min_max_avg(snapshots: list, key: str, now_ts: int):
|
|
|
387
387
|
return min(vals), sum(vals) / len(vals), max(vals)
|
|
388
388
|
|
|
389
389
|
|
|
390
|
+
def _lift_claude_env_token() -> Optional[str]:
|
|
391
|
+
"""Find a fresh CLAUDE_CODE_OAUTH_TOKEN from a running `claude` process.
|
|
392
|
+
|
|
393
|
+
Pro/Max users can't refresh the keychain token in-place (refreshToken is
|
|
394
|
+
empty). After expiry (~24 h), the keychain token returns 401 forever,
|
|
395
|
+
triggering sustained 429s on the OAuth usage endpoint — the menu bar
|
|
396
|
+
locks into "[rate limited]" with no recovery short of starting a new
|
|
397
|
+
`claude` session.
|
|
398
|
+
|
|
399
|
+
A running `claude` parent process has the fresh token in its env vars.
|
|
400
|
+
Lifting it lets tokenmaxxing keep polling indefinitely as long as any
|
|
401
|
+
claude session is alive.
|
|
402
|
+
"""
|
|
403
|
+
env_tok = os.environ.get("CLAUDE_CODE_OAUTH_TOKEN")
|
|
404
|
+
if env_tok:
|
|
405
|
+
return env_tok
|
|
406
|
+
try:
|
|
407
|
+
out = subprocess.check_output(
|
|
408
|
+
["pgrep", "-x", "claude"],
|
|
409
|
+
stderr=subprocess.DEVNULL,
|
|
410
|
+
timeout=KEYCHAIN_TIMEOUT,
|
|
411
|
+
).decode()
|
|
412
|
+
except (subprocess.CalledProcessError, subprocess.TimeoutExpired, OSError):
|
|
413
|
+
return None
|
|
414
|
+
pids = [p.strip() for p in out.splitlines() if p.strip()]
|
|
415
|
+
for pid in pids:
|
|
416
|
+
try:
|
|
417
|
+
env_out = subprocess.check_output(
|
|
418
|
+
["ps", "-E", "-p", pid],
|
|
419
|
+
stderr=subprocess.DEVNULL,
|
|
420
|
+
timeout=KEYCHAIN_TIMEOUT,
|
|
421
|
+
).decode()
|
|
422
|
+
except (subprocess.CalledProcessError, subprocess.TimeoutExpired, OSError):
|
|
423
|
+
continue
|
|
424
|
+
for tok in env_out.split():
|
|
425
|
+
if tok.startswith("CLAUDE_CODE_OAUTH_TOKEN="):
|
|
426
|
+
return tok.split("=", 1)[1]
|
|
427
|
+
return None
|
|
428
|
+
|
|
429
|
+
|
|
390
430
|
def _get_oauth_data() -> Optional[dict]:
|
|
391
431
|
"""Read Claude Code's OAuth credentials from keychain or file. Returns the full claudeAiOauth dict."""
|
|
392
432
|
user = os.environ.get("USER", "")
|
|
@@ -422,9 +462,15 @@ def get_access_token() -> Optional[str]:
|
|
|
422
462
|
|
|
423
463
|
def fetch_usage(oauth_data: Optional[dict] = None):
|
|
424
464
|
"""Returns (payload_dict, error_str, is_rate_limited). One of payload/error is always None."""
|
|
425
|
-
token
|
|
465
|
+
# Prefer a fresh token lifted from a running `claude` process — the
|
|
466
|
+
# keychain token can't refresh in-place for Pro/Max users and will 401
|
|
467
|
+
# once it expires. Fall back to the keychain only if no claude session
|
|
468
|
+
# is alive.
|
|
469
|
+
token = _lift_claude_env_token()
|
|
470
|
+
if not token and oauth_data:
|
|
471
|
+
token = oauth_data.get("accessToken")
|
|
426
472
|
if not token:
|
|
427
|
-
return None, "no Claude Code token", False
|
|
473
|
+
return None, "no Claude Code token (start a claude session)", False
|
|
428
474
|
req = urllib.request.Request(
|
|
429
475
|
USAGE_URL,
|
|
430
476
|
headers={
|
|
@@ -440,7 +486,7 @@ def fetch_usage(oauth_data: Optional[dict] = None):
|
|
|
440
486
|
if e.code == HTTP_STATUS_RATE_LIMITED:
|
|
441
487
|
return None, "rate limited — data is stale, retrying soon", True
|
|
442
488
|
if e.code == HTTP_STATUS_UNAUTHORIZED:
|
|
443
|
-
return None, "auth expired (
|
|
489
|
+
return None, "auth expired (start a claude session)", False
|
|
444
490
|
return None, f"HTTP {e.code}", False
|
|
445
491
|
except urllib.error.URLError as e:
|
|
446
492
|
return None, f"net: {e.reason}", False
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|