ccburn 0.1.7__py3-none-any.whl → 0.2.1__py3-none-any.whl
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.
- ccburn/data/credentials.py +66 -15
- ccburn/display/chart.py +21 -12
- ccburn/display/gauges.py +4 -10
- ccburn/utils/formatting.py +34 -12
- {ccburn-0.1.7.dist-info → ccburn-0.2.1.dist-info}/METADATA +23 -5
- {ccburn-0.1.7.dist-info → ccburn-0.2.1.dist-info}/RECORD +10 -10
- {ccburn-0.1.7.dist-info → ccburn-0.2.1.dist-info}/WHEEL +0 -0
- {ccburn-0.1.7.dist-info → ccburn-0.2.1.dist-info}/entry_points.txt +0 -0
- {ccburn-0.1.7.dist-info → ccburn-0.2.1.dist-info}/licenses/LICENSE +0 -0
- {ccburn-0.1.7.dist-info → ccburn-0.2.1.dist-info}/top_level.txt +0 -0
ccburn/data/credentials.py
CHANGED
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
import json
|
|
4
4
|
import os
|
|
5
|
+
import platform
|
|
6
|
+
import subprocess
|
|
5
7
|
from datetime import datetime, timezone
|
|
6
8
|
from pathlib import Path
|
|
7
9
|
|
|
@@ -44,32 +46,81 @@ def get_credentials_path() -> Path:
|
|
|
44
46
|
return Path.home() / ".claude" / ".credentials.json"
|
|
45
47
|
|
|
46
48
|
|
|
47
|
-
def
|
|
48
|
-
"""Read
|
|
49
|
+
def _read_credentials_from_keychain() -> dict | None:
|
|
50
|
+
"""Read credentials from macOS Keychain.
|
|
49
51
|
|
|
50
52
|
Returns:
|
|
51
|
-
Parsed credentials dictionary
|
|
53
|
+
Parsed credentials dictionary or None if not found/failed.
|
|
54
|
+
"""
|
|
55
|
+
try:
|
|
56
|
+
result = subprocess.run(
|
|
57
|
+
["security", "find-generic-password", "-s", "Claude Code-credentials", "-w"],
|
|
58
|
+
capture_output=True,
|
|
59
|
+
text=True,
|
|
60
|
+
check=True,
|
|
61
|
+
)
|
|
62
|
+
return json.loads(result.stdout.strip())
|
|
63
|
+
except (subprocess.CalledProcessError, json.JSONDecodeError, FileNotFoundError):
|
|
64
|
+
return None
|
|
52
65
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
66
|
+
|
|
67
|
+
def _read_credentials_from_file() -> dict | None:
|
|
68
|
+
"""Read credentials from file.
|
|
69
|
+
|
|
70
|
+
Returns:
|
|
71
|
+
Parsed credentials dictionary or None if not found/failed.
|
|
56
72
|
"""
|
|
57
73
|
creds_path = get_credentials_path()
|
|
58
74
|
|
|
59
75
|
if not creds_path.exists():
|
|
60
|
-
|
|
61
|
-
f"Credentials file not found at {creds_path}\n"
|
|
62
|
-
"Please ensure Claude Code is installed and you are logged in.\n"
|
|
63
|
-
"Run 'claude' to log in if needed."
|
|
64
|
-
)
|
|
76
|
+
return None
|
|
65
77
|
|
|
66
78
|
try:
|
|
67
79
|
with open(creds_path) as f:
|
|
68
80
|
return json.load(f)
|
|
69
|
-
except json.JSONDecodeError
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
81
|
+
except (json.JSONDecodeError, PermissionError):
|
|
82
|
+
return None
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def read_credentials() -> dict:
|
|
86
|
+
"""Read credentials from the appropriate storage.
|
|
87
|
+
|
|
88
|
+
On macOS, tries Keychain first, then falls back to file.
|
|
89
|
+
On Linux/Windows, reads from file.
|
|
90
|
+
|
|
91
|
+
Returns:
|
|
92
|
+
Parsed credentials dictionary
|
|
93
|
+
|
|
94
|
+
Raises:
|
|
95
|
+
CredentialsNotFoundError: If credentials not found
|
|
96
|
+
InvalidCredentialsError: If credentials are malformed
|
|
97
|
+
"""
|
|
98
|
+
credentials = None
|
|
99
|
+
|
|
100
|
+
# On macOS, try Keychain first
|
|
101
|
+
if platform.system() == "Darwin":
|
|
102
|
+
credentials = _read_credentials_from_keychain()
|
|
103
|
+
|
|
104
|
+
# Fall back to file (or use file on non-macOS)
|
|
105
|
+
if credentials is None:
|
|
106
|
+
credentials = _read_credentials_from_file()
|
|
107
|
+
|
|
108
|
+
if credentials is None:
|
|
109
|
+
creds_path = get_credentials_path()
|
|
110
|
+
if platform.system() == "Darwin":
|
|
111
|
+
raise CredentialsNotFoundError(
|
|
112
|
+
"Credentials not found in macOS Keychain or file.\n"
|
|
113
|
+
"Please ensure Claude Code is installed and you are logged in.\n"
|
|
114
|
+
"Run 'claude' to log in if needed."
|
|
115
|
+
)
|
|
116
|
+
else:
|
|
117
|
+
raise CredentialsNotFoundError(
|
|
118
|
+
f"Credentials file not found at {creds_path}\n"
|
|
119
|
+
"Please ensure Claude Code is installed and you are logged in.\n"
|
|
120
|
+
"Run 'claude' to log in if needed."
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
return credentials
|
|
73
124
|
|
|
74
125
|
|
|
75
126
|
def check_token_expired(credentials: dict) -> bool:
|
ccburn/display/chart.py
CHANGED
|
@@ -10,8 +10,10 @@ from rich.jupyter import JupyterMixin
|
|
|
10
10
|
|
|
11
11
|
try:
|
|
12
12
|
from ..data.models import LimitData, UsageSnapshot
|
|
13
|
+
from ..utils.formatting import get_utilization_color
|
|
13
14
|
except ImportError:
|
|
14
15
|
from ccburn.data.models import LimitData, UsageSnapshot
|
|
16
|
+
from ccburn.utils.formatting import get_utilization_color
|
|
15
17
|
|
|
16
18
|
|
|
17
19
|
class BurnupChart(JupyterMixin):
|
|
@@ -143,8 +145,11 @@ class BurnupChart(JupyterMixin):
|
|
|
143
145
|
values.append(util_pct)
|
|
144
146
|
|
|
145
147
|
if times:
|
|
146
|
-
#
|
|
147
|
-
|
|
148
|
+
# Calculate budget pace for color determination
|
|
149
|
+
elapsed_hours = (now - original_window_start).total_seconds() / 3600
|
|
150
|
+
budget_pace = min(elapsed_hours / original_window_hours, 1.0)
|
|
151
|
+
# Determine line color based on utilization AND burn rate
|
|
152
|
+
color = self._get_plotext_color(self.limit_data.utilization, budget_pace)
|
|
148
153
|
# Use fillx=True for area chart effect (fills down to x-axis)
|
|
149
154
|
plt.plot(
|
|
150
155
|
times,
|
|
@@ -258,23 +263,27 @@ class BurnupChart(JupyterMixin):
|
|
|
258
263
|
|
|
259
264
|
return plt.build()
|
|
260
265
|
|
|
261
|
-
def _get_plotext_color(
|
|
262
|
-
|
|
266
|
+
def _get_plotext_color(
|
|
267
|
+
self, utilization: float, budget_pace: float = 0.0
|
|
268
|
+
) -> tuple[int, int, int]:
|
|
269
|
+
"""Get plotext RGB color based on utilization and burn rate.
|
|
263
270
|
|
|
264
271
|
Args:
|
|
265
272
|
utilization: Current utilization (0-1)
|
|
273
|
+
budget_pace: How much of window has elapsed (0-1)
|
|
266
274
|
|
|
267
275
|
Returns:
|
|
268
276
|
RGB tuple for plotext - bright vivid colors matching Rich progress bars
|
|
269
277
|
"""
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
+
# Reuse shared color logic, map Rich color names to RGB
|
|
279
|
+
color_name = get_utilization_color(utilization, budget_pace)
|
|
280
|
+
color_map = {
|
|
281
|
+
"green": (0, 255, 0),
|
|
282
|
+
"yellow": (255, 255, 0),
|
|
283
|
+
"bright_red": (255, 165, 0), # Orange
|
|
284
|
+
"red": (255, 0, 0),
|
|
285
|
+
}
|
|
286
|
+
return color_map.get(color_name, (255, 255, 0))
|
|
278
287
|
|
|
279
288
|
|
|
280
289
|
def create_simple_chart(
|
ccburn/display/gauges.py
CHANGED
|
@@ -110,9 +110,9 @@ def create_gauge_section(
|
|
|
110
110
|
utilization_percent = limit_data.utilization * 100
|
|
111
111
|
pace_percent = budget_pace * 100
|
|
112
112
|
|
|
113
|
-
# Usage bar - color by threshold
|
|
113
|
+
# Usage bar - color by threshold AND burn rate
|
|
114
114
|
# complete_style = filled portion (bright), style = unfilled portion (dim)
|
|
115
|
-
usage_color = get_utilization_color(limit_data.utilization)
|
|
115
|
+
usage_color = get_utilization_color(limit_data.utilization, budget_pace)
|
|
116
116
|
usage_bar = ProgressBar(
|
|
117
117
|
total=100,
|
|
118
118
|
completed=utilization_percent,
|
|
@@ -120,15 +120,9 @@ def create_gauge_section(
|
|
|
120
120
|
complete_style=Style(color=usage_color), # Bright color for filled
|
|
121
121
|
)
|
|
122
122
|
|
|
123
|
-
# Usage label
|
|
124
|
-
usage_emoji = "📊"
|
|
125
|
-
if limit_data.utilization >= 0.9:
|
|
126
|
-
usage_emoji = "🚨"
|
|
127
|
-
elif limit_data.utilization >= 0.75:
|
|
128
|
-
usage_emoji = "⚠️"
|
|
129
|
-
|
|
123
|
+
# Usage label - keep emoji consistent (⚠️ has inconsistent width across terminals)
|
|
130
124
|
usage_label = Text()
|
|
131
|
-
usage_label.append(
|
|
125
|
+
usage_label.append("📊 ", style="")
|
|
132
126
|
usage_label.append("Usage", style=f"bold {usage_color}")
|
|
133
127
|
|
|
134
128
|
table.add_row(
|
ccburn/utils/formatting.py
CHANGED
|
@@ -82,28 +82,50 @@ def format_reset_time(resets_at: datetime, now: datetime | None = None) -> str:
|
|
|
82
82
|
# Convert to local time for display
|
|
83
83
|
local_time = resets_at.astimezone()
|
|
84
84
|
day_name = local_time.strftime("%a") # "Tue"
|
|
85
|
-
|
|
85
|
+
# Use %I and strip leading zero (%-I is Unix-only, %#I is Windows-only)
|
|
86
|
+
time_str = local_time.strftime("%I:%M %p").lstrip("0") # "4:00 PM"
|
|
86
87
|
return f"Resets {day_name} {time_str}"
|
|
87
88
|
|
|
88
89
|
|
|
89
|
-
def get_utilization_color(utilization: float) -> str:
|
|
90
|
-
"""Get color based on utilization
|
|
90
|
+
def get_utilization_color(utilization: float, budget_pace: float = 0.0) -> str:
|
|
91
|
+
"""Get color based on utilization and burn rate.
|
|
91
92
|
|
|
92
93
|
Args:
|
|
93
|
-
utilization:
|
|
94
|
+
utilization: Current usage (0-1)
|
|
95
|
+
budget_pace: How much of window has elapsed (0-1)
|
|
94
96
|
|
|
95
97
|
Returns:
|
|
96
|
-
Color name: "green", "yellow", "
|
|
98
|
+
Color name: "green", "yellow", "bright_red", or "red"
|
|
97
99
|
"""
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
elif utilization < 0.75:
|
|
101
|
-
return "yellow"
|
|
102
|
-
elif utilization < 0.9:
|
|
103
|
-
return "bright_red" # Rich uses "bright_red" for orange-like
|
|
104
|
-
else:
|
|
100
|
+
# Critical: always red at very high utilization
|
|
101
|
+
if utilization >= 0.9:
|
|
105
102
|
return "red"
|
|
106
103
|
|
|
104
|
+
# Calculate burn ratio if we have meaningful data
|
|
105
|
+
burn_ratio = 1.0
|
|
106
|
+
if budget_pace >= 0.05 and utilization >= 0.01:
|
|
107
|
+
burn_ratio = utilization / budget_pace
|
|
108
|
+
|
|
109
|
+
# High utilization: at least orange, red if also burning fast
|
|
110
|
+
if utilization >= 0.75:
|
|
111
|
+
return "red" if burn_ratio > 1.5 else "bright_red"
|
|
112
|
+
|
|
113
|
+
# Moderate utilization: color based on burn rate
|
|
114
|
+
if utilization >= 0.5:
|
|
115
|
+
if burn_ratio > 2.0:
|
|
116
|
+
return "red"
|
|
117
|
+
if burn_ratio > 1.5:
|
|
118
|
+
return "bright_red"
|
|
119
|
+
return "yellow"
|
|
120
|
+
|
|
121
|
+
# Low utilization: escalate only if burning very fast
|
|
122
|
+
if burn_ratio > 3.0:
|
|
123
|
+
return "bright_red" # Will hit limit at ~33% of window
|
|
124
|
+
if burn_ratio > 2.0:
|
|
125
|
+
return "yellow" # Will hit limit at ~50% of window
|
|
126
|
+
|
|
127
|
+
return "green"
|
|
128
|
+
|
|
107
129
|
|
|
108
130
|
def get_status_indicator(utilization: float, budget_pace: float) -> str:
|
|
109
131
|
"""Get status indicator for compact output.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: ccburn
|
|
3
|
-
Version: 0.1
|
|
3
|
+
Version: 0.2.1
|
|
4
4
|
Summary: Terminal-based Claude Code usage limit visualizer with real-time burn-up charts
|
|
5
5
|
Author: JuanjoFuchs
|
|
6
6
|
License-Expression: MIT
|
|
@@ -41,11 +41,12 @@ Dynamic: license-file
|
|
|
41
41
|
|
|
42
42
|
[](https://github.com/JuanjoFuchs/ccburn/actions/workflows/ci.yml)
|
|
43
43
|
[](https://github.com/JuanjoFuchs/ccburn/actions/workflows/release.yml)
|
|
44
|
+
[](https://www.npmjs.com/package/ccburn)
|
|
44
45
|
[](https://pypi.org/project/ccburn/)
|
|
45
46
|
[](https://pypi.org/project/ccburn/)
|
|
46
47
|
[](https://github.com/JuanjoFuchs/ccburn/releases)
|
|
47
48
|
[](https://github.com/microsoft/winget-pkgs/pulls?q=is%3Apr+ccburn)
|
|
48
|
-
[](LICENSE)
|
|
49
50
|
|
|
50
51
|
<p align="center">
|
|
51
52
|
<img src="docs/cash1.png" alt="Burning tokens" width="140">
|
|
@@ -70,13 +71,27 @@ TUI and CLI for Claude Code usage limits — burn-up charts, compact mode for st
|
|
|
70
71
|
|
|
71
72
|
## Installation
|
|
72
73
|
|
|
73
|
-
|
|
74
|
+
Run `claude` and login first to refresh credentials.
|
|
75
|
+
|
|
76
|
+
### WinGet (*pending approval*)
|
|
74
77
|
|
|
75
78
|
```powershell
|
|
76
79
|
winget install JuanjoFuchs.ccburn
|
|
77
80
|
```
|
|
78
81
|
|
|
79
|
-
###
|
|
82
|
+
### npx
|
|
83
|
+
|
|
84
|
+
```bash
|
|
85
|
+
npx ccburn
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
### npm
|
|
89
|
+
|
|
90
|
+
```bash
|
|
91
|
+
npm install -g ccburn
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
### pip
|
|
80
95
|
|
|
81
96
|
```bash
|
|
82
97
|
pip install ccburn
|
|
@@ -92,7 +107,10 @@ pip install -e ".[dev]"
|
|
|
92
107
|
|
|
93
108
|
## Quick Start
|
|
94
109
|
|
|
95
|
-
1. **
|
|
110
|
+
1. **Run Claude Code first** to ensure credentials are fresh:
|
|
111
|
+
```bash
|
|
112
|
+
claude
|
|
113
|
+
```
|
|
96
114
|
2. **Run ccburn:**
|
|
97
115
|
```bash
|
|
98
116
|
ccburn # Session limit (default)
|
|
@@ -3,20 +3,20 @@ ccburn/app.py,sha256=FcVHLbnygKbMTGIFZRN5jS_Pve8VdKImfLi2KsDuEoU,15510
|
|
|
3
3
|
ccburn/cli.py,sha256=5qYYmOWcUXNYEdK-E3DBixfWQK4wKIQs6FPvTrIfOVI,7184
|
|
4
4
|
ccburn/main.py,sha256=TqWLl9xxOtbpTQr-ObomzSLG3jNec2GZ6RKEQYYdGLg,2474
|
|
5
5
|
ccburn/data/__init__.py,sha256=ZczEZwodQ-MMO5F7fVNsyIpUCRY8Ya9W4pwdOOJWxm4,803
|
|
6
|
-
ccburn/data/credentials.py,sha256=
|
|
6
|
+
ccburn/data/credentials.py,sha256=wDiiTkZZDBjnYspvtWJ_52xXdTdIgBndLlfFMi-peZ8,5228
|
|
7
7
|
ccburn/data/history.py,sha256=ouBxrXpMp_eTs0kba1Bg55TI6bsBSMToJ32tH1wNHQI,12879
|
|
8
8
|
ccburn/data/models.py,sha256=Sd2T36gH6OaNHl9zRlnnQXI-ziBA8Gl6rPYQIzmr7G4,5403
|
|
9
9
|
ccburn/data/usage_client.py,sha256=_dGwmI5vYPk4S-HUe2_fnTwSuAfTPaOFff7mKPFnhps,4570
|
|
10
10
|
ccburn/display/__init__.py,sha256=aL7TV53kU5oxlIwJ8M17stG2aC6UeGB-pj2u5BOpegs,495
|
|
11
|
-
ccburn/display/chart.py,sha256=
|
|
12
|
-
ccburn/display/gauges.py,sha256=
|
|
11
|
+
ccburn/display/chart.py,sha256=HGDcMqaqyNtlQzV-d-gUOStq8qjpVt4EYvCIU6BTAx8,11983
|
|
12
|
+
ccburn/display/gauges.py,sha256=DCRunsEtv1HflRkTZLwnNbVZhe47gCIH78p3gfqFwDo,8594
|
|
13
13
|
ccburn/display/layout.py,sha256=UndPxyh32jWGdDgOZCvedz06WcKxYMSchLwpOkkXQKo,8093
|
|
14
14
|
ccburn/utils/__init__.py,sha256=N6EzUX9hUJkuga_l9Ci3of1CWNtQgpNmMmNyY2DgYrg,1119
|
|
15
15
|
ccburn/utils/calculator.py,sha256=QcFm5X-VWZzucHdInEjjqKV5oZaNsdpMgl8oKvHAQYc,6174
|
|
16
|
-
ccburn/utils/formatting.py,sha256=
|
|
17
|
-
ccburn-0.1.
|
|
18
|
-
ccburn-0.1.
|
|
19
|
-
ccburn-0.1.
|
|
20
|
-
ccburn-0.1.
|
|
21
|
-
ccburn-0.1.
|
|
22
|
-
ccburn-0.1.
|
|
16
|
+
ccburn/utils/formatting.py,sha256=MEVIohBmvSur0hcc67oyYRDooiUMf0rPa4LO1fc2Ud4,4174
|
|
17
|
+
ccburn-0.2.1.dist-info/licenses/LICENSE,sha256=Qf2mqNi2qJ35JytfoTdR1SgYhZ2Mt4Ohcf-tu_MuYC0,1068
|
|
18
|
+
ccburn-0.2.1.dist-info/METADATA,sha256=gnEPQbkE97in73KUywvp6hF7nrMUzYCG5Lvv1UU06ZI,6979
|
|
19
|
+
ccburn-0.2.1.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
20
|
+
ccburn-0.2.1.dist-info/entry_points.txt,sha256=GfFQ5VusMR8RJ9meygqWjaErdmYsf_arbILzf64WjLU,43
|
|
21
|
+
ccburn-0.2.1.dist-info/top_level.txt,sha256=SM8TwGQZqQKKIQObVWQkfpA0OI4gRut7bPl-iM3g5RI,7
|
|
22
|
+
ccburn-0.2.1.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|