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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: tokenmaxxing
3
- Version: 0.1.3
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.3"
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"
@@ -1,6 +1,6 @@
1
1
  """tokenmaxxing — menu bar app for Claude Code session and weekly usage."""
2
2
 
3
- __version__ = "0.1.3"
3
+ __version__ = "0.1.4"
4
4
 
5
5
  from tokenmaxxing.app import main
6
6
 
@@ -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 = oauth_data.get("accessToken") if oauth_data else None
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 (run `claude` to refresh)", False
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