tokenmaxxing 0.1.2__tar.gz → 0.1.3__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.2 → tokenmaxxing-0.1.3}/PKG-INFO +1 -1
- {tokenmaxxing-0.1.2 → tokenmaxxing-0.1.3}/pyproject.toml +1 -1
- {tokenmaxxing-0.1.2 → tokenmaxxing-0.1.3}/src/tokenmaxxing/__init__.py +1 -1
- {tokenmaxxing-0.1.2 → tokenmaxxing-0.1.3}/src/tokenmaxxing/app.py +54 -33
- {tokenmaxxing-0.1.2 → tokenmaxxing-0.1.3}/.gitignore +0 -0
- {tokenmaxxing-0.1.2 → tokenmaxxing-0.1.3}/LICENSE +0 -0
- {tokenmaxxing-0.1.2 → tokenmaxxing-0.1.3}/README.md +0 -0
- {tokenmaxxing-0.1.2 → tokenmaxxing-0.1.3}/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.3
|
|
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.3"
|
|
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"
|
|
@@ -604,42 +604,58 @@ class ClaudeMonitorApp(rumps.App):
|
|
|
604
604
|
# ----- worker / dispatch -----
|
|
605
605
|
|
|
606
606
|
def _worker_loop(self):
|
|
607
|
+
"""Background poll loop. Must NEVER die — any uncaught exception
|
|
608
|
+
leaves the UI frozen on the last state forever, with no further
|
|
609
|
+
polls to recover."""
|
|
607
610
|
consecutive_failures = 0
|
|
608
611
|
while True:
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
+
try:
|
|
613
|
+
now = time.time()
|
|
614
|
+
if now < self._backoff_until:
|
|
615
|
+
self._wake.wait(timeout=min(REFRESH_SECONDS, self._backoff_until - now))
|
|
616
|
+
self._wake.clear()
|
|
617
|
+
continue
|
|
618
|
+
|
|
619
|
+
oauth_data = _get_oauth_data()
|
|
620
|
+
payload, err, is_rate_limited = fetch_usage(oauth_data)
|
|
621
|
+
# Drop any Refresh-now clicks that arrived during the poll — the
|
|
622
|
+
# in-flight poll already satisfies them, and re-polling immediately
|
|
623
|
+
# can trip the OAuth endpoint's rate limit.
|
|
612
624
|
self._wake.clear()
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
#
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
625
|
+
with self._pending_lock:
|
|
626
|
+
self._pending = (payload, err, is_rate_limited, oauth_data)
|
|
627
|
+
|
|
628
|
+
if payload is not None:
|
|
629
|
+
consecutive_failures = 0
|
|
630
|
+
self._backoff_until = 0
|
|
631
|
+
self._record_snapshot(payload)
|
|
632
|
+
sleep_for = REFRESH_SECONDS
|
|
633
|
+
elif is_rate_limited:
|
|
634
|
+
consecutive_failures += 1
|
|
635
|
+
backoff = _calc_backoff(consecutive_failures)
|
|
636
|
+
self._backoff_until = time.time() + backoff
|
|
637
|
+
# Retry as soon as the backoff window allows, instead of waiting
|
|
638
|
+
# the full REFRESH_SECONDS — otherwise the menu keeps showing
|
|
639
|
+
# "[rate limited]" for ~5 min after a single transient 429 even
|
|
640
|
+
# when the API recovered within seconds.
|
|
641
|
+
sleep_for = backoff
|
|
642
|
+
else:
|
|
643
|
+
sleep_for = REFRESH_SECONDS
|
|
644
|
+
|
|
645
|
+
self._wake.wait(timeout=sleep_for)
|
|
646
|
+
self._wake.clear()
|
|
647
|
+
except Exception as e: # noqa: BLE001
|
|
648
|
+
# Surface the failure to the UI as a transient error and keep
|
|
649
|
+
# looping. Without this, a single bad poll kills polling forever.
|
|
650
|
+
print(f"tokenmaxxing worker: {type(e).__name__}: {e}", file=sys.stderr)
|
|
651
|
+
with self._pending_lock:
|
|
652
|
+
self._pending = (None, f"worker error: {type(e).__name__}", False, None)
|
|
653
|
+
# Avoid a tight loop if the exception happens immediately.
|
|
654
|
+
try:
|
|
655
|
+
self._wake.wait(timeout=REFRESH_SECONDS)
|
|
656
|
+
self._wake.clear()
|
|
657
|
+
except Exception: # noqa: BLE001
|
|
658
|
+
time.sleep(REFRESH_SECONDS)
|
|
643
659
|
|
|
644
660
|
def _apply_pending(self, _sender):
|
|
645
661
|
with self._pending_lock:
|
|
@@ -665,6 +681,11 @@ class ClaudeMonitorApp(rumps.App):
|
|
|
665
681
|
self._render()
|
|
666
682
|
|
|
667
683
|
def _manual_refresh(self, _sender):
|
|
684
|
+
# Force-clear any pending backoff so the click actually polls.
|
|
685
|
+
# Without this, when the worker is in backoff (e.g. after sustained
|
|
686
|
+
# 429s), the wake event fires but the loop top-checks _backoff_until
|
|
687
|
+
# and goes right back to waiting — the click is swallowed.
|
|
688
|
+
self._backoff_until = 0
|
|
668
689
|
self._wake.set()
|
|
669
690
|
|
|
670
691
|
def _record_snapshot(self, payload):
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|