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/.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/.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/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..dff535d --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,196 @@ +# 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 + +### 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 + +### Setup and Installation + +#### Modern Installation with uv (Recommended) +```bash +# Install global dependency +npm install -g ccusage + +# 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 +# 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 +``` + +### 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 + +### 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 + +## 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 +- PyPI package distribution +- Docker containerization with web dashboard +- Enhanced analytics and prediction features \ No newline at end of file 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 3bb2b14..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) @@ -49,9 +49,46 @@ 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 + +# 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 +# or +claude-monitor +``` + +### 🔧 Legacy Installation Methods + +#### Quick Start (Development/Testing) + +For immediate testing or development: ```bash # Install dependencies @@ -64,14 +101,12 @@ cd Claude-Code-Usage-Monitor python ccusage_monitor.py ``` -### 🔒 Production Setup (Recommended) - #### 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 +#### Virtual Environment Setup #### Why Use Virtual Environment? @@ -182,6 +217,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 +241,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 +273,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 +295,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 @@ -295,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) ``` @@ -304,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 @@ -390,10 +479,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 +495,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 +512,7 @@ The auto-detection system: ```bash # Just run it with defaults -./ccusage_monitor.py +ccusage-monitor # Press Ctrl+C after checking status ``` @@ -435,7 +524,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 +533,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 +551,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 +587,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 +596,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,9 +611,12 @@ The auto-detection system: 2. **Workflow Integration** ```bash - # Start monitoring with your development session - tmux new-session -d -s claude-monitor './ccusage_monitor.py' + # 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 tmux attach -t claude-monitor ``` @@ -537,7 +631,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 +644,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 ``` --- @@ -597,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 0000000..1d58928 Binary files /dev/null and b/__pycache__/ccusage_monitor.cpython-311.pyc differ 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 new file mode 100644 index 0000000..cd0f29f --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,65 @@ +[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.7" +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.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.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" +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", +]