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.
@@ -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 read_credentials() -> dict:
48
- """Read the raw credentials file.
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
- Raises:
54
- CredentialsNotFoundError: If file doesn't exist
55
- InvalidCredentialsError: If file is malformed
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
- raise CredentialsNotFoundError(
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 as e:
70
- raise InvalidCredentialsError(f"Invalid JSON in credentials file: {e}") from e
71
- except PermissionError as e:
72
- raise CredentialsError(f"Permission denied reading {creds_path}") from e
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
- # Determine line color based on current utilization
147
- color = self._get_plotext_color(self.limit_data.utilization)
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(self, utilization: float) -> tuple[int, int, int]:
262
- """Get plotext RGB color based on utilization.
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
- if utilization < 0.5:
271
- return (0, 255, 0) # Bright green
272
- elif utilization < 0.75:
273
- return (255, 255, 0) # Bright yellow
274
- elif utilization < 0.9:
275
- return (255, 165, 0) # Orange
276
- else:
277
- return (255, 0, 0) # Bright red
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 with emoji based on status
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(f"{usage_emoji} ", style="")
125
+ usage_label.append("📊 ", style="")
132
126
  usage_label.append("Usage", style=f"bold {usage_color}")
133
127
 
134
128
  table.add_row(
@@ -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
- time_str = local_time.strftime("%-I:%M %p") # "4:00 PM"
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 percentage.
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: Float between 0 and 1
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", "orange", or "red"
98
+ Color name: "green", "yellow", "bright_red", or "red"
97
99
  """
98
- if utilization < 0.5:
99
- return "green"
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.7
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
  [![CI](https://img.shields.io/github/actions/workflow/status/JuanjoFuchs/ccburn/ci.yml?branch=main&label=CI)](https://github.com/JuanjoFuchs/ccburn/actions/workflows/ci.yml)
43
43
  [![Release](https://img.shields.io/github/actions/workflow/status/JuanjoFuchs/ccburn/release.yml?label=Release)](https://github.com/JuanjoFuchs/ccburn/actions/workflows/release.yml)
44
+ [![npm](https://img.shields.io/npm/v/ccburn)](https://www.npmjs.com/package/ccburn)
44
45
  [![PyPI](https://img.shields.io/pypi/v/ccburn)](https://pypi.org/project/ccburn/)
45
46
  [![Python](https://img.shields.io/pypi/pyversions/ccburn)](https://pypi.org/project/ccburn/)
46
47
  [![GitHub Release](https://img.shields.io/github/v/release/JuanjoFuchs/ccburn)](https://github.com/JuanjoFuchs/ccburn/releases)
47
48
  [![WinGet](https://img.shields.io/badge/WinGet-pending-yellow)](https://github.com/microsoft/winget-pkgs/pulls?q=is%3Apr+ccburn)
48
- [![License](https://img.shields.io/badge/license-MIT-green)](LICENSE)
49
+ [![License](https://img.shields.io/github/license/JuanjoFuchs/ccburn)](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
- ### Windows (WinGet) *pending approval*
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
- ### Cross-Platform (pip)
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. **Ensure Claude Code is installed** ccburn reads credentials from Claude Code's config
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=PYrgzx-2NyiMTtIwBhulUdKawfT8cvWW4wIw2dOptks,3752
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=j1qM6YX_K5SfKCxaob3-mT1vJjr8VMdsHIN8ORJu-2c,11421
12
- ccburn/display/gauges.py,sha256=jkPy3BuPsN1__4Ynt0DxK8t3Su_2jl5HAJifCmdtiiA,8696
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=vFpGY8v60G8OjTmZ1I1dYCr3h7YIfB8ezVMICb6RyZk,3327
17
- ccburn-0.1.7.dist-info/licenses/LICENSE,sha256=Qf2mqNi2qJ35JytfoTdR1SgYhZ2Mt4Ohcf-tu_MuYC0,1068
18
- ccburn-0.1.7.dist-info/METADATA,sha256=_LEB2WIp8EaFVJnhP8IZdtJz4EybYIzrYZ-L3sGM0jw,6787
19
- ccburn-0.1.7.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
20
- ccburn-0.1.7.dist-info/entry_points.txt,sha256=GfFQ5VusMR8RJ9meygqWjaErdmYsf_arbILzf64WjLU,43
21
- ccburn-0.1.7.dist-info/top_level.txt,sha256=SM8TwGQZqQKKIQObVWQkfpA0OI4gRut7bPl-iM3g5RI,7
22
- ccburn-0.1.7.dist-info/RECORD,,
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