From 6f81f60a834c081b17545ee53a993617450bccaa Mon Sep 17 00:00:00 2001 From: Adam Wallis Date: Thu, 19 Jun 2025 14:49:33 -0400 Subject: [PATCH 1/3] feat: add uv package manager support Add modern Python packaging with uv tool installation support: - Add pyproject.toml with console script entry points (ccusage-monitor, claude-monitor) - Update README.md to prioritize uv installation over legacy methods - Add CLAUDE.md development documentation - Add comprehensive .gitignore for Python project Users can now install with: uv tool install claude-usage-monitor Maintains full backwards compatibility with existing installation methods --- .gitignore | 201 +++++++++++++++++++++++++++++++++++++++++++++++++ CLAUDE.md | 145 +++++++++++++++++++++++++++++++++++ README.md | 158 ++++++++++++++++++++++++++++++-------- pyproject.toml | 59 +++++++++++++++ 4 files changed, 530 insertions(+), 33 deletions(-) create mode 100644 .gitignore create mode 100644 CLAUDE.md create mode 100644 pyproject.toml diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..598f462 --- /dev/null +++ b/.gitignore @@ -0,0 +1,201 @@ +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ +cover/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +.pybuilder/ +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +# For a library or package, you might want to ignore these files since the code is +# intended to run in multiple environments; otherwise, check them in: +# .python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +#Pipfile.lock + +# poetry +# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. +# This is especially recommended for binary packages to ensure reproducibility, and is more +# commonly ignored for libraries. +# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control +#poetry.lock + +# pdm +# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. +#pdm.lock +# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it +# in version control. +# https://pdm.fming.dev/#use-with-ide +.pdm.toml + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +# pytype static type analyzer +.pytype/ + +# Cython debug symbols +cython_debug/ + +# PyCharm +# JetBrains specific template is maintained in a separate JetBrains.gitignore that can +# be added to the global gitignore or merged into this project gitignore. For a PyCharm +# project, it is not recommended to check the machine-specific absolute paths. +.idea/ + +# VS Code +.vscode/ + +# macOS +.DS_Store +.DS_Store? +._* +.Spotlight-V100 +.Trashes +ehthumbs.db +Thumbs.db + +# Windows +Thumbs.db +ehthumbs.db +Desktop.ini + +# uv +.python-version +uv.lock + +# Project-specific +# Claude monitor database (for future ML features) +*.db +*.sqlite +.claude_monitor/ + +# Temporary files +*.tmp +*.temp +*.swp +*.swo +*~ + +# Log files +*.log +logs/ + +# Editor backups +*.bak +*.orig \ No newline at end of file diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..1bbf221 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,145 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## Project Overview + +Claude Code Usage Monitor is a Python-based terminal application that provides real-time monitoring of Claude AI token usage. The project tracks token consumption, calculates burn rates, and predicts when tokens will be depleted across different Claude subscription plans. + +## Core Architecture + +### Main Components +- **ccusage_monitor.py**: Single-file Python application containing all monitoring logic +- **ccusage CLI integration**: Uses the `ccusage` npm package to fetch token usage data via `ccusage blocks --json` +- **Session management**: Tracks 5-hour rolling session windows with automatic detection +- **Plan detection**: Supports Pro (~7K), Max5 (~35K), Max20 (~140K), and custom_max (auto-detected) plans + +### Key Functions +- `run_ccusage()`: Executes ccusage CLI and parses JSON output at ccusage_monitor.py:13 +- `calculate_hourly_burn_rate()`: Analyzes token consumption patterns from the last hour at ccusage_monitor.py:101 +- Session tracking logic handles overlapping 5-hour windows and automatic plan switching + +## Development Commands + +### Setup and Installation + +#### Modern Installation with uv (Recommended) +```bash +# Install global dependency +npm install -g ccusage + +# Install the tool directly with uv +uv tool install claude-usage-monitor + +# Run from anywhere +ccusage-monitor +# or +claude-monitor +``` + +#### Traditional Installation +```bash +# Install global dependency +npm install -g ccusage + +# Create virtual environment (recommended) +python3 -m venv venv +source venv/bin/activate # Linux/Mac +# venv\Scripts\activate # Windows + +# Install Python dependencies +pip install pytz + +# Make executable (Linux/Mac) +chmod +x ccusage_monitor.py +``` + +#### Development Setup with uv +```bash +# Install global dependency +npm install -g ccusage + +# Clone and set up for development +git clone https://github.com/Maciek-roboblog/Claude-Code-Usage-Monitor.git +cd Claude-Code-Usage-Monitor + +# Install in development mode with uv +uv sync +uv run ccusage_monitor.py +``` + +### Running the Monitor + +#### With uv tool installation +```bash +# Default mode (Pro plan) +ccusage-monitor +# or +claude-monitor + +# Different plans +ccusage-monitor --plan max5 +ccusage-monitor --plan max20 +ccusage-monitor --plan custom_max + +# Custom configuration +ccusage-monitor --reset-hour 9 --timezone US/Eastern +``` + +#### Traditional/Development mode +```bash +# Default mode (Pro plan) +python ccusage_monitor.py +./ccusage_monitor.py # If made executable + +# Different plans +./ccusage_monitor.py --plan max5 +./ccusage_monitor.py --plan max20 +./ccusage_monitor.py --plan custom_max + +# Custom configuration +./ccusage_monitor.py --reset-hour 9 --timezone US/Eastern + +# With uv in development +uv run ccusage_monitor.py --plan max5 +``` + +### Testing +Currently no automated test suite. Manual testing involves: +- Running on different platforms (Linux, macOS, Windows) +- Testing with different Python versions (3.6+) +- Verifying plan detection and session tracking + +## Dependencies + +### External Dependencies +- **ccusage**: npm package for Claude token usage data (must be installed globally) +- **pytz**: Python timezone handling library + +### Standard Library Usage +- subprocess: For executing ccusage CLI commands +- json: For parsing ccusage output +- datetime/timedelta: For session time calculations +- argparse: For command-line interface + +## Development Notes + +### Session Logic +The monitor operates on Claude's 5-hour rolling session system: +- Sessions start with first message and last exactly 5 hours +- Multiple sessions can be active simultaneously +- Token limits apply per 5-hour session window +- Burn rate calculated from all sessions in the last hour + +### Plan Detection +- Starts with Pro plan (7K tokens) by default +- Automatically switches to custom_max when Pro limit exceeded +- custom_max scans previous sessions to find actual token limits +- Supports manual plan specification via command line + +### Future Development +See DEVELOPMENT.md for roadmap including: +- ML-powered auto-detection with DuckDB storage +- PyPI package distribution +- Docker containerization with web dashboard +- Enhanced analytics and prediction features \ No newline at end of file diff --git a/README.md b/README.md index 3bb2b14..b65ccff 100644 --- a/README.md +++ b/README.md @@ -49,9 +49,44 @@ A beautiful real-time terminal monitoring tool for Claude AI token usage. Track ## 🚀 Installation -### ⚡ Quick Start +### ⚡ Modern Installation with uv (Recommended) -For immediate testing (not recommended for regular use): +The fastest and easiest way to install and use the monitor: + +#### First-time uv users +If you don't have uv installed yet, get it with one command: + +```bash +# Install uv (one-time setup) + +# On Linux/macOS: +curl -LsSf https://astral.sh/uv/install.sh | sh + +# On Windows: +powershell -ExecutionPolicy ByPass -c "irm https://astral.sh/uv/install.ps1 | iex" + +# After installation, restart your terminal +``` + +#### Install and run the monitor +```bash +# Install dependencies +npm install -g ccusage + +# Install the tool directly with uv +uv tool install claude-usage-monitor + +# Run from anywhere +ccusage-monitor +# or +claude-monitor +``` + +### 🔧 Legacy Installation Methods + +#### Quick Start (Development/Testing) + +For immediate testing or development: ```bash # Install dependencies @@ -64,14 +99,12 @@ cd Claude-Code-Usage-Monitor python ccusage_monitor.py ``` -### 🔒 Production Setup (Recommended) - #### Prerequisites 1. **Python 3.6+** installed on your system 2. **Node.js** for ccusage CLI tool -### Virtual Environment Setup +#### Virtual Environment Setup #### Why Use Virtual Environment? @@ -182,6 +215,18 @@ claude-monitor ### Basic Usage +#### With uv tool installation (Recommended) +```bash +# Default (Pro plan - 7,000 tokens) +ccusage-monitor +# or +claude-monitor + +# Exit the monitor +# Press Ctrl+C to gracefully exit +``` + +#### Traditional/Development mode ```bash # Default (Pro plan - 7,000 tokens) ./ccusage_monitor.py @@ -194,6 +239,22 @@ claude-monitor #### Specify Your Plan +**With uv tool installation:** +```bash +# Pro plan (~7,000 tokens) - Default +ccusage-monitor --plan pro + +# Max5 plan (~35,000 tokens) +ccusage-monitor --plan max5 + +# Max20 plan (~140,000 tokens) +ccusage-monitor --plan max20 + +# Auto-detect from highest previous session +ccusage-monitor --plan custom_max +``` + +**Traditional/Development mode:** ```bash # Pro plan (~7,000 tokens) - Default ./ccusage_monitor.py --plan pro @@ -210,6 +271,16 @@ claude-monitor #### Custom Reset Times +**With uv tool installation:** +```bash +# Reset at 3 AM +ccusage-monitor --reset-hour 3 + +# Reset at 10 PM +ccusage-monitor --reset-hour 22 +``` + +**Traditional/Development mode:** ```bash # Reset at 3 AM ./ccusage_monitor.py --reset-hour 3 @@ -222,6 +293,22 @@ claude-monitor The default timezone is **Europe/Warsaw**. Change it to any valid timezone: +**With uv tool installation:** +```bash +# Use US Eastern Time +ccusage-monitor --timezone US/Eastern + +# Use Tokyo time +ccusage-monitor --timezone Asia/Tokyo + +# Use UTC +ccusage-monitor --timezone UTC + +# Use London time +ccusage-monitor --timezone Europe/London +``` + +**Traditional/Development mode:** ```bash # Use US Eastern Time ./ccusage_monitor.py --timezone US/Eastern @@ -390,10 +477,10 @@ The auto-detection system: ```bash # Auto-detect your highest previous usage -./ccusage_monitor.py --plan custom_max +ccusage-monitor --plan custom_max # Monitor with custom scheduling -./ccusage_monitor.py --plan custom_max --reset-hour 6 +ccusage-monitor --plan custom_max --reset-hour 6 ``` **Approach**: @@ -406,16 +493,16 @@ The auto-detection system: ```bash # US East Coast -./ccusage_monitor.py --timezone America/New_York +ccusage-monitor --timezone America/New_York # Europe -./ccusage_monitor.py --timezone Europe/London +ccusage-monitor --timezone Europe/London # Asia Pacific -./ccusage_monitor.py --timezone Asia/Singapore +ccusage-monitor --timezone Asia/Singapore # UTC for international team coordination -./ccusage_monitor.py --timezone UTC --reset-hour 12 +ccusage-monitor --timezone UTC --reset-hour 12 ``` #### ⚡ Quick Check @@ -423,7 +510,7 @@ The auto-detection system: ```bash # Just run it with defaults -./ccusage_monitor.py +ccusage-monitor # Press Ctrl+C after checking status ``` @@ -435,7 +522,7 @@ The auto-detection system: **Start with Default (Recommended for New Users)** ```bash # Pro plan detection with auto-switching -./ccusage_monitor.py +ccusage-monitor ``` - Monitor will detect if you exceed Pro limits - Automatically switches to custom_max if needed @@ -444,16 +531,16 @@ The auto-detection system: **Known Subscription Users** ```bash # If you know you have Max5 -./ccusage_monitor.py --plan max5 +ccusage-monitor --plan max5 # If you know you have Max20 -./ccusage_monitor.py --plan max20 +ccusage-monitor --plan max20 ``` **Unknown Limits** ```bash # Auto-detect from previous usage -./ccusage_monitor.py --plan custom_max +ccusage-monitor --plan custom_max ``` ### Best Practices @@ -462,27 +549,29 @@ The auto-detection system: 1. **Start Early in Sessions** ```bash - # Begin monitoring when starting Claude work + # Begin monitoring when starting Claude work (uv installation) + ccusage-monitor + + # Or development mode ./ccusage_monitor.py ``` - Gives accurate session tracking from the start - Better burn rate calculations - Early warning for limit approaches -2. **Use Virtual Environment** +2. **Use Modern Installation (Recommended)** ```bash - # Production setup with isolation - python3 -m venv venv - source venv/bin/activate - pip install pytz + # Easy installation and updates with uv + uv tool install claude-usage-monitor + ccusage-monitor --plan max5 ``` - - Prevents dependency conflicts - - Clean uninstallation - - Reproducible environments + - Clean system installation + - Easy updates and maintenance + - Available from anywhere -3. **Custom Shell Alias** +3. **Custom Shell Alias (Legacy Setup)** ```bash - # Add to ~/.bashrc or ~/.zshrc + # Add to ~/.bashrc or ~/.zshrc (only for development setup) alias claude-monitor='cd ~/Claude-Code-Usage-Monitor && source venv/bin/activate && ./ccusage_monitor.py' ``` @@ -496,7 +585,7 @@ The auto-detection system: 2. **Strategic Session Planning** ```bash # Plan heavy usage around reset times - ./ccusage_monitor.py --reset-hour 9 + ccusage-monitor --reset-hour 9 ``` - Schedule large tasks after resets - Use lighter tasks when approaching limits @@ -505,7 +594,7 @@ The auto-detection system: 3. **Timezone Awareness** ```bash # Always use your actual timezone - ./ccusage_monitor.py --timezone Europe/Warsaw + ccusage-monitor --timezone Europe/Warsaw ``` - Accurate reset time predictions - Better planning for work schedules @@ -520,7 +609,10 @@ The auto-detection system: 2. **Workflow Integration** ```bash - # Start monitoring with your development session + # Start monitoring with your development session (uv installation) + tmux new-session -d -s claude-monitor 'ccusage-monitor' + + # Or development mode tmux new-session -d -s claude-monitor './ccusage_monitor.py' # Check status anytime @@ -537,7 +629,7 @@ The auto-detection system: **Large Project Development** ```bash # Setup for sustained development -./ccusage_monitor.py --plan max20 --reset-hour 8 --timezone America/New_York +ccusage-monitor --plan max20 --reset-hour 8 --timezone America/New_York ``` **Daily Routine**: @@ -550,13 +642,13 @@ The auto-detection system: **Learning & Experimentation** ```bash # Flexible setup for learning -./ccusage_monitor.py --plan pro +ccusage-monitor --plan pro ``` **Sprint Development** ```bash # High-intensity development setup -./ccusage_monitor.py --plan max20 --reset-hour 6 +ccusage-monitor --plan max20 --reset-hour 6 ``` --- diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..d99782f --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,59 @@ +[build-system] +requires = ["hatchling"] +build-backend = "hatchling.build" + +[project] +name = "claude-usage-monitor" +version = "1.0.0" +description = "A real-time terminal monitoring tool for Claude AI token usage" +readme = "README.md" +license = "MIT" +requires-python = ">=3.6" +authors = [ + { name = "Maciek", email = "maciek@roboblog.eu" }, +] +keywords = ["claude", "ai", "token", "monitoring", "usage", "terminal"] +classifiers = [ + "Development Status :: 5 - Production/Stable", + "Environment :: Console", + "Intended Audience :: Developers", + "License :: OSI Approved :: MIT License", + "Operating System :: OS Independent", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.6", + "Programming Language :: Python :: 3.7", + "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Topic :: Software Development", + "Topic :: System :: Monitoring", +] +dependencies = [ + "pytz", +] + +[project.urls] +Homepage = "https://github.com/Maciek-roboblog/Claude-Code-Usage-Monitor" +Repository = "https://github.com/Maciek-roboblog/Claude-Code-Usage-Monitor.git" +Issues = "https://github.com/Maciek-roboblog/Claude-Code-Usage-Monitor/issues" + +[project.scripts] +ccusage-monitor = "ccusage_monitor:main" +claude-monitor = "ccusage_monitor:main" + +[tool.hatch.build.targets.wheel] +packages = ["."] +include = ["ccusage_monitor.py"] + +[tool.hatch.build.targets.sdist] +include = [ + "ccusage_monitor.py", + "README.md", + "LICENSE", + "CLAUDE.md", + "DEVELOPMENT.md", + "CONTRIBUTING.md", + "TROUBLESHOOTING.md", +] \ No newline at end of file From 26bae77dfc2d85a539d5e71639dc253be6da9f78 Mon Sep 17 00:00:00 2001 From: Adam Wallis Date: Thu, 19 Jun 2025 14:56:59 -0400 Subject: [PATCH 2/3] docs: fix uv installation instructions for local install Update installation docs to use local directory instead of PyPI: - Change from 'uv tool install claude-usage-monitor' to 'uv tool install .' - Add git clone step since package not yet published to PyPI - Clarify platform-specific uv installation commands Fixes installation errors for users trying to install from PyPI --- CLAUDE.md | 67 ++++++++++++++++++++++++++++++++++++++++++++++++------- README.md | 6 +++-- 2 files changed, 63 insertions(+), 10 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 1bbf221..dff535d 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -8,15 +8,23 @@ Claude Code Usage Monitor is a Python-based terminal application that provides r ## Core Architecture -### Main Components -- **ccusage_monitor.py**: Single-file Python application containing all monitoring logic -- **ccusage CLI integration**: Uses the `ccusage` npm package to fetch token usage data via `ccusage blocks --json` -- **Session management**: Tracks 5-hour rolling session windows with automatic detection -- **Plan detection**: Supports Pro (~7K), Max5 (~35K), Max20 (~140K), and custom_max (auto-detected) plans +### Project Structure +This is a single-file Python application (418 lines) with modern packaging: +- **ccusage_monitor.py**: Main application containing all monitoring logic +- **pyproject.toml**: Modern Python packaging configuration with console script entry points +- **ccusage CLI integration**: External dependency on `ccusage` npm package for data fetching + +### Key Components +- **Data Collection**: Uses `ccusage blocks --json` to fetch Claude usage data +- **Session Management**: Tracks 5-hour rolling session windows with automatic detection +- **Plan Detection**: Supports Pro (~7K), Max5 (~35K), Max20 (~140K), and custom_max (auto-detected) plans +- **Real-time Display**: Terminal UI with progress bars and burn rate calculations +- **Console Scripts**: Two entry points (`ccusage-monitor`, `claude-monitor`) both calling `main()` ### Key Functions - `run_ccusage()`: Executes ccusage CLI and parses JSON output at ccusage_monitor.py:13 - `calculate_hourly_burn_rate()`: Analyzes token consumption patterns from the last hour at ccusage_monitor.py:101 +- `main()`: Entry point function at ccusage_monitor.py:249 for console script integration - Session tracking logic handles overlapping 5-hour windows and automatic plan switching ## Development Commands @@ -28,8 +36,10 @@ Claude Code Usage Monitor is a Python-based terminal application that provides r # Install global dependency npm install -g ccusage -# Install the tool directly with uv -uv tool install claude-usage-monitor +# Clone and install the tool with uv +git clone https://github.com/Maciek-roboblog/Claude-Code-Usage-Monitor.git +cd Claude-Code-Usage-Monitor +uv tool install . # Run from anywhere ccusage-monitor @@ -104,11 +114,36 @@ python ccusage_monitor.py uv run ccusage_monitor.py --plan max5 ``` -### Testing +### Building and Testing + +#### Package Building +```bash +# Build package with uv +uv build + +# Verify build artifacts +ls dist/ # Should show .whl and .tar.gz files +``` + +#### Testing Installation +```bash +# Test local installation +uv tool install --editable . + +# Verify commands work +ccusage-monitor --help +claude-monitor --help + +# Test uninstall +uv tool uninstall claude-usage-monitor +``` + +#### Manual Testing Currently no automated test suite. Manual testing involves: - Running on different platforms (Linux, macOS, Windows) - Testing with different Python versions (3.6+) - Verifying plan detection and session tracking +- Testing console script entry points (`ccusage-monitor`, `claude-monitor`) ## Dependencies @@ -137,6 +172,22 @@ The monitor operates on Claude's 5-hour rolling session system: - custom_max scans previous sessions to find actual token limits - Supports manual plan specification via command line +## Package Structure + +### Console Script Entry Points +The `pyproject.toml` defines two console commands: +```toml +[project.scripts] +ccusage-monitor = "ccusage_monitor:main" +claude-monitor = "ccusage_monitor:main" +``` +Both commands call the same `main()` function for consistency. + +### Build Configuration +- **Build backend**: hatchling (modern Python build system) +- **Python requirement**: >=3.6 for broad compatibility +- **Package includes**: Main script, documentation files, license + ### Future Development See DEVELOPMENT.md for roadmap including: - ML-powered auto-detection with DuckDB storage diff --git a/README.md b/README.md index b65ccff..9f9b5c5 100644 --- a/README.md +++ b/README.md @@ -73,8 +73,10 @@ powershell -ExecutionPolicy ByPass -c "irm https://astral.sh/uv/install.ps1 | ie # Install dependencies npm install -g ccusage -# Install the tool directly with uv -uv tool install claude-usage-monitor +# Clone and install the tool with uv +git clone https://github.com/Maciek-roboblog/Claude-Code-Usage-Monitor.git +cd Claude-Code-Usage-Monitor +uv tool install . # Run from anywhere ccusage-monitor From 4386d1c4fac1cc635b35e3f352a59fb50969ea03 Mon Sep 17 00:00:00 2001 From: Adam Wallis Date: Thu, 19 Jun 2025 15:18:40 -0400 Subject: [PATCH 3/3] feat: integrate ruff for code quality and formatting - Add comprehensive .ruff.toml configuration with essential linting rules - Set up pre-commit hooks for automated code quality checks - Create GitHub Actions CI workflow for ruff validation - Configure VS Code settings for real-time ruff integration - Format existing codebase to comply with ruff standards - Update documentation with ruff usage instructions - Update Python requirement from 3.6+ to 3.7+ (ruff minimum) - Replace outdated tools (black, flake8) with modern ruff tooling Resolves https://github.com/Maciek-roboblog/Claude-Code-Usage-Monitor/issues/15 --- .github/workflows/lint.yml | 51 +++ .pre-commit-config.yaml | 25 ++ .ruff.toml | 11 + .vscode/settings.json | 16 + CONTRIBUTING.md | 24 +- DEVELOPMENT.md | 41 ++- README.md | 12 +- __pycache__/ccusage_monitor.cpython-311.pyc | Bin 0 -> 16283 bytes ccusage_monitor.py | 348 +++++++++++--------- pyproject.toml | 12 +- 10 files changed, 359 insertions(+), 181 deletions(-) create mode 100644 .github/workflows/lint.yml create mode 100644 .pre-commit-config.yaml create mode 100644 .ruff.toml create mode 100644 .vscode/settings.json create mode 100644 __pycache__/ccusage_monitor.cpython-311.pyc diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml new file mode 100644 index 0000000..e7202d2 --- /dev/null +++ b/.github/workflows/lint.yml @@ -0,0 +1,51 @@ +name: Lint + +on: + push: + branches: [main] + pull_request: + branches: [main] + +jobs: + ruff: + runs-on: ubuntu-latest + name: Lint with Ruff + steps: + - uses: actions/checkout@v4 + + - name: Install uv + uses: astral-sh/setup-uv@v4 + with: + version: "latest" + + - name: Set up Python + run: uv python install + + - name: Install dependencies + run: uv sync --extra dev + + - name: Run Ruff linter + run: uv run ruff check --output-format=github . + + - name: Run Ruff formatter + run: uv run ruff format --check . + + pre-commit: + runs-on: ubuntu-latest + name: Pre-commit hooks + steps: + - uses: actions/checkout@v4 + + - name: Install uv + uses: astral-sh/setup-uv@v4 + with: + version: "latest" + + - name: Set up Python + run: uv python install + + - name: Install pre-commit + run: uv tool install pre-commit --with pre-commit-uv + + - name: Run pre-commit + run: uv tool run pre-commit run --all-files diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..482d076 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,25 @@ +# Pre-commit configuration +# https://pre-commit.com/ + +repos: + - repo: https://github.com/astral-sh/ruff-pre-commit + # Ruff version. + rev: v0.8.4 # Use the ref you want to point at + hooks: + # Run the linter. + - id: ruff + args: [--fix] + # Run the formatter. + - id: ruff-format + + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v5.0.0 + hooks: + - id: trailing-whitespace + - id: end-of-file-fixer + - id: check-yaml + - id: check-added-large-files + - id: check-merge-conflict + - id: check-toml + - id: mixed-line-ending + args: ['--fix=lf'] diff --git a/.ruff.toml b/.ruff.toml new file mode 100644 index 0000000..e3e98fd --- /dev/null +++ b/.ruff.toml @@ -0,0 +1,11 @@ +# Ruff configuration - replaces Black, isort, and basic linting +line-length = 88 +target-version = "py37" + +[lint] +# Essential rules only +select = ["E", "W", "F", "I"] # pycodestyle + Pyflakes + isort +ignore = ["E501"] # Line length handled by formatter + +[format] +quote-style = "double" diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..2585a71 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,16 @@ +{ + "[python]": { + "editor.formatOnSave": true, + "editor.codeActionsOnSave": { + "source.fixAll": "explicit", + "source.organizeImports": "explicit" + }, + "editor.defaultFormatter": "charliermarsh.ruff" + }, + "python.linting.enabled": true, + "python.linting.ruffEnabled": true, + "python.formatting.provider": "none", + "ruff.organizeImports": true, + "ruff.fixAll": true, + "ruff.showNotification": "always" +} diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 04e3059..559cabd 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -44,7 +44,7 @@ source venv/bin/activate # Linux/Mac pip install pytz # Install development dependencies (when available) -pip install pytest black flake8 +pip install pytest ruff # Make script executable (Linux/Mac) chmod +x ccusage_monitor.py @@ -93,7 +93,7 @@ We follow **PEP 8** with these specific guidelines: current_token_count = 1500 session_start_time = datetime.now() -# Bad: Unclear abbreviations +# Bad: Unclear abbreviations curr_tok_cnt = 1500 sess_st_tm = datetime.now() @@ -105,11 +105,11 @@ def calculate_burn_rate(tokens_used, time_elapsed): def predict_token_depletion(current_usage, burn_rate): """ Predicts when tokens will be depleted based on current burn rate. - + Args: current_usage (int): Current token count burn_rate (float): Tokens consumed per minute - + Returns: datetime: Estimated depletion time """ @@ -151,10 +151,10 @@ def test_token_calculation(): def test_burn_rate_calculation(): """Test burn rate calculation with edge cases.""" monitor = TokenMonitor() - + # Normal case assert monitor.calculate_burn_rate(100, 10) == 10.0 - + # Edge case: zero time assert monitor.calculate_burn_rate(100, 0) == 0 ``` @@ -172,7 +172,7 @@ git commit -m "Docs: Add examples for timezone configuration" # Prefixes to use: # Add: New features -# Fix: Bug fixes +# Fix: Bug fixes # Update: Improvements to existing features # Docs: Documentation changes # Test: Test additions or changes @@ -349,7 +349,7 @@ tox We aim for high test coverage: - **Core functionality**: 95%+ coverage -- **ML components**: 90%+ coverage +- **ML components**: 90%+ coverage - **UI components**: 80%+ coverage - **Utility functions**: 95%+ coverage @@ -360,7 +360,7 @@ Help us test on different platforms: - **Linux**: Ubuntu, Fedora, Arch, Debian - **macOS**: Intel and Apple Silicon Macs - **Windows**: Windows 10/11, different Python installations -- **Python versions**: 3.6, 3.7, 3.8, 3.9, 3.10, 3.11 +- **Python versions**: 3.7, 3.8, 3.9, 3.10, 3.11, 3.12 --- @@ -402,7 +402,7 @@ We're collecting **anonymized data** about token limits to improve auto-detectio Help us understand usage patterns: - Peak usage times -- Session duration preferences +- Session duration preferences - Token consumption patterns - Feature usage statistics @@ -471,10 +471,10 @@ If you experience unacceptable behavior, contact: [maciek@roboblog.eu](mailto:ma Thank you for considering contributing to Claude Code Usage Monitor! Every contribution, no matter how small, helps make this tool better for the entire community. -**Ready to get started?** +**Ready to get started?** 1. 🍴 Fork the repository -2. 💻 Set up your development environment +2. 💻 Set up your development environment 3. 🔍 Look at open issues for ideas 4. 🚀 Start coding! diff --git a/DEVELOPMENT.md b/DEVELOPMENT.md index 4d94e62..91b2d46 100644 --- a/DEVELOPMENT.md +++ b/DEVELOPMENT.md @@ -6,7 +6,7 @@ Features currently in development and planned for future releases of Claude Code ## 🎯 Current Development Status ### 🧠 ML-Powered Auto Mode -**Status**: 🔶 In Active Development +**Status**: 🔶 In Active Development #### Overview Intelligent Auto Mode with machine learning will actively learn your actual token limits and usage patterns. @@ -133,7 +133,7 @@ claude-usage-monitor/ --- ### 🐳 Docker Image -**Status**: 🔶 In Planning Phase +**Status**: 🔶 In Planning Phase #### Overview Docker containerization for easy deployment, consistent environments, and optional web dashboard. @@ -314,6 +314,35 @@ FROM python:alpine AS app ## 📋 Development Guidelines +### 🛠️ Code Quality Tools + +**Ruff Integration**: This project uses [Ruff](https://docs.astral.sh/ruff/) for fast Python linting and formatting. + +```bash +# Install pre-commit for automatic code quality checks +uv tool install pre-commit --with pre-commit-uv + +# Install pre-commit hooks +pre-commit install + +# Run ruff manually +ruff check . # Lint code +ruff format . # Format code +ruff check --fix . # Auto-fix issues +``` + +**Pre-commit Hooks**: Automatic code quality checks run before each commit: +- Ruff linting and formatting +- Import sorting +- Trailing whitespace removal +- YAML and TOML validation + +**VS Code Integration**: The project includes VS Code settings for: +- Auto-format on save with Ruff +- Real-time linting feedback +- Import organization +- Consistent code style + ### 🔄 Development Workflow 1. **Feature Planning** @@ -323,7 +352,7 @@ FROM python:alpine AS app 2. **Development Process** - Fork repository and create feature branch - - Follow code style guidelines (PEP 8 for Python) + - Code is automatically formatted and linted via pre-commit hooks - Write tests for new functionality - Update documentation @@ -365,9 +394,9 @@ FROM python:alpine AS app For technical discussions about development: -**📧 Email**: [maciek@roboblog.eu](mailto:maciek@roboblog.eu) -**💬 GitHub**: Open issues for feature discussions -**🔧 Technical Questions**: Include code examples and specific requirements +**📧 Email**: [maciek@roboblog.eu](mailto:maciek@roboblog.eu) +**💬 GitHub**: Open issues for feature discussions +**🔧 Technical Questions**: Include code examples and specific requirements --- diff --git a/README.md b/README.md index 9f9b5c5..70ac52d 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # 🎯 Claude Code Usage Monitor -[![Python Version](https://img.shields.io/badge/python-3.6+-blue.svg)](https://python.org) +[![Python Version](https://img.shields.io/badge/python-3.7+-blue.svg)](https://python.org) [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) [![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg)](http://makeapullrequest.com) @@ -103,7 +103,7 @@ python ccusage_monitor.py #### Prerequisites -1. **Python 3.6+** installed on your system +1. **Python 3.7+** installed on your system 2. **Node.js** for ccusage CLI tool #### Virtual Environment Setup @@ -384,7 +384,7 @@ Claude Code operates on a **5-hour rolling session window system**: 10:30 AM - First message (Session A starts) 03:30 PM - Session A expires (5 hours later) -12:15 PM - First message (Session B starts) +12:15 PM - First message (Session B starts) 05:15 PM - Session B expires (5 hours later) ``` @@ -393,7 +393,7 @@ Claude Code operates on a **5-hour rolling session window system**: The monitor calculates burn rate using sophisticated analysis: 1. **Data Collection**: Gathers token usage from all sessions in the last hour -2. **Pattern Analysis**: Identifies consumption trends across overlapping sessions +2. **Pattern Analysis**: Identifies consumption trends across overlapping sessions 3. **Velocity Tracking**: Calculates tokens consumed per minute 4. **Prediction Engine**: Estimates when current session tokens will deplete 5. **Real-time Updates**: Adjusts predictions as usage patterns change @@ -616,7 +616,7 @@ ccusage-monitor --plan custom_max # Or development mode tmux new-session -d -s claude-monitor './ccusage_monitor.py' - + # Check status anytime tmux attach -t claude-monitor ``` @@ -691,4 +691,4 @@ This tool builds upon the excellent [ccusage](https://github.com/ryoppippi/ccusa [Report Bug](https://github.com/Maciek-roboblog/Claude-Code-Usage-Monitor/issues) • [Request Feature](https://github.com/Maciek-roboblog/Claude-Code-Usage-Monitor/issues) • [Contribute](CONTRIBUTING.md) - \ No newline at end of file + diff --git a/__pycache__/ccusage_monitor.cpython-311.pyc b/__pycache__/ccusage_monitor.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..1d58928fd60faf34535a01ea0c69bc934614757d GIT binary patch literal 16283 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 literal 0 HcmV?d00001 diff --git a/ccusage_monitor.py b/ccusage_monitor.py index 23d15f5..d834d03 100644 --- a/ccusage_monitor.py +++ b/ccusage_monitor.py @@ -1,19 +1,22 @@ #!/usr/bin/env python3 -import subprocess +import argparse import json +import os +import subprocess import sys import time -from datetime import datetime, timedelta, timezone -import os -import argparse +from datetime import datetime, timedelta + import pytz def run_ccusage(): """Execute ccusage blocks --json command and return parsed JSON data.""" try: - result = subprocess.run(['ccusage', 'blocks', '--json'], capture_output=True, text=True, check=True) + result = subprocess.run( + ["ccusage", "blocks", "--json"], capture_output=True, text=True, check=True + ) return json.loads(result.stdout) except subprocess.CalledProcessError as e: print(f"Error running ccusage: {e}") @@ -37,16 +40,16 @@ def format_time(minutes): def create_token_progress_bar(percentage, width=50): """Create a token usage progress bar with bracket style.""" filled = int(width * percentage / 100) - + # Create the bar with green fill and red empty space - green_bar = '█' * filled - red_bar = '░' * (width - filled) - + green_bar = "█" * filled + red_bar = "░" * (width - filled) + # Color codes - green = '\033[92m' # Bright green - red = '\033[91m' # Bright red - reset = '\033[0m' - + green = "\033[92m" # Bright green + red = "\033[91m" # Bright red + reset = "\033[0m" + return f"🟢 [{green}{green_bar}{red}{red_bar}{reset}] {percentage:.1f}%" @@ -56,31 +59,31 @@ def create_time_progress_bar(elapsed_minutes, total_minutes, width=50): percentage = 0 else: percentage = min(100, (elapsed_minutes / total_minutes) * 100) - + filled = int(width * percentage / 100) - + # Create the bar with blue fill and red empty space - blue_bar = '█' * filled - red_bar = '░' * (width - filled) - + blue_bar = "█" * filled + red_bar = "░" * (width - filled) + # Color codes - blue = '\033[94m' # Bright blue - red = '\033[91m' # Bright red - reset = '\033[0m' - + blue = "\033[94m" # Bright blue + red = "\033[91m" # Bright red + reset = "\033[0m" + remaining_time = format_time(max(0, total_minutes - elapsed_minutes)) return f"⏰ [{blue}{blue_bar}{red}{red_bar}{reset}] {remaining_time}" def print_header(): """Print the stylized header with sparkles.""" - cyan = '\033[96m' - blue = '\033[94m' - reset = '\033[0m' - + cyan = "\033[96m" + blue = "\033[94m" + reset = "\033[0m" + # Sparkle pattern sparkles = f"{cyan}✦ ✧ ✦ ✧ {reset}" - + print(f"{sparkles}{cyan}CLAUDE TOKEN MONITOR{reset} {sparkles}") print(f"{blue}{'=' * 60}{reset}") print() @@ -89,73 +92,81 @@ def print_header(): def get_velocity_indicator(burn_rate): """Get velocity emoji based on burn rate.""" if burn_rate < 50: - return '🐌' # Slow + return "🐌" # Slow elif burn_rate < 150: - return '➡️' # Normal + return "➡️" # Normal elif burn_rate < 300: - return '🚀' # Fast + return "🚀" # Fast else: - return '⚡' # Very fast + return "⚡" # Very fast def calculate_hourly_burn_rate(blocks, current_time): """Calculate burn rate based on all sessions in the last hour.""" if not blocks: return 0 - + one_hour_ago = current_time - timedelta(hours=1) total_tokens = 0 - + for block in blocks: - start_time_str = block.get('startTime') + start_time_str = block.get("startTime") if not start_time_str: continue - + # Parse start time - start_time = datetime.fromisoformat(start_time_str.replace('Z', '+00:00')) - + start_time = datetime.fromisoformat(start_time_str.replace("Z", "+00:00")) + # Skip gaps - if block.get('isGap', False): + if block.get("isGap", False): continue - + # Determine session end time - if block.get('isActive', False): + if block.get("isActive", False): # For active sessions, use current time session_actual_end = current_time else: # For completed sessions, use actualEndTime or current time - actual_end_str = block.get('actualEndTime') + actual_end_str = block.get("actualEndTime") if actual_end_str: - session_actual_end = datetime.fromisoformat(actual_end_str.replace('Z', '+00:00')) + session_actual_end = datetime.fromisoformat( + actual_end_str.replace("Z", "+00:00") + ) else: session_actual_end = current_time - + # Check if session overlaps with the last hour if session_actual_end < one_hour_ago: # Session ended before the last hour continue - + # Calculate how much of this session falls within the last hour session_start_in_hour = max(start_time, one_hour_ago) session_end_in_hour = min(session_actual_end, current_time) - + if session_end_in_hour <= session_start_in_hour: continue - + # Calculate portion of tokens used in the last hour - total_session_duration = (session_actual_end - start_time).total_seconds() / 60 # minutes - hour_duration = (session_end_in_hour - session_start_in_hour).total_seconds() / 60 # minutes - + total_session_duration = ( + session_actual_end - start_time + ).total_seconds() / 60 # minutes + hour_duration = ( + session_end_in_hour - session_start_in_hour + ).total_seconds() / 60 # minutes + if total_session_duration > 0: - session_tokens = block.get('totalTokens', 0) + session_tokens = block.get("totalTokens", 0) tokens_in_hour = session_tokens * (hour_duration / total_session_duration) total_tokens += tokens_in_hour - + # Return tokens per minute return total_tokens / 60 if total_tokens > 0 else 0 -def get_next_reset_time(current_time, custom_reset_hour=None, timezone_str='Europe/Warsaw'): +def get_next_reset_time( + current_time, custom_reset_hour=None, timezone_str="Europe/Warsaw" +): """Calculate next token reset time based on fixed 5-hour intervals. Default reset times in specified timezone: 04:00, 09:00, 14:00, 18:00, 23:00 Or use custom reset hour if provided. @@ -165,255 +176,284 @@ def get_next_reset_time(current_time, custom_reset_hour=None, timezone_str='Euro target_tz = pytz.timezone(timezone_str) except pytz.exceptions.UnknownTimeZoneError: print(f"Warning: Unknown timezone '{timezone_str}', using Europe/Warsaw") - target_tz = pytz.timezone('Europe/Warsaw') - + target_tz = pytz.timezone("Europe/Warsaw") + # If current_time is timezone-aware, convert to target timezone if current_time.tzinfo is not None: target_time = current_time.astimezone(target_tz) else: # Assume current_time is in target timezone if not specified target_time = target_tz.localize(current_time) - + if custom_reset_hour is not None: # Use single daily reset at custom hour reset_hours = [custom_reset_hour] else: # Default 5-hour intervals reset_hours = [4, 9, 14, 18, 23] - + # Get current hour and minute current_hour = target_time.hour current_minute = target_time.minute - + # Find next reset hour next_reset_hour = None for hour in reset_hours: if current_hour < hour or (current_hour == hour and current_minute == 0): next_reset_hour = hour break - + # If no reset hour found today, use first one tomorrow if next_reset_hour is None: next_reset_hour = reset_hours[0] next_reset_date = target_time.date() + timedelta(days=1) else: next_reset_date = target_time.date() - + # Create next reset datetime in target timezone next_reset = target_tz.localize( - datetime.combine(next_reset_date, datetime.min.time().replace(hour=next_reset_hour)), - is_dst=None + datetime.combine( + next_reset_date, datetime.min.time().replace(hour=next_reset_hour) + ), + is_dst=None, ) - + # Convert back to the original timezone if needed if current_time.tzinfo is not None and current_time.tzinfo != target_tz: next_reset = next_reset.astimezone(current_time.tzinfo) - + return next_reset def parse_args(): """Parse command line arguments.""" - parser = argparse.ArgumentParser(description='Claude Token Monitor - Real-time token usage monitoring') - parser.add_argument('--plan', type=str, default='pro', - choices=['pro', 'max5', 'max20', 'custom_max'], - help='Claude plan type (default: pro). Use "custom_max" to auto-detect from highest previous block') - parser.add_argument('--reset-hour', type=int, - help='Change the reset hour (0-23) for daily limits') - parser.add_argument('--timezone', type=str, default='Europe/Warsaw', - help='Timezone for reset times (default: Europe/Warsaw). Examples: US/Eastern, Asia/Tokyo, UTC') + parser = argparse.ArgumentParser( + description="Claude Token Monitor - Real-time token usage monitoring" + ) + parser.add_argument( + "--plan", + type=str, + default="pro", + choices=["pro", "max5", "max20", "custom_max"], + help='Claude plan type (default: pro). Use "custom_max" to auto-detect from highest previous block', + ) + parser.add_argument( + "--reset-hour", type=int, help="Change the reset hour (0-23) for daily limits" + ) + parser.add_argument( + "--timezone", + type=str, + default="Europe/Warsaw", + help="Timezone for reset times (default: Europe/Warsaw). Examples: US/Eastern, Asia/Tokyo, UTC", + ) return parser.parse_args() def get_token_limit(plan, blocks=None): """Get token limit based on plan type.""" - if plan == 'custom_max' and blocks: + if plan == "custom_max" and blocks: # Find the highest token count from all previous blocks max_tokens = 0 for block in blocks: - if not block.get('isGap', False) and not block.get('isActive', False): - tokens = block.get('totalTokens', 0) + if not block.get("isGap", False) and not block.get("isActive", False): + tokens = block.get("totalTokens", 0) if tokens > max_tokens: max_tokens = tokens # Return the highest found, or default to pro if none found return max_tokens if max_tokens > 0 else 7000 - - limits = { - 'pro': 7000, - 'max5': 35000, - 'max20': 140000 - } + + limits = {"pro": 7000, "max5": 35000, "max20": 140000} return limits.get(plan, 7000) def main(): """Main monitoring loop.""" args = parse_args() - + # For 'custom_max' plan, we need to get data first to determine the limit - if args.plan == 'custom_max': + if args.plan == "custom_max": initial_data = run_ccusage() - if initial_data and 'blocks' in initial_data: - token_limit = get_token_limit(args.plan, initial_data['blocks']) + if initial_data and "blocks" in initial_data: + token_limit = get_token_limit(args.plan, initial_data["blocks"]) else: - token_limit = get_token_limit('pro') # Fallback to pro + token_limit = get_token_limit("pro") # Fallback to pro else: token_limit = get_token_limit(args.plan) - + try: # Initial screen clear and hide cursor - os.system('clear' if os.name == 'posix' else 'cls') - print('\033[?25l', end='', flush=True) # Hide cursor - + os.system("clear" if os.name == "posix" else "cls") + print("\033[?25l", end="", flush=True) # Hide cursor + while True: # Move cursor to top without clearing - print('\033[H', end='', flush=True) - + print("\033[H", end="", flush=True) + data = run_ccusage() - if not data or 'blocks' not in data: + if not data or "blocks" not in data: print("Failed to get usage data") continue - + # Find the active block active_block = None - for block in data['blocks']: - if block.get('isActive', False): + for block in data["blocks"]: + if block.get("isActive", False): active_block = block break - + if not active_block: print("No active session found") continue - + # Extract data from active block - tokens_used = active_block.get('totalTokens', 0) - + tokens_used = active_block.get("totalTokens", 0) + # Check if tokens exceed limit and switch to custom_max if needed - if tokens_used > token_limit and args.plan == 'pro': + if tokens_used > token_limit and args.plan == "pro": # Auto-switch to custom_max when pro limit is exceeded - new_limit = get_token_limit('custom_max', data['blocks']) + new_limit = get_token_limit("custom_max", data["blocks"]) if new_limit > token_limit: token_limit = new_limit - - usage_percentage = (tokens_used / token_limit) * 100 if token_limit > 0 else 0 + + usage_percentage = ( + (tokens_used / token_limit) * 100 if token_limit > 0 else 0 + ) tokens_left = token_limit - tokens_used - + # Time calculations - start_time_str = active_block.get('startTime') + start_time_str = active_block.get("startTime") if start_time_str: - start_time = datetime.fromisoformat(start_time_str.replace('Z', '+00:00')) + start_time = datetime.fromisoformat( + start_time_str.replace("Z", "+00:00") + ) current_time = datetime.now(start_time.tzinfo) elapsed = current_time - start_time elapsed_minutes = elapsed.total_seconds() / 60 else: elapsed_minutes = 0 - + session_duration = 300 # 5 hours in minutes - remaining_minutes = max(0, session_duration - elapsed_minutes) - + max(0, session_duration - elapsed_minutes) + # Calculate burn rate from ALL sessions in the last hour - burn_rate = calculate_hourly_burn_rate(data['blocks'], current_time) - + burn_rate = calculate_hourly_burn_rate(data["blocks"], current_time) + # Reset time calculation - use fixed schedule or custom hour with timezone - reset_time = get_next_reset_time(current_time, args.reset_hour, args.timezone) - + reset_time = get_next_reset_time( + current_time, args.reset_hour, args.timezone + ) + # Calculate time to reset time_to_reset = reset_time - current_time minutes_to_reset = time_to_reset.total_seconds() / 60 - + # Predicted end calculation - when tokens will run out based on burn rate if burn_rate > 0 and tokens_left > 0: minutes_to_depletion = tokens_left / burn_rate - predicted_end_time = current_time + timedelta(minutes=minutes_to_depletion) + predicted_end_time = current_time + timedelta( + minutes=minutes_to_depletion + ) else: # If no burn rate or tokens already depleted, use reset time predicted_end_time = reset_time - + # Color codes - cyan = '\033[96m' - green = '\033[92m' - blue = '\033[94m' - red = '\033[91m' - yellow = '\033[93m' - white = '\033[97m' - gray = '\033[90m' - reset = '\033[0m' - + cyan = "\033[96m" + red = "\033[91m" + yellow = "\033[93m" + white = "\033[97m" + gray = "\033[90m" + reset = "\033[0m" + # Display header print_header() - + # Token Usage section - print(f"📊 {white}Token Usage:{reset} {create_token_progress_bar(usage_percentage)}") + print( + f"📊 {white}Token Usage:{reset} {create_token_progress_bar(usage_percentage)}" + ) print() - + # Time to Reset section - calculate progress based on time since last reset # Estimate time since last reset (max 5 hours = 300 minutes) time_since_reset = max(0, 300 - minutes_to_reset) - print(f"⏳ {white}Time to Reset:{reset} {create_time_progress_bar(time_since_reset, 300)}") + print( + f"⏳ {white}Time to Reset:{reset} {create_time_progress_bar(time_since_reset, 300)}" + ) print() - + # Detailed stats - print(f"🎯 {white}Tokens:{reset} {white}{tokens_used:,}{reset} / {gray}~{token_limit:,}{reset} ({cyan}{tokens_left:,} left{reset})") - print(f"🔥 {white}Burn Rate:{reset} {yellow}{burn_rate:.1f}{reset} {gray}tokens/min{reset}") + print( + f"🎯 {white}Tokens:{reset} {white}{tokens_used:,}{reset} / {gray}~{token_limit:,}{reset} ({cyan}{tokens_left:,} left{reset})" + ) + print( + f"🔥 {white}Burn Rate:{reset} {yellow}{burn_rate:.1f}{reset} {gray}tokens/min{reset}" + ) print() - + # Predictions - convert to configured timezone for display try: local_tz = pytz.timezone(args.timezone) - except: - local_tz = pytz.timezone('Europe/Warsaw') + except pytz.exceptions.UnknownTimeZoneError: + local_tz = pytz.timezone("Europe/Warsaw") predicted_end_local = predicted_end_time.astimezone(local_tz) reset_time_local = reset_time.astimezone(local_tz) - + predicted_end_str = predicted_end_local.strftime("%H:%M") reset_time_str = reset_time_local.strftime("%H:%M") print(f"🏁 {white}Predicted End:{reset} {predicted_end_str}") print(f"🔄 {white}Token Reset:{reset} {reset_time_str}") print() - + # Show notification if we switched to custom_max show_switch_notification = False - if tokens_used > 7000 and args.plan == 'pro' and token_limit > 7000: + if tokens_used > 7000 and args.plan == "pro" and token_limit > 7000: show_switch_notification = True - + # Notification when tokens exceed max limit show_exceed_notification = tokens_used > token_limit - + # Show notifications if show_switch_notification: - print(f"🔄 {yellow}Tokens exceeded Pro limit - switched to custom_max ({token_limit:,}){reset}") + print( + f"🔄 {yellow}Tokens exceeded Pro limit - switched to custom_max ({token_limit:,}){reset}" + ) print() - + if show_exceed_notification: - print(f"🚨 {red}TOKENS EXCEEDED MAX LIMIT! ({tokens_used:,} > {token_limit:,}){reset}") + print( + f"🚨 {red}TOKENS EXCEEDED MAX LIMIT! ({tokens_used:,} > {token_limit:,}){reset}" + ) print() - + # Warning if tokens will run out before reset if predicted_end_time < reset_time: print(f"⚠️ {red}Tokens will run out BEFORE reset!{reset}") print() - + # Status line current_time_str = datetime.now().strftime("%H:%M:%S") - print(f"⏰ {gray}{current_time_str}{reset} 📝 {cyan}Smooth sailing...{reset} | {gray}Ctrl+C to exit{reset} 🟨") - + print( + f"⏰ {gray}{current_time_str}{reset} 📝 {cyan}Smooth sailing...{reset} | {gray}Ctrl+C to exit{reset} 🟨" + ) + # Clear any remaining lines below to prevent artifacts - print('\033[J', end='', flush=True) - + print("\033[J", end="", flush=True) + time.sleep(3) - + except KeyboardInterrupt: # Show cursor before exiting - print('\033[?25h', end='', flush=True) + print("\033[?25h", end="", flush=True) print(f"\n\n{cyan}Monitoring stopped.{reset}") # Clear the terminal - os.system('clear' if os.name == 'posix' else 'cls') + os.system("clear" if os.name == "posix" else "cls") sys.exit(0) except Exception: # Show cursor on any error - print('\033[?25h', end='', flush=True) + print("\033[?25h", end="", flush=True) raise if __name__ == "__main__": - main() \ No newline at end of file + main() diff --git a/pyproject.toml b/pyproject.toml index d99782f..cd0f29f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -8,7 +8,7 @@ version = "1.0.0" description = "A real-time terminal monitoring tool for Claude AI token usage" readme = "README.md" license = "MIT" -requires-python = ">=3.6" +requires-python = ">=3.7" authors = [ { name = "Maciek", email = "maciek@roboblog.eu" }, ] @@ -20,7 +20,6 @@ classifiers = [ "License :: OSI Approved :: MIT License", "Operating System :: OS Independent", "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3.6", "Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", @@ -34,6 +33,13 @@ dependencies = [ "pytz", ] +[project.optional-dependencies] +dev = [ + "ruff>=0.8.0", + "pre-commit>=2.20.0; python_version<'3.8'", + "pre-commit>=3.0.0; python_version>='3.8'", +] + [project.urls] Homepage = "https://github.com/Maciek-roboblog/Claude-Code-Usage-Monitor" Repository = "https://github.com/Maciek-roboblog/Claude-Code-Usage-Monitor.git" @@ -56,4 +62,4 @@ include = [ "DEVELOPMENT.md", "CONTRIBUTING.md", "TROUBLESHOOTING.md", -] \ No newline at end of file +]