Showing preview only (793K chars total). Download the full file or copy to clipboard to get everything.
Repository: Maciek-roboblog/Claude-Code-Usage-Monitor
Branch: main
Commit: 06f0fe11e694
Files: 77
Total size: 761.9 KB
Directory structure:
gitextract_6dymq9k3/
├── .gitattributes
├── .github/
│ ├── FUNDING.yml
│ └── workflows/
│ ├── lint.yml
│ ├── release.yml
│ ├── test.yml
│ └── version-bump.yml
├── .gitignore
├── .pre-commit-config.yaml
├── CHANGELOG.md
├── CONTRIBUTING.md
├── DEVELOPMENT.md
├── LICENSE
├── README.md
├── RELEASE.md
├── TROUBLESHOOTING.md
├── VERSION_MANAGEMENT.md
├── pyproject.toml
└── src/
├── claude_monitor/
│ ├── __init__.py
│ ├── __main__.py
│ ├── _version.py
│ ├── cli/
│ │ ├── __init__.py
│ │ ├── bootstrap.py
│ │ └── main.py
│ ├── core/
│ │ ├── __init__.py
│ │ ├── calculations.py
│ │ ├── data_processors.py
│ │ ├── models.py
│ │ ├── p90_calculator.py
│ │ ├── plans.py
│ │ ├── pricing.py
│ │ └── settings.py
│ ├── data/
│ │ ├── __init__.py
│ │ ├── aggregator.py
│ │ ├── analysis.py
│ │ ├── analyzer.py
│ │ └── reader.py
│ ├── error_handling.py
│ ├── monitoring/
│ │ ├── __init__.py
│ │ ├── data_manager.py
│ │ ├── orchestrator.py
│ │ └── session_monitor.py
│ ├── terminal/
│ │ ├── __init__.py
│ │ ├── manager.py
│ │ └── themes.py
│ ├── ui/
│ │ ├── __init__.py
│ │ ├── components.py
│ │ ├── display_controller.py
│ │ ├── layouts.py
│ │ ├── progress_bars.py
│ │ ├── session_display.py
│ │ └── table_views.py
│ └── utils/
│ ├── __init__.py
│ ├── formatting.py
│ ├── model_utils.py
│ ├── notifications.py
│ ├── time_utils.py
│ └── timezone.py
└── tests/
├── __init__.py
├── conftest.py
├── examples/
│ └── api_examples.py
├── run_tests.py
├── test_aggregator.py
├── test_analysis.py
├── test_calculations.py
├── test_cli_main.py
├── test_data_reader.py
├── test_display_controller.py
├── test_error_handling.py
├── test_formatting.py
├── test_monitoring_orchestrator.py
├── test_pricing.py
├── test_session_analyzer.py
├── test_settings.py
├── test_table_views.py
├── test_time_utils.py
├── test_timezone.py
└── test_version.py
================================================
FILE CONTENTS
================================================
================================================
FILE: .gitattributes
================================================
# Auto detect text files and perform LF normalization
* text=auto
================================================
FILE: .github/FUNDING.yml
================================================
github: [Maciek-roboblog]
buy_me_a_coffee: maciekroboblog
thanks_dev: u/gh/maciek-roboblog
================================================
FILE: .github/workflows/lint.yml
================================================
name: Lint
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
ruff:
runs-on: ubuntu-latest
name: Lint with Ruff
strategy:
matrix:
python-version: ["3.9", "3.10", "3.11", "3.12", "3.13"]
steps:
- uses: actions/checkout@v4
- name: Install uv
uses: astral-sh/setup-uv@v4
with:
version: "latest"
- name: Set up Python ${{ matrix.python-version }}
run: uv python install ${{ matrix.python-version }}
- 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
strategy:
matrix:
python-version: ["3.9", "3.10", "3.11", "3.12", "3.13"]
steps:
- uses: actions/checkout@v4
- name: Install uv
uses: astral-sh/setup-uv@v4
with:
version: "latest"
- name: Set up Python ${{ matrix.python-version }}
run: uv python install ${{ matrix.python-version }}
- name: Install pre-commit
run: uv tool install pre-commit --with pre-commit-uv
- name: Run pre-commit
run: |
# Run pre-commit and check if any files would be modified
uv tool run pre-commit run --all-files --show-diff-on-failure || (
echo "Pre-commit hooks would modify files. Please run 'pre-commit run --all-files' locally and commit the changes."
exit 1
)
================================================
FILE: .github/workflows/release.yml
================================================
name: Release
on:
push:
branches: [main]
workflow_dispatch:
jobs:
check-version:
runs-on: ubuntu-latest
outputs:
should_release: ${{ steps.check.outputs.should_release }}
version: ${{ steps.extract.outputs.version }}
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Extract version from pyproject.toml
id: extract
run: |
VERSION=$(grep '^version = ' pyproject.toml | sed 's/version = "\(.*\)"/\1/')
echo "version=$VERSION" >> $GITHUB_OUTPUT
echo "Version: $VERSION"
- name: Check if tag exists
id: check
run: |
VERSION="${{ steps.extract.outputs.version }}"
if git rev-parse "v$VERSION" >/dev/null 2>&1; then
echo "Tag v$VERSION already exists"
echo "should_release=false" >> $GITHUB_OUTPUT
else
echo "Tag v$VERSION does not exist"
echo "should_release=true" >> $GITHUB_OUTPUT
fi
release:
needs: check-version
if: needs.check-version.outputs.should_release == 'true'
runs-on: ubuntu-latest
permissions:
contents: write
id-token: write # For trusted PyPI publishing
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Install uv
uses: astral-sh/setup-uv@v4
with:
version: "latest"
- name: Set up Python
run: uv python install
- name: Extract changelog for version
id: changelog
run: |
VERSION="${{ needs.check-version.outputs.version }}"
echo "Extracting changelog for version $VERSION"
# Extract the changelog section for this version using sed
sed -n "/^## \\[$VERSION\\]/,/^## \\[/{/^## \\[$VERSION\\]/d; /^## \\[/q; /^$/d; p}" CHANGELOG.md > release_notes.md
# If no changelog found, create a simple message
if [ ! -s release_notes.md ]; then
echo "No specific changelog found for version $VERSION" > release_notes.md
fi
echo "Release notes:"
cat release_notes.md
- name: Create git tag
run: |
VERSION="${{ needs.check-version.outputs.version }}"
git config user.name "maciekdymarczyk"
git config user.email "maciek@roboblog.eu"
git tag -a "v$VERSION" -m "Release v$VERSION"
git push origin "v$VERSION"
- name: Create GitHub Release
uses: softprops/action-gh-release@v2
with:
tag_name: v${{ needs.check-version.outputs.version }}
name: Release v${{ needs.check-version.outputs.version }}
body_path: release_notes.md
draft: false
prerelease: false
- name: Build package
run: |
uv build
ls -la dist/
- name: Publish to PyPI
uses: pypa/gh-action-pypi-publish@release/v1
with:
skip-existing: true
notify-success:
needs: [check-version, release]
if: needs.check-version.outputs.should_release == 'true' && success()
runs-on: ubuntu-latest
steps:
- name: Success notification
run: |
echo "🎉 Successfully released v${{ needs.check-version.outputs.version }}!"
echo "- GitHub Release: https://github.com/${{ github.repository }}/releases/tag/v${{ needs.check-version.outputs.version }}"
echo "- PyPI: https://pypi.org/project/claude-monitor/${{ needs.check-version.outputs.version }}/"
================================================
FILE: .github/workflows/test.yml
================================================
name: Test Suite
on:
push:
branches: [main, develop]
pull_request:
branches: [main, develop]
jobs:
test:
runs-on: ${{ matrix.os }}
name: Test on Python ${{ matrix.python-version }} (${{ matrix.os }})
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest, macos-latest]
# os: [ubuntu-latest, windows-latest, macos-latest]
python-version: ["3.9", "3.10", "3.11", "3.12", "3.13"]
steps:
- uses: actions/checkout@v4
- name: Install uv
uses: astral-sh/setup-uv@v4
with:
version: "latest"
- name: Set up Python ${{ matrix.python-version }}
run: uv python install ${{ matrix.python-version }}
- name: Install dependencies
run: uv sync --extra test --extra dev
- name: Run unit tests
run: uv run pytest src/tests/ -v --tb=short --cov=claude_monitor --cov-report=xml --cov-report=term-missing
- name: Upload coverage reports to Codecov
if: matrix.os == 'ubuntu-latest' && matrix.python-version == '3.11'
uses: codecov/codecov-action@v5
with:
token: ${{ secrets.CODECOV_TOKEN }}
# security:
# runs-on: ubuntu-latest
# name: Security scanning
# strategy:
# matrix:
# python-version: ["3.11"]
#
# steps:
# - uses: actions/checkout@v4
#
# - name: Install uv
# uses: astral-sh/setup-uv@v4
# with:
# version: "latest"
#
# - name: Set up Python ${{ matrix.python-version }}
# run: uv python install ${{ matrix.python-version }}
#
# - name: Install dependencies
# run: uv sync --extra security --extra dev
#
# - name: Run Bandit security linter
# run: uv run bandit -r src/claude_monitor -f json -o bandit-report.json
#
# - name: Run Safety dependency scanner
# run: uv run safety check --json --output safety-report.json || true
#
# - name: Upload security artifacts
# uses: actions/upload-artifact@v4
# if: always()
# with:
# name: security-reports
# path: |
# bandit-report.json
# safety-report.json
# performance:
# runs-on: ubuntu-latest
# name: Performance benchmarks
# strategy:
# matrix:
# python-version: ["3.11"]
#
# steps:
# - uses: actions/checkout@v4
#
# - name: Install uv
# uses: astral-sh/setup-uv@v4
# with:
# version: "latest"
#
# - name: Set up Python ${{ matrix.python-version }}
# run: uv python install ${{ matrix.python-version }}
#
# - name: Install dependencies
# run: uv sync --extra performance --extra dev
#
# - name: Run performance benchmarks
# run: uv run pytest src/tests/ -m benchmark --benchmark-json=benchmark-results.json
#
# - name: Upload benchmark results
# uses: actions/upload-artifact@v4
# if: always()
# with:
# name: benchmark-results
# path: benchmark-results.json
================================================
FILE: .github/workflows/version-bump.yml
================================================
name: Version Bump Helper
on:
workflow_dispatch:
inputs:
bump_type:
description: 'Version bump type'
required: true
default: 'patch'
type: choice
options:
- patch
- minor
- major
changelog_entry:
description: 'Changelog entry (brief description of changes)'
required: true
type: string
jobs:
bump-version:
runs-on: ubuntu-latest
permissions:
contents: write
pull-requests: write
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
token: ${{ secrets.GITHUB_TOKEN }}
- name: Install uv
uses: astral-sh/setup-uv@v4
with:
version: "latest"
- name: Set up Python
run: uv python install
- name: Extract current version
id: current
run: |
CURRENT_VERSION=$(grep '^version = ' pyproject.toml | sed 's/version = "\(.*\)"/\1/')
echo "version=$CURRENT_VERSION" >> $GITHUB_OUTPUT
echo "Current version: $CURRENT_VERSION"
- name: Calculate new version
id: new
run: |
CURRENT="${{ steps.current.outputs.version }}"
BUMP_TYPE="${{ github.event.inputs.bump_type }}"
# Split version into components
IFS='.' read -r MAJOR MINOR PATCH <<< "$CURRENT"
# Bump according to type
case "$BUMP_TYPE" in
major)
MAJOR=$((MAJOR + 1))
MINOR=0
PATCH=0
;;
minor)
MINOR=$((MINOR + 1))
PATCH=0
;;
patch)
PATCH=$((PATCH + 1))
;;
esac
NEW_VERSION="$MAJOR.$MINOR.$PATCH"
echo "version=$NEW_VERSION" >> $GITHUB_OUTPUT
echo "New version: $NEW_VERSION"
- name: Update pyproject.toml
run: |
NEW_VERSION="${{ steps.new.outputs.version }}"
sed -i "s/^version = .*/version = \"$NEW_VERSION\"/" pyproject.toml
echo "Updated pyproject.toml to version $NEW_VERSION"
- name: Update CHANGELOG.md
run: |
NEW_VERSION="${{ steps.new.outputs.version }}"
TODAY=$(date +%Y-%m-%d)
CHANGELOG_ENTRY="${{ github.event.inputs.changelog_entry }}"
# Create new changelog section
echo "## [$NEW_VERSION] - $TODAY" > changelog_new.md
echo "" >> changelog_new.md
echo "### Changed" >> changelog_new.md
echo "- $CHANGELOG_ENTRY" >> changelog_new.md
echo "" >> changelog_new.md
# Find the line number where we should insert (after the # Changelog header)
LINE_NUM=$(grep -n "^# Changelog" CHANGELOG.md | head -1 | cut -d: -f1)
if [ -n "$LINE_NUM" ]; then
# Insert after the Changelog header and empty line
head -n $((LINE_NUM + 1)) CHANGELOG.md > changelog_temp.md
cat changelog_new.md >> changelog_temp.md
tail -n +$((LINE_NUM + 2)) CHANGELOG.md >> changelog_temp.md
mv changelog_temp.md CHANGELOG.md
else
# If no header found, prepend to file
cat changelog_new.md CHANGELOG.md > changelog_temp.md
mv changelog_temp.md CHANGELOG.md
fi
# Add the version link at the bottom
echo "" >> CHANGELOG.md
echo "[$NEW_VERSION]: https://github.com/Maciek-roboblog/Claude-Code-Usage-Monitor/releases/tag/v$NEW_VERSION" >> CHANGELOG.md
echo "Updated CHANGELOG.md with new version entry"
- name: Create Pull Request
uses: peter-evans/create-pull-request@v6
with:
token: ${{ secrets.GITHUB_TOKEN }}
commit-message: "Bump version to ${{ steps.new.outputs.version }}"
title: "chore: bump version to ${{ steps.new.outputs.version }}"
body: |
## Version Bump: ${{ steps.current.outputs.version }} → ${{ steps.new.outputs.version }}
**Bump Type**: ${{ github.event.inputs.bump_type }}
**Changes**: ${{ github.event.inputs.changelog_entry }}
This PR was automatically created by the Version Bump workflow.
### Checklist
- [ ] Review the version bump in `pyproject.toml`
- [ ] Review the changelog entry in `CHANGELOG.md`
- [ ] Merge this PR to trigger the release workflow
branch: version-bump-${{ steps.new.outputs.version }}
delete-branch: true
labels: |
version-bump
automated
================================================
FILE: .gitignore
================================================
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
*$py.class
# C extensions
*.so
MAIN_INSTRUCTION.md
.TASKS
# 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/ - allowing settings.json for team consistency
# 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
/.claude/
/ULTRATHINK_COMPLETE_GUIDE.md
/SLASH_COMMANDS.md
/optimize_tokens.sh
/MAIN_INSTRUCTION.md
/CLAUDE_SYSTEM_PROMPT.md
/claude_optimize.py
/CLAUDE.md
/src/Claude-Code-Usage-Monitor_Features_Missing_in_claude_monitor.md
/src/Functionality_Coverage_Claude-Code-Usage-Monitor_Missing_From_claude_monitor.md
/TODO.md
*Zone.Identifier
# Local linting scripts
lint*.py
lint*.sh
================================================
FILE: .pre-commit-config.yaml
================================================
# .pre-commit-config.yaml
repos:
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.12.3
hooks:
# Lint-only pass (no auto-fix)
- id: ruff
# Formatting pass (auto-fix)
- 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']
================================================
FILE: CHANGELOG.md
================================================
# Changelog
## [3.1.0] - 2025-07-23
### 🆕 New Features
- **📊 Usage Analysis Views**: Added `--view` parameter for different time aggregation periods
- `--view realtime` (default): Live monitoring with real-time updates
- `--view daily`: Daily token usage aggregated in comprehensive table format
- `--view monthly`: Monthly token usage aggregated for long-term trend analysis
### 📝 Use Cases
- **Daily Analysis**: Track daily usage patterns and identify peak consumption periods
- **Monthly Planning**: Long-term budget analysis and trend identification
- **Usage Optimization**: Historical data analysis for better resource planning
## [3.0.0] - 2025-01-13
### 🚨 Breaking Changes
- **Package Name Change**: Renamed from `claude-usage-monitor` to `claude-monitor`
- New installation: `pip install claude-monitor` or `uv tool install claude-monitor`
- New command aliases: `claude-monitor` and `cmonitor`
- **Python Requirement**: Minimum Python version raised from 3.8 to 3.9
- **Architecture Overhaul**: Complete rewrite from single-file to modular package structure
- **Entry Point Changes**: Module execution now via `claude_monitor.__main__:main`
### 🏗️ Complete Architectural Restructuring
- **📁 Professional Package Layout**: Migrated to `src/claude_monitor/` structure with proper namespace isolation
- Replaced single `claude_monitor.py` file with comprehensive modular architecture
- Implemented clean separation of concerns across 8 specialized modules
- **🔧 Modular Design**: New package organization:
- `cli/` - Command-line interface and bootstrap logic
- `core/` - Business logic, models, settings, calculations, and pricing
- `data/` - Data management, analysis, and reading utilities
- `monitoring/` - Real-time session monitoring and orchestration
- `ui/` - User interface components, layouts, and display controllers
- `terminal/` - Terminal management and theme handling
- `utils/` - Formatting, notifications, timezone, and model utilities
- **⚡ Enhanced Performance**: Optimized data processing with caching, threading, and efficient session management
### 🎨 Rich Terminal UI System
- **💫 Rich Integration**: Complete UI overhaul using Rich library for professional terminal interface
- Advanced progress bars with semantic color coding (🟢🟡🔴)
- Responsive layouts with proper terminal width handling (80+ characters required)
- Enhanced typography and visual hierarchy
- **🌈 Improved Theme System**: Enhanced automatic theme detection with better contrast ratios
- **📊 Advanced Display Components**: New progress visualization with burn rate indicators and time-based metrics
### 🔒 Type Safety and Validation
- **🛡️ Pydantic Integration**: Complete type safety implementation
- Comprehensive settings validation with user-friendly error messages
- Type-safe data models (`UsageEntry`, `SessionBlock`, `TokenCounts`)
- CLI parameter validation with detailed feedback
- **⚙️ Smart Configuration**: Pydantic-based settings with last-used parameter persistence
- **🔍 Enhanced Error Handling**: Centralized error management with optional Sentry integration
### 📈 Advanced Analytics Features
- **🧮 P90 Percentile Calculations**: Machine learning-inspired usage prediction and limit detection
- **📊 Smart Plan Detection**: Auto-detection of Claude plan limits with custom plan support
- **⏱️ Real-time Monitoring**: Enhanced session tracking with threading and callback systems
- **💡 Intelligent Insights**: Advanced burn rate calculations and velocity indicators
### 🔧 Developer Experience Improvements
- **🚀 Modern Build System**: Migrated from Hatchling to Setuptools with src layout
- **🧪 Comprehensive Testing**: Professional test infrastructure with pytest and coverage reporting
- **📝 Enhanced Documentation**: Updated troubleshooting guide with v3.0.0-specific solutions
- **🔄 CI/CD Reactivation**: Restored and enhanced GitHub Actions workflows:
- Multi-Python version testing (3.9-3.12)
- Automated linting with Ruff
- Trusted PyPI publishing with OIDC
- Automated version bumping and changelog management
### 📦 Dependency and Packaging Updates
- **🆕 Core Dependencies Added**:
- `pydantic>=2.0.0` & `pydantic-settings>=2.0.0` - Type validation and settings
- `numpy>=1.21.0` - Advanced calculations
- `sentry-sdk>=1.40.0` - Optional error tracking
- `pyyaml>=6.0` - Configuration file support
- **⬆️ Dependency Upgrades**:
- `rich`: `>=13.0.0` → `>=13.7.0` - Enhanced UI features
- `pytz`: No constraint → `>=2023.3` - Improved timezone handling
- **🛠️ Development Tools**: Expanded with MyPy, Bandit, testing frameworks, and documentation tools
### 🎯 Enhanced User Features
- **🎛️ Flexible Configuration**: Support for auto-detection, manual overrides, and persistent settings
- **🌍 Improved Timezone Handling**: Enhanced timezone detection and validation
- **⚡ Performance Optimizations**: Faster startup times and reduced memory usage
- **🔔 Smart Notifications**: Enhanced feedback system with contextual messaging
### 🔧 Installation and Compatibility
- **📋 Installation Method Updates**: Full support for `uv`, `pipx`, and traditional pip installation
- **🐧 Platform Compatibility**: Enhanced support for modern Linux distributions with externally-managed environments
- **🛣️ Migration Path**: Automatic handling of legacy configurations and smooth upgrade experience
### 📚 Technical Implementation Details
- **🏢 Professional Architecture**: Implementation of SOLID principles with single responsibility modules
- **🔄 Async-Ready Design**: Threading infrastructure for real-time monitoring capabilities
- **💾 Efficient Data Handling**: Optimized JSONL parsing with error resilience
- **🔐 Security Enhancements**: Secure configuration handling and optional telemetry integration
## [2.0.0] - 2025-06-25
### Added
- **🎨 Smart Theme System**: Automatic light/dark theme detection for optimal terminal appearance
- Intelligent theme detection based on terminal environment, system settings, and background color
- Manual theme override options: `--theme light`, `--theme dark`, `--theme auto`
- Theme debug mode: `--theme-debug` for troubleshooting theme detection
- Platform-specific theme detection (macOS, Windows, Linux)
- Support for VSCode integrated terminal, iTerm2, Windows Terminal
- **📊 Enhanced Progress Bar Colors**: Improved visual feedback with smart color coding
- Token usage progress bars with three-tier color system:
- 🟢 Green (0-49%): Safe usage level
- 🟡 Yellow (50-89%): Warning - approaching limit
- 🔴 Red (90-100%): Critical - near or at limit
- Time progress bars with consistent blue indicators
- Burn rate velocity indicators with emoji feedback (🐌➡️🚀⚡)
- **🌈 Rich Theme Support**: Optimized color schemes for both light and dark terminals
- Dark theme: Bright colors optimized for dark backgrounds
- Light theme: Darker colors optimized for light backgrounds
- Automatic terminal capability detection (truecolor, 256-color, 8-color)
- **🔧 Advanced Terminal Detection**: Comprehensive environment analysis
- COLORTERM, TERM_PROGRAM, COLORFGBG environment variable support
- Terminal background color querying using OSC escape sequences
- Cross-platform system theme integration
### Changed
- **Breaking**: Progress bar color logic now uses semantic color names (`cost.low`, `cost.medium`, `cost.high`)
- Enhanced visual consistency across different terminal environments
- Improved accessibility with better contrast ratios in both themes
### Technical Details
- New `usage_analyzer/themes/` module with theme detection and color management
- `ThemeDetector` class with multi-method theme detection algorithm
- Rich theme integration with automatic console configuration
- Environment-aware color selection for maximum compatibility
## [1.0.19] - 2025-06-23
### Fixed
- Fixed timezone handling by locking calculation to Europe/Warsaw timezone
- Separated display timezone from reset time calculation for improved reliability
- Removed dynamic timezone input and related error handling to simplify reset time logic
## [1.0.17] - 2025-06-23
### Added
- Loading screen that displays immediately on startup to eliminate "black screen" experience
- Visual feedback with header and "Fetching Claude usage data..." message during initial data load
## [1.0.16] - 2025-06-23
### Fixed
- Fixed UnboundLocalError when Ctrl+C is pressed by initializing color variables at the start of main()
- Fixed ccusage command hanging indefinitely by adding 30-second timeout to subprocess calls
- Added ccusage availability check at startup with helpful error messages
- Improved error display when ccusage fails with better debugging information
- Fixed npm 7+ compatibility issue where npx doesn't find globally installed packages
### Added
- Timeout handling for all ccusage subprocess calls to prevent hanging
- Pre-flight check for ccusage availability before entering main loop
- More informative error messages suggesting installation steps and login requirements
- Dual command execution: tries direct `ccusage` command first, then falls back to `npx ccusage`
- Detection and reporting of which method (direct or npx) is being used
## [1.0.11] - 2025-06-22
### Changed
- Replaced `init_dependency.py` with simpler `check_dependency.py` module
- Refactored dependency checking to use separate `test_node()` and `test_npx()` functions
- Removed automatic Node.js installation functionality in favor of explicit dependency checking
- Updated package includes in `pyproject.toml` to reference new dependency module
### Fixed
- Simplified dependency handling by removing complex installation logic
- Improved error messages for missing Node.js or npx dependencies
## [1.0.8] - 2025-06-21
### Added
- Automatic Node.js installation support
## [1.0.7] - 2025-06-21
### Changed
- Enhanced `init_dependency.py` module with improved documentation and error handling
- Added automatic `npx` installation if not available
- Improved cross-platform Node.js installation logic
- Better error messages throughout the dependency initialization process
## [1.0.6] - 2025-06-21
### Added
- Modern Python packaging with `pyproject.toml` and hatchling build system
- Automatic Node.js installation via `init_dependency.py` module
- Terminal handling improvements with input flushing and proper cleanup
- GitHub Actions workflow for automated code quality checks
- Pre-commit hooks configuration with Ruff linter and formatter
- VS Code settings for consistent development experience
- CLAUDE.md documentation for Claude Code AI assistant integration
- Support for `uv` tool as recommended installation method
- Console script entry point `claude-monitor` for system-wide usage
- Comprehensive .gitignore for Python projects
- CHANGELOG.md for tracking project history
### Changed
- Renamed main script from `ccusage_monitor.py` to `claude_monitor.py`
- Use `npx ccusage` instead of direct `ccusage` command for better compatibility
- Improved terminal handling to prevent input corruption during monitoring
- Updated all documentation files (README, CONTRIBUTING, DEVELOPMENT, TROUBLESHOOTING)
- Enhanced project structure for PyPI packaging readiness
### Fixed
- Terminal input corruption when typing during monitoring
- Proper Ctrl+C handling with cursor restoration
- Terminal settings restoration on exit
[3.0.0]: https://github.com/Maciek-roboblog/Claude-Code-Usage-Monitor/releases/tag/v3.0.0
[2.0.0]: https://github.com/Maciek-roboblog/Claude-Code-Usage-Monitor/releases/tag/v2.0.0
[1.0.19]: https://github.com/Maciek-roboblog/Claude-Code-Usage-Monitor/releases/tag/v1.0.19
[1.0.17]: https://github.com/Maciek-roboblog/Claude-Code-Usage-Monitor/releases/tag/v1.0.17
[1.0.16]: https://github.com/Maciek-roboblog/Claude-Code-Usage-Monitor/releases/tag/v1.0.16
[1.0.11]: https://github.com/Maciek-roboblog/Claude-Code-Usage-Monitor/releases/tag/v1.0.11
[1.0.8]: https://github.com/Maciek-roboblog/Claude-Code-Usage-Monitor/releases/tag/v1.0.8
[1.0.7]: https://github.com/Maciek-roboblog/Claude-Code-Usage-Monitor/releases/tag/v1.0.7
[1.0.6]: https://github.com/Maciek-roboblog/Claude-Code-Usage-Monitor/releases/tag/v1.0.6
================================================
FILE: CONTRIBUTING.md
================================================
# 🤝 Contributing Guide
Welcome to the Claude Code Usage Monitor project! We're excited to have you contribute to making this tool better for everyone.
---
## 🌟 How to Contribute
### 🎯 Types of Contributions
We welcome all kinds of contributions:
- **🐛 Bug Reports**: Found something broken? Let us know!
- **💡 Feature Requests**: Have an idea for improvement?
- **📝 Documentation**: Help improve guides and examples
- **🔧 Code Contributions**: Fix bugs or implement new features
- **🧪 Testing**: Help test on different platforms
- **🎨 UI/UX**: Improve the visual design and user experience
- **🧠 ML Research**: Contribute to machine learning features
- **📦 Packaging**: Help with PyPI, Docker, or distribution
---
## 🚀 Quick Start for Contributors
### 1. Fork and Clone
```bash
# Fork the repository on GitHub
# Then clone your fork
git clone https://github.com/YOUR-USERNAME/Claude-Code-Usage-Monitor.git
cd Claude-Code-Usage-Monitor
```
### 2. Set Up Development Environment
```bash
# Create virtual environment
python3 -m venv venv
source venv/bin/activate # Linux/Mac
# venv\Scripts\activate # Windows
# Install project and development dependencies
pip install -e .[dev]
# Make script executable (Linux/Mac)
chmod +x claude_monitor.py
```
### 3. Create a Feature Branch
```bash
# Create and switch to feature branch
git checkout -b feature/your-feature-name
# Or for bug fixes
git checkout -b fix/bug-description
```
### 4. Make Your Changes
- Follow our coding standards (see below)
- Add tests for new functionality
- Update documentation if needed
- Test your changes thoroughly
### 5. Submit Your Contribution
```bash
# Add and commit your changes
git add .
git commit -m "Add: Brief description of your change"
# Push to your fork
git push origin feature/your-feature-name
# Open a Pull Request on GitHub
```
---
## 📋 Development Guidelines
### 🐍 Python Code Style
We follow **PEP 8** with these specific guidelines:
```python
# Good: Clear variable names
current_token_count = 1500
session_start_time = datetime.now()
# Bad: Unclear abbreviations
curr_tok_cnt = 1500
sess_st_tm = datetime.now()
# Good: Descriptive function names
def calculate_burn_rate(tokens_used, time_elapsed):
return tokens_used / time_elapsed
# Good: Clear comments for complex logic
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
"""
pass
```
### 🧪 Testing Guidelines
```python
# Test file naming: test_*.py
# tests/test_core.py
import pytest
from claude_monitor.core import TokenMonitor
def test_token_calculation():
"""Test token usage calculation."""
monitor = TokenMonitor()
result = monitor.calculate_usage(1000, 500)
assert result == 50.0 # 50% usage
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
```
### 📝 Commit Message Format
Use clear, descriptive commit messages:
```bash
# Good commit messages
git commit -m "Add: ML-powered token prediction algorithm"
git commit -m "Fix: Handle edge case when no sessions are active"
git commit -m "Update: Improve error handling in ccusage integration"
git commit -m "Docs: Add examples for timezone configuration"
# Prefixes to use:
# Add: New features
# Fix: Bug fixes
# Update: Improvements to existing features
# Docs: Documentation changes
# Test: Test additions or changes
# Refactor: Code refactoring
# Style: Code style changes
```
## 🎯 Contribution Areas (Priority things)
### 📦 PyPI Package Development
**Current Needs**:
- Create proper package structure
- Configure setup.py and requirements
- Implement global configuration system
- Add command-line entry points
**Skills Helpful**:
- Python packaging (setuptools, wheel)
- Configuration management
- Cross-platform compatibility
- Command-line interface design
**Getting Started**:
1. Study existing PyPI packages for examples
2. Create basic package structure
3. Test installation in virtual environments
4. Implement configuration file handling
### 🐳 Docker & Web Features
**Current Needs**:
- Create efficient Dockerfile
- Build web dashboard interface
- Implement REST API
- Design responsive UI
**Skills Helpful**:
- Docker containerization
- React/TypeScript for frontend
- Python web frameworks (Flask/FastAPI)
- Responsive web design
**Getting Started**:
1. Create basic Dockerfile for current script
2. Design web interface mockups
3. Implement simple REST API
4. Build responsive dashboard components
### 🔧 Core Features & Bug Fixes
**Current Needs**:
- Improve error handling
- Add more configuration options
- Optimize performance
- Fix cross-platform issues
**Skills Helpful**:
- Python development
- Terminal/console applications
- Cross-platform compatibility
- Performance optimization
**Getting Started**:
1. Run the monitor on different platforms
2. Identify and fix platform-specific issues
3. Improve error messages and handling
4. Add new configuration options
---
## 🐛 Bug Reports
### 📋 Before Submitting a Bug Report
1. **Check existing issues**: Search GitHub issues for similar problems
2. **Update to latest version**: Ensure you're using the latest code
3. **Test in clean environment**: Try in fresh virtual environment
4. **Gather information**: Collect system details and error messages
### 📝 Bug Report Template
```markdown
**Bug Description**
A clear description of what the bug is.
**To Reproduce**
Steps to reproduce the behavior:
1. Run command '...'
2. Configure with '...'
3. See error
**Expected Behavior**
What you expected to happen.
**Actual Behavior**
What actually happened.
**Environment**
- OS: [e.g. Ubuntu 20.04, Windows 11, macOS 12]
- Python version: [e.g. 3.9.7]
- ccusage version: [run: ccusage --version]
- Monitor version: [git commit hash]
**Error Output**
```
Paste full error messages here
```
**Additional Context**
Add any other context about the problem here.
```
---
## 💡 Feature Requests
### 🎯 Feature Request Template
```markdown
**Feature Description**
A clear description of the feature you'd like to see.
**Problem Statement**
What problem does this feature solve?
**Proposed Solution**
How do you envision this feature working?
**Alternative Solutions**
Any alternative approaches you've considered.
**Use Cases**
Specific scenarios where this feature would be helpful.
**Implementation Ideas**
Any ideas about how this could be implemented (optional).
```
### 🔍 Feature Evaluation Criteria
We evaluate features based on:
1. **User Value**: How many users would benefit?
2. **Complexity**: Implementation effort required
3. **Maintenance**: Long-term maintenance burden
4. **Compatibility**: Impact on existing functionality
5. **Performance**: Effect on monitor performance
6. **Dependencies**: Additional dependencies required
---
## 🧪 Testing Contributions
### 🔧 Running Tests
```bash
# Run all tests
pytest
# Run specific test file
pytest tests/test_core.py
# Run with coverage
pytest --cov=claude_monitor
# Run tests on multiple Python versions (if using tox)
tox
```
### 📊 Test Coverage
We aim for high test coverage:
- **Core functionality**: 95%+ coverage
- **ML components**: 90%+ coverage
- **UI components**: 80%+ coverage
- **Utility functions**: 95%+ coverage
### 🌍 Platform Testing
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.7, 3.8, 3.9, 3.10, 3.11, 3.12
---
## 📝 Documentation Contributions
### 📚 Documentation Areas
- **README improvements**: Make getting started easier
- **Code comments**: Explain complex algorithms
- **Usage examples**: Real-world scenarios
- **API documentation**: Function and class documentation
- **Troubleshooting guides**: Common problems and solutions
### ✍️ Writing Guidelines
- **Be clear and concise**: Avoid jargon when possible
- **Use examples**: Show don't just tell
- **Consider all users**: From beginners to advanced
- **Keep it updated**: Ensure examples work with current code
- **Use consistent formatting**: Follow existing style
---
## 📊 Data Collection for Improvement
### 🔍 Help Us Improve Token Limit Detection
We're collecting **anonymized data** about token limits to improve auto-detection:
**What to share in [Issue #1](https://github.com/Maciek-roboblog/Claude-Code-Usage-Monitor/issues/1)**:
- Your subscription type (Pro, Teams, Enterprise)
- Maximum tokens reached (custom_max value)
- When the limit was exceeded
- Usage patterns you've noticed
**Privacy**: Only share what you're comfortable with. No personal information needed.
### 📈 Usage Pattern Research
Help us understand usage patterns:
- Peak usage times
- Session duration preferences
- Token consumption patterns
- Feature usage statistics
This helps prioritize development and improve user experience.
## 🏆 Recognition
### 📸 Contributor Spotlight
Outstanding contributors will be featured:
- **README acknowledgments**: Credit for major contributions
- **Release notes**: Mention significant contributions
- **Social media**: Share contributor achievements
- **Reference letters**: Happy to provide references for good contributors
### 🎖️ Contribution Levels
- **🌟 First Contribution**: Welcome to the community!
- **🔧 Regular Contributor**: Multiple merged PRs
- **🚀 Core Contributor**: Significant feature development
- **👑 Maintainer**: Ongoing project stewardship
## ❓ Getting Help
### 💬 Where to Ask Questions
1. **GitHub Issues**: For bug reports and feature requests
2. **GitHub Discussions**: For general questions and ideas
3. **Email**: [maciek@roboblog.eu](mailto:maciek@roboblog.eu) for direct contact
4. **Code Review**: Ask questions in PR comments
### 📚 Resources
- **[DEVELOPMENT.md](DEVELOPMENT.md)**: Development roadmap
- **[README.md](README.md)**: Installation, usage, and features
- **[TROUBLESHOOTING.md](TROUBLESHOOTING.md)**: Common issues
---
## 📜 Code of Conduct
### 🤝 Our Standards
- **Be respectful**: Treat everyone with respect and kindness
- **Be inclusive**: Welcome contributors of all backgrounds
- **Be constructive**: Provide helpful feedback and suggestions
- **Be patient**: Remember everyone is learning
- **Be professional**: Keep interactions focused on the project
### 🚫 Unacceptable Behavior
- Harassment or discriminatory language
- Personal attacks or trolling
- Spam or off-topic discussions
- Sharing private information without permission
### 📞 Reporting Issues
If you experience unacceptable behavior, contact: [maciek@roboblog.eu](mailto:maciek@roboblog.eu)
---
## 🎉 Thank You!
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?**
1. 🍴 Fork the repository
2. 💻 Set up your development environment
3. 🔍 Look at open issues for ideas
4. 🚀 Start coding!
We can't wait to see what you'll contribute! 🚀
================================================
FILE: DEVELOPMENT.md
================================================
# 🚧 Development Status & Roadmap
Current implementation status and planned features for Claude Code Usage Monitor v3.0.0+.
## 🎯 Current Implementation Status (v3.0.0)
### ✅ **Fully Implemented & Production Ready**
#### 🔧 **Core Monitoring System**
- **Real-time token monitoring** with configurable refresh rates (0.1-20 Hz)
- **5-hour session tracking** with intelligent session block analysis
- **Multi-plan support**: Pro (44k), Max5 (88k), Max20 (220k), Custom (P90-based)
- **Advanced analytics** with burn rate calculations and usage projections
- **Cost tracking** with model-specific pricing (Opus, Sonnet, Haiku)
- **Cache token support** for creation and read tokens
#### 🎨 **Rich Terminal UI**
- **Adaptive color themes** with WCAG-compliant contrast ratios
- **Auto-detection** of terminal background (light/dark/classic)
- **Scientific color schemes** optimized for accessibility
- **Responsive layouts** that adapt to terminal size
- **Live display** with Rich framework integration
#### ⚙️ **Professional Architecture**
- **Type-safe configuration** with Pydantic validation
- **Thread-safe monitoring** with callback-driven updates
- **Component-based design** following Single Responsibility Principle
- **Comprehensive error handling** with optional Sentry integration
- **Atomic file operations** for configuration persistence
#### 🧠 **Advanced Analytics**
- **P90 percentile analysis** for intelligent limit detection
- **Statistical confidence scoring** for custom plan limits
- **Multi-session overlap handling**
- **Historical pattern recognition** with session metadata
- **Predictive modeling** for session completion times
#### 📦 **Package Distribution**
- **PyPI-ready** with modern setuptools configuration
- **Entry points**: `claude-monitor`, `cmonitor`, and `ccm` commands
- **Cross-platform support** (Windows, macOS, Linux)
- **Professional CI/CD** with automated testing and releases
**📋 Command Aliases**:
- `claude-monitor` - Main command (full name)
- `cmonitor` - Short alias for convenience
- `ccm` - Ultra-short alias for power users
#### 🛠️ **Development Infrastructure**
- **100+ test cases** with comprehensive coverage (80% requirement)
- **Modern toolchain**: Ruff, MyPy, UV package manager
- **Automated workflows**: GitHub Actions with matrix testing
- **Code quality**: Pre-commit hooks, security scanning
- **Documentation**: Sphinx-ready with type hint integration
---
### 🐳 **Docker Containerization**
**Status**: 🔶 Planning Phase
#### Overview
Container-based deployment with optional web dashboard for team environments.
#### Planned Features
**🚀 Container Deployment**:
```bash
# Lightweight monitoring
docker run -e PLAN=max5 maciek/claude-monitor
# With web dashboard
docker run -p 8080:8080 maciek/claude-monitor --web-mode
# Persistent data
docker run -v ~/.claude_monitor:/data maciek/claude-monitor
```
**📊 Web Dashboard**:
- React-based real-time interface
- Historical usage visualization
- REST API for integrations
- Mobile-responsive design
#### Development Tasks
- [ ] **Multi-stage Dockerfile** - Optimized build process
- [ ] **Web Interface** - React dashboard development
- [ ] **API Design** - RESTful endpoints for data access
- [ ] **Security Hardening** - Non-root user, minimal attack surface
### 📱 **Mobile & Web Features**
**Status**: 🔶 Future Roadmap
#### Overview
Cross-platform monitoring with mobile apps and web interfaces for enterprise environments.
#### Planned Features
**📱 Mobile Applications**:
- iOS/Android apps for remote monitoring
- Push notifications for usage milestones
- Offline usage tracking
- Mobile-optimized dashboard
**🌐 Enterprise Features**:
- Multi-user team coordination
- Shared usage insights (anonymized)
- Organization-level analytics
- Role-based access control
**🔔 Advanced Notifications**:
- Desktop notifications for token warnings
- Email alerts for usage milestones
- Slack/Discord integration
- Webhook support for custom integrations
#### Development Tasks
- [ ] **Mobile App Architecture** - React Native foundation
- [ ] **Push Notification System** - Cross-platform notifications
- [ ] **Enterprise Dashboard** - Multi-tenant interface
- [ ] **Integration APIs** - Third-party service connectors
## 🔬 **Technical Architecture & Quality**
### 🏗️ **Current Architecture Highlights**
#### **Modern Python Development (2025)**
- **Python 3.9+** with comprehensive type annotations
- **Pydantic v2** for type-safe configuration and validation
- **UV package manager** for fast, reliable dependency resolution
- **Ruff linting** with 50+ rule sets for code quality
- **Rich framework** for beautiful terminal interfaces
#### **Professional Testing Suite**
- **100+ test cases** across 15 test files with comprehensive fixtures
- **80% coverage requirement** with HTML/XML reporting
- **Matrix testing**: Python 3.9-3.13 across multiple platforms
- **Benchmark testing** with pytest-benchmark integration
- **Security scanning** with Bandit integration
#### **CI/CD Excellence**
- **GitHub Actions workflows** with automated testing and releases
- **Smart versioning** with automatic changelog generation
- **PyPI publishing** with trusted OIDC authentication
- **Pre-commit hooks** for consistent code quality
- **Cross-platform validation** (Windows, macOS, Linux)
#### **Production-Ready Features**
- **Thread-safe architecture** with proper synchronization
- **Component isolation** preventing cascade failures
- **Comprehensive error handling** with optional Sentry integration
- **Performance optimization** with caching and efficient data structures
- **Memory management** with proper resource cleanup
### 🧪 **Code Quality Metrics**
| Metric | Current Status | Target |
|--------|---------------|---------|
| Test Coverage | 80%+ | 80% minimum |
| Type Annotations | 100% | 100% |
| Linting Rules | 50+ Ruff rules | All applicable |
| Security Scan | Bandit clean | Zero issues |
| Performance | <100ms startup | <50ms target |
### 🔧 **Development Toolchain**
#### **Core Tools**
- **Ruff**: Modern Python linter and formatter (2025 best practices)
- **MyPy**: Strict type checking with comprehensive validation
- **UV**: Next-generation Python package manager
- **Pytest**: Advanced testing with fixtures and benchmarks
- **Pre-commit**: Automated code quality checks
#### **Quality Assurance**
- **Black**: Code formatting with 88-character lines
- **isort**: Import organization with black compatibility
- **Bandit**: Security vulnerability scanning
- **Safety**: Dependency vulnerability checking
## 🤝 **Contributing & Community**
### 🚀 **Getting Started with Development**
#### **Quick Setup**
```bash
# Clone the repository
git clone https://github.com/Maciek-roboblog/Claude-Code-Usage-Monitor.git
cd Claude-Code-Usage-Monitor
# Install development dependencies with UV
uv sync --extra dev
# Install pre-commit hooks
uv run pre-commit install
# Run tests
uv run pytest
# Run linting
uv run ruff check .
uv run ruff format .
```
#### **Development Workflow**
1. **Feature Planning**: Create GitHub issue with detailed requirements
2. **Branch Creation**: Fork repository and create feature branch
3. **Development**: Code with automatic formatting and linting via pre-commit
4. **Testing**: Write tests and ensure 80% coverage requirement
5. **Quality Checks**: All tools run automatically on commit
6. **Pull Request**: Submit with clear description and documentation updates
### 🎯 **Contribution Priorities**
#### **High Priority (Immediate Impact)**
- **ML algorithm implementation** for intelligent plan detection
- **Performance optimization** for real-time monitoring
- **Cross-platform testing** and compatibility improvements
- **Documentation expansion** and user guides
#### **Medium Priority (Future Releases)**
- **Docker containerization** for deployment flexibility
- **Web dashboard development** for team environments
- **Advanced analytics features** and visualizations
- **API design** for third-party integrations
#### **Research & Innovation**
- **ML model research** for usage pattern analysis
- **Mobile app architecture** planning
- **Enterprise features** design and planning
- **Plugin system** architecture development
### 🔬 **Research Areas**
#### **ML Algorithm Evaluation**
**Current Research Focus**: Optimal approaches for token prediction and limit detection
**Algorithms Under Investigation**:
- **LSTM Networks**: Sequential pattern recognition in usage data
- **Prophet**: Time series forecasting with daily/weekly seasonality
- **Isolation Forest**: Anomaly detection for subscription changes
- **XGBoost**: Feature-based limit prediction with confidence scores
- **DBSCAN**: Clustering similar usage sessions for pattern analysis
**Key Research Questions**:
- What accuracy can we achieve for individual user limit prediction?
- How do usage patterns correlate with subscription tier changes?
- Can we automatically detect Claude API limit modifications?
- What's the minimum historical data needed for reliable predictions?
---
### 🛠️ **Skills & Expertise Needed**
#### **Machine Learning & Data Science**
**Skills**: Python, NumPy, Pandas, Scikit-learn, DuckDB, Time Series Analysis
**Current Opportunities**:
- LSTM/Prophet model implementation for usage forecasting
- Statistical analysis of P90 percentile calculations
- Anomaly detection algorithm development
- Model validation and performance optimization
#### **Web Development & UI/UX**
**Skills**: React, TypeScript, REST APIs, WebSocket, Responsive Design
**Current Opportunities**:
- Real-time dashboard development with live data streaming
- Mobile-responsive interface design
- Component library development for reusable UI elements
- User experience optimization for accessibility
#### **DevOps & Infrastructure**
**Skills**: Docker, Kubernetes, CI/CD, GitHub Actions, Security
**Current Opportunities**:
- Multi-stage Docker optimization for minimal image size
- Advanced CI/CD pipeline enhancement
- Security hardening and vulnerability management
- Performance monitoring and observability
#### **Mobile Development**
**Skills**: React Native, iOS/Android Native, Push Notifications
**Future Opportunities**:
- Cross-platform mobile app architecture
- Offline data synchronization
- Native performance optimization
- Push notification system integration
---
## 📊 **Project Metrics & Goals**
### 🎯 **Current Performance Metrics**
- **Test Coverage**: 80%+ maintained across all modules
- **Startup Time**: <100ms for typical monitoring sessions
- **Memory Usage**: <50MB peak for standard workloads
- **CPU Usage**: <5% average during monitoring
- **Type Safety**: 100% type annotation coverage
### 🚀 **Version Roadmap**
| Version | Focus | Timeline | Key Features |
|---------|-------|----------|-------------|
| **v3.1** | Performance & UX | Q2 2025 | ML auto-detection, UI improvements |
| **v3.5** | Platform Expansion | Q3 2025 | Docker support, web dashboard |
| **v4.0** | Intelligence | Q4 2025 | Advanced ML, enterprise features |
| **v4.5** | Ecosystem | Q1 2026 | Mobile apps, plugin system |
### 📈 **Success Metrics**
- **User Adoption**: Growing community with active contributors
- **Code Quality**: Maintained high standards with automated enforcement
- **Performance**: Sub-second response times for all operations
- **Reliability**: 99.9% uptime for monitoring functionality
- **Documentation**: Comprehensive guides for all features
---
## 📞 **Developer Resources**
### 🔗 **Key Links**
- **Repository**: [Claude-Code-Usage-Monitor](https://github.com/Maciek-roboblog/Claude-Code-Usage-Monitor)
- **Issues**: [GitHub Issues](https://github.com/Maciek-roboblog/Claude-Code-Usage-Monitor/issues)
- **Discussions**: [GitHub Discussions](https://github.com/Maciek-roboblog/Claude-Code-Usage-Monitor/discussions)
- **Releases**: [GitHub Releases](https://github.com/Maciek-roboblog/Claude-Code-Usage-Monitor/releases)
### 📧 **Contact & Support**
- **Technical Questions**: Open GitHub issues with detailed context
- **Feature Requests**: Use GitHub discussions for community input
- **Security Issues**: Email [maciek@roboblog.eu](mailto:maciek@roboblog.eu) directly
- **General Inquiries**: GitHub discussions or repository issues
### 📚 **Documentation**
- **User Guide**: README.md with comprehensive usage examples
- **API Documentation**: Auto-generated from type hints
- **Contributing Guide**: CONTRIBUTING.md with detailed workflows
- **Code Examples**: /docs/examples/ directory with practical demonstrations
---
*Ready to contribute? This v3.0.0 codebase represents a mature, production-ready foundation for the next generation of intelligent Claude monitoring!*
================================================
FILE: LICENSE
================================================
MIT License
Copyright (c) 2025 Maciej
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
================================================
FILE: README.md
================================================
# 🎯 Claude Code Usage Monitor
[](https://pypi.org/project/claude-monitor/)
[](https://python.org)
[](https://opensource.org/licenses/MIT)
[](http://makeapullrequest.com)
[](https://codecov.io/gh/Maciek-roboblog/Claude-Code-Usage-Monitor)
A beautiful real-time terminal monitoring tool for Claude AI token usage with advanced analytics, machine learning-based predictions, and Rich UI. Track your token consumption, burn rate, cost analysis, and get intelligent predictions about session limits.

---
## 📑 Table of Contents
- [✨ Key Features](#-key-features)
- [🚀 Installation](#-installation)
- [⚡ Modern Installation with uv (Recommended)](#-modern-installation-with-uv-recommended)
- [📦 Installation with pip](#-installation-with-pip)
- [🛠️ Other Package Managers](#️-other-package-managers)
- [📖 Usage](#-usage)
- [Get Help](#get-help)
- [Basic Usage](#basic-usage)
- [Configuration Options](#configuration-options)
- [Available Plans](#available-plans)
- [🙏 Please Help Test This Release!](#-please-help-test-this-release)
- [✨ Features & How It Works](#-features--how-it-works)
- [Current Features](#current-features)
- [Understanding Claude Sessions](#understanding-claude-sessions)
- [Token Limits by Plan](#token-limits-by-plan)
- [Smart Detection Features](#smart-detection-features)
- [🚀 Usage Examples](#-usage-examples)
- [Common Scenarios](#common-scenarios)
- [Best Practices](#best-practices)
- [🔧 Development Installation](#-development-installation)
- [Troubleshooting](#troubleshooting)
- [Installation Issues](#installation-issues)
- [Runtime Issues](#runtime-issues)
- [📞 Contact](#-contact)
- [📚 Additional Documentation](#-additional-documentation)
- [📝 License](#-license)
- [🤝 Contributors](#-contributors)
- [🙏 Acknowledgments](#-acknowledgments)
## ✨ Key Features
### 🚀 **v3.0.0 Major Update - Complete Architecture Rewrite**
- **🔮 ML-based predictions** - P90 percentile calculations and intelligent session limit detection
- **🔄 Real-time monitoring** - Configurable refresh rates (0.1-20 Hz) with intelligent display updates
- **📊 Advanced Rich UI** - Beautiful color-coded progress bars, tables, and layouts with WCAG-compliant contrast
- **🤖 Smart auto-detection** - Automatic plan switching with custom limit discovery
- **📋 Enhanced plan support** - Updated limits: Pro (44k), Max5 (88k), Max20 (220k), Custom (P90-based)
- **⚠️ Advanced warning system** - Multi-level alerts with cost and time predictions
- **💼 Professional Architecture** - Modular design with Single Responsibility Principle (SRP) compliance
- **🎨 Intelligent theming** - Scientific color schemes with automatic terminal background detection
- **⏰ Advanced scheduling** - Auto-detected system timezone and time format preferences
- **📈 Cost analytics** - Model-specific pricing with cache token calculations
- **🔧 Pydantic validation** - Type-safe configuration with automatic validation
- **📝 Comprehensive logging** - Optional file logging with configurable levels
- **🧪 Extensive testing** - 100+ test cases with full coverage
- **🎯 Error reporting** - Optional Sentry integration for production monitoring
- **⚡ Performance optimized** - Advanced caching and efficient data processing
### 📋 Default Custom Plan
The **Custom plan** is now the default option, specifically designed for 5-hour Claude Code sessions. It monitors three critical metrics:
- **Token usage** - Tracks your token consumption
- **Messages usage** - Monitors message count
- **Cost usage** - The most important metric for long sessions
The Custom plan automatically adapts to your usage patterns by analyzing all your sessions from the last 192 hours (8 days) and calculating personalized limits based on your actual usage. This ensures accurate predictions and warnings tailored to your specific workflow.
## 🚀 Installation
### ⚡ Modern Installation with uv (Recommended)
**Why uv is the best choice:**
- ✅ Creates isolated environments automatically (no system conflicts)
- ✅ No Python version issues
- ✅ No "externally-managed-environment" errors
- ✅ Easy updates and uninstallation
- ✅ Works on all platforms
The fastest and easiest way to install and use the monitor:
[](https://pypi.org/project/claude-monitor/)
#### Install from PyPI
```bash
# Install directly from PyPI with uv (easiest)
uv tool install claude-monitor
# Run from anywhere
claude-monitor # or cmonitor, ccmonitor for short
```
#### Install from Source
```bash
# Clone and install from source
git clone https://github.com/Maciek-roboblog/Claude-Code-Usage-Monitor.git
cd Claude-Code-Usage-Monitor
uv tool install .
# Run from anywhere
claude-monitor
```
#### First-time uv users
If you don't have uv installed yet, get it with one command:
```bash
# 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
```
### 📦 Installation with pip
```bash
# Install from PyPI
pip install claude-monitor
# If claude-monitor command is not found, add ~/.local/bin to PATH:
echo 'export PATH="$HOME/.local/bin:$PATH"' >> ~/.bashrc
source ~/.bashrc # or restart your terminal
# Run from anywhere
claude-monitor # or cmonitor, ccmonitor for short
```
>
> **⚠️ PATH Setup**: If you see WARNING: The script claude-monitor is installed in '/home/username/.local/bin' which is not on PATH, follow the export PATH command above.
>
> **⚠️ Important**: On modern Linux distributions (Ubuntu 23.04+, Debian 12+, Fedora 38+), you may encounter an "externally-managed-environment" error. Instead of using --break-system-packages, we strongly recommend:
> 1. **Use uv instead** (see above) - it's safer and easier
> 2. **Use a virtual environment** - python3 -m venv myenv && source myenv/bin/activate
> 3. **Use pipx** - pipx install claude-monitor
>
> See the Troubleshooting section for detailed solutions.
### 🛠️ Other Package Managers
#### pipx (Isolated Environments)
```bash
# Install with pipx
pipx install claude-monitor
# Run from anywhere
claude-monitor # or claude-code-monitor, cmonitor, ccmonitor, ccm for short
```
#### conda/mamba
```bash
# Install with pip in conda environment
pip install claude-monitor
# Run from anywhere
claude-monitor # or cmonitor, ccmonitor for short
```
## 📖 Usage
### Get Help
```bash
# Show help information
claude-monitor --help
```
#### Available Command-Line Parameters
| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| --plan | string | custom | Plan type: pro, max5, max20, or custom |
| --custom-limit-tokens | int | None | Token limit for custom plan (must be > 0) |
| --view | string | realtime | View type: realtime, daily, or monthly |
| --timezone | string | auto | Timezone (auto-detected). Examples: UTC, America/New_York, Europe/London |
| --time-format | string | auto | Time format: 12h, 24h, or auto |
| --theme | string | auto | Display theme: light, dark, classic, or auto |
| --refresh-rate | int | 10 | Data refresh rate in seconds (1-60) |
| --refresh-per-second | float | 0.75 | Display refresh rate in Hz (0.1-20.0) |
| --reset-hour | int | None | Daily reset hour (0-23) |
| --log-level | string | INFO | Logging level: DEBUG, INFO, WARNING, ERROR, CRITICAL |
| --log-file | path | None | Log file path |
| --debug | flag | False | Enable debug logging |
| --version, -v | flag | False | Show version information |
| --clear | flag | False | Clear saved configuration |
#### Plan Options
| Plan | Token Limit | Cost Limit | Description |
|------|-------------|------------------|-------------|
| pro | 19,000 | $18.00 | Claude Pro subscription |
| max5 | 88,000 | $35.00 | Claude Max5 subscription |
| max20 | 220,000 | $140.00 | Claude Max20 subscription |
| custom | P90-based | (default) $50.00 | Auto-detection with ML analysis |
#### Command Aliases
The tool can be invoked using any of these commands:
- claude-monitor (primary)
- claude-code-monitor (full name)
- cmonitor (short)
- ccmonitor (short alternative)
- ccm (shortest)
#### Save Flags Feature
The monitor automatically saves your preferences to avoid re-specifying them on each run:
**What Gets Saved:**
- View type (--view)
- Theme preferences (--theme)
- Timezone settings (--timezone)
- Time format (--time-format)
- Refresh rates (--refresh-rate, --refresh-per-second)
- Reset hour (--reset-hour)
- Custom token limits (--custom-limit-tokens)
**Configuration Location:** ~/.claude-monitor/last_used.json
**Usage Examples:**
```bash
# First run - specify preferences
claude-monitor --plan pro --theme dark --timezone "America/New_York"
# Subsequent runs - preferences automatically restored
claude-monitor --plan pro
# Override saved settings for this session
claude-monitor --plan pro --theme light
# Clear all saved preferences
claude-monitor --clear
```
**Key Features:**
- ✅ Automatic parameter persistence between sessions
- ✅ CLI arguments always override saved settings
- ✅ Atomic file operations prevent corruption
- ✅ Graceful fallback if config files are damaged
- ✅ Plan parameter never saved (must specify each time)
### Basic Usage
#### With uv tool installation (Recommended)
```bash
# Default (Custom plan with auto-detection)
claude-monitor
# Alternative commands
claude-code-monitor # Full descriptive name
cmonitor # Short alias
ccmonitor # Short alternative
ccm # Shortest alias
# Exit the monitor
# Press Ctrl+C to gracefully exit
```
#### Development mode
If running from source, use python -m claude_monitor from the src/ directory.
### Configuration Options
#### Specify Your Plan
```bash
# Custom plan with P90 auto-detection (Default)
claude-monitor --plan custom
# Pro plan (~44,000 tokens)
claude-monitor --plan pro
# Max5 plan (~88,000 tokens)
claude-monitor --plan max5
# Max20 plan (~220,000 tokens)
claude-monitor --plan max20
# Custom plan with explicit token limit
claude-monitor --plan custom --custom-limit-tokens 100000
```
#### Custom Reset Times
```bash
# Reset at 3 AM
claude-monitor --reset-hour 3
# Reset at 10 PM
claude-monitor --reset-hour 22
```
#### Usage View Configuration
```bash
# Real-time monitoring with live updates (Default)
claude-monitor --view realtime
# Daily token usage aggregated in table format
claude-monitor --view daily
# Monthly token usage aggregated in table format
claude-monitor --view monthly
```
#### Performance and Display Configuration
```bash
# Adjust refresh rate (1-60 seconds, default: 10)
claude-monitor --refresh-rate 5
# Adjust display refresh rate (0.1-20 Hz, default: 0.75)
claude-monitor --refresh-per-second 1.0
# Set time format (auto-detected by default)
claude-monitor --time-format 24h # or 12h
# Force specific theme
claude-monitor --theme dark # light, dark, classic, auto
# Clear saved configuration
claude-monitor --clear
```
#### Timezone Configuration
The default timezone is **auto-detected from your system**. Override with any valid timezone:
```bash
# Use US Eastern Time
claude-monitor --timezone America/New_York
# Use Tokyo time
claude-monitor --timezone Asia/Tokyo
# Use UTC
claude-monitor --timezone UTC
# Use London time
claude-monitor --timezone Europe/London
```
#### Logging and Debugging
```bash
# Enable debug logging
claude-monitor --debug
# Log to file
claude-monitor --log-file ~/.claude-monitor/logs/monitor.log
# Set log level
claude-monitor --log-level WARNING # DEBUG, INFO, WARNING, ERROR, CRITICAL
```
### Available Plans
| Plan | Token Limit | Best For |
|------|-----------------|----------|
| **custom** | P90 auto-detect | Intelligent limit detection (default) |
| **pro** | ~19,000 | Claude Pro subscription |
| **max5** | ~88,000 | Claude Max5 subscription |
| **max20** | ~220,000 | Claude Max20 subscription |
#### Advanced Plan Features
- **P90 Analysis**: Custom plan uses 90th percentile calculations from your usage history
- **Cost Tracking**: Model-specific pricing with cache token calculations
- **Limit Detection**: Intelligent threshold detection with 95% confidence
## 🚀 What's New in v3.0.0
### Major Changes
#### **Complete Architecture Rewrite**
- Modular design with Single Responsibility Principle (SRP) compliance
- Pydantic-based configuration with type safety and validation
- Advanced error handling with optional Sentry integration
- Comprehensive test suite with 100+ test cases
#### **Enhanced Functionality**
- **P90 Analysis**: Machine learning-based limit detection using 90th percentile calculations
- **Updated Plan Limits**: Pro (44k), Max5 (88k), Max20 (220k) tokens
- **Cost Analytics**: Model-specific pricing with cache token calculations
- **Rich UI**: WCAG-compliant themes with automatic terminal background detection
#### **New CLI Options**
- --refresh-per-second: Configurable display refresh rate (0.1-20 Hz)
- --time-format: Automatic 12h/24h format detection
- --custom-limit-tokens: Explicit token limits for custom plans
- --log-file and --log-level: Advanced logging capabilities
- --clear: Reset saved configuration
- Command aliases: claude-code-monitor, cmonitor, ccmonitor, ccm for convenience
#### **Breaking Changes**
- Package name changed from claude-usage-monitor to claude-monitor
- Default plan changed from pro to custom (with auto-detection)
- Minimum Python version increased to 3.9+
- Command structure updated (see examples above)
## ✨ Features & How It Works
### v3.0.0 Architecture Overview
The new version features a complete rewrite with modular architecture following Single Responsibility Principle (SRP):
### 🖥️ User Interface Layer
| Component | Description |
| -------------------- | --------------------- |
| **CLI Module** | Pydantic-based |
| **Settings/Config** | Type-safe |
| **Error Handling** | Sentry-ready |
| **Rich Terminal UI** | Adaptive Theme |
---
### 🎛️ Monitoring Orchestrator
| Component | Key Responsibilities |
| ------------------------ | ---------------------------------------------------------------- |
| **Central Control Hub** | Session Mgmt · Real-time Data Flow · Component Coordination |
| **Data Manager** | Cache Mgmt · File I/O · State Persist |
| **Session Monitor** | Real-time · 5 hr Windows · Token Track |
| **UI Controller** | Rich Display · Progress Bars · Theme System |
| **Analytics** | P90 Calculator · Burn Rate · Predictions |
---
### 🏗️ Foundation Layer
| Component | Core Features |
| ------------------- | ------------------------------------------------------- |
| **Core Models** | Session Data · Config Schema · Type Safety |
| **Analysis Engine** | ML Algorithms · Statistical · Forecasting |
| **Terminal Themes** | Auto-detection · WCAG Colors · Contrast Opt |
| **Claude API Data** | Token Tracking · Cost Calculator · Session Blocks |
---
**🔄 Data Flow:**
Claude Config Files → Data Layer → Analysis Engine → UI Components → Terminal Display
### Current Features
#### 🔄 Advanced Real-time Monitoring
- Configurable update intervals (1-60 seconds)
- High-precision display refresh (0.1-20 Hz)
- Intelligent change detection to minimize CPU usage
- Multi-threaded orchestration with callback system
#### 📊 Rich UI Components
- **Progress Bars**: WCAG-compliant color schemes with scientific contrast ratios
- **Data Tables**: Sortable columns with model-specific statistics
- **Layout Manager**: Responsive design that adapts to terminal size
- **Theme System**: Auto-detects terminal background for optimal readability
#### 📈 Multiple Usage Views
- **Realtime View** (Default): Live monitoring with progress bars, current session data, and burn rate analysis
- **Daily View**: Aggregated daily statistics showing Date, Models, Input/Output/Cache tokens, Total tokens, and Cost
- **Monthly View**: Monthly aggregated data for long-term trend analysis and budget planning
#### 🔮 Machine Learning Predictions
- **P90 Calculator**: 90th percentile analysis for intelligent limit detection
- **Burn Rate Analytics**: Multi-session consumption pattern analysis
- **Cost Projections**: Model-specific pricing with cache token calculations
- **Session Forecasting**: Predicts when sessions will expire based on usage patterns
#### 🤖 Intelligent Auto-Detection
- **Background Detection**: Automatically determines terminal theme (light/dark)
- **System Integration**: Auto-detects timezone and time format preferences
- **Plan Recognition**: Analyzes usage patterns to suggest optimal plans
- **Limit Discovery**: Scans historical data to find actual token limits
### Understanding Claude Sessions
#### How Claude Code Sessions Work
Claude Code operates on a **5-hour rolling session window system**:
1. **Session Start**: Begins with your first message to Claude
2. **Session Duration**: Lasts exactly 5 hours from that first message
3. **Token Limits**: Apply within each 5-hour session window
4. **Multiple Sessions**: Can have several active sessions simultaneously
5. **Rolling Windows**: New sessions can start while others are still active
#### Session Reset Schedule
**Example Session Timeline:**
10:30 AM - First message (Session A starts at 10 AM)
03:00 PM - Session A expires (5 hours later)
12:15 PM - First message (Session B starts 12PM)
05:15 PM - Session B expires (5 hours later 5PM)
#### Burn Rate Calculation
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
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
### Token Limits by Plan
#### v3.0.0 Updated Plan Limits
| Plan | Limit (Tokens) | Cost Limit | Messages | Algorithm |
|------|----------------|------------------|----------|-----------|
| **Claude Pro** | 19,000 | $18.00 | 250 | Fixed limit |
| **Claude Max5** | 88,000 | $35.00 | 1,000 | Fixed limit |
| **Claude Max20** | 220,000 | $140.00 | 2,000 | Fixed limit |
| **Custom** | P90-based | (default) $50.00 | 250+ | Machine learning |
#### Advanced Limit Detection
- **P90 Analysis**: Uses 90th percentile of your historical usage
- **Confidence Threshold**: 95% accuracy in limit detection
- **Cache Support**: Includes cache creation and read token costs
- **Model-Specific**: Adapts to Claude 3.5, Claude 4, and future models
### Technical Requirements
#### Dependencies (v3.0.0)
```toml
# Core dependencies (automatically installed)
pytz>=2023.3 # Timezone handling
rich>=13.7.0 # Rich terminal UI
pydantic>=2.0.0 # Type validation
pydantic-settings>=2.0.0 # Configuration management
numpy>=1.21.0 # Statistical calculations
sentry-sdk>=1.40.0 # Error reporting (optional)
pyyaml>=6.0 # Configuration files
tzdata # Windows timezone data
```
#### Python Requirements
- **Minimum**: Python 3.9+
- **Recommended**: Python 3.11+
- **Tested on**: Python 3.9, 3.10, 3.11, 3.12, 3.13
### Smart Detection Features
#### Automatic Plan Switching
When using the default Pro plan:
1. **Detection**: Monitor notices token usage exceeding 7,000
2. **Analysis**: Scans previous sessions for actual limits
3. **Switch**: Automatically changes to custom_max mode
4. **Notification**: Displays clear message about the change
5. **Continuation**: Keeps monitoring with new, higher limit
#### Limit Discovery Process
The auto-detection system:
1. **Scans History**: Examines all available session blocks
2. **Finds Peaks**: Identifies highest token usage achieved
3. **Validates Data**: Ensures data quality and recency
4. **Sets Limits**: Uses discovered maximum as new limit
5. **Learns Patterns**: Adapts to your actual usage capabilities
## 🚀 Usage Examples
### Common Scenarios
#### 🌅 Morning Developer
**Scenario**: You start work at 9 AM and want tokens to reset aligned with your schedule.
```bash
# Set custom reset time to 9 AM
./claude_monitor.py --reset-hour 9
# With your timezone
./claude_monitor.py --reset-hour 9 --timezone US/Eastern
```
**Benefits**:
- Reset times align with your work schedule
- Better planning for daily token allocation
- Predictable session windows
#### 🌙 Night Owl Coder
**Scenario**: You often work past midnight and need flexible reset scheduling.
```bash
# Reset at midnight for clean daily boundaries
./claude_monitor.py --reset-hour 0
# Late evening reset (11 PM)
./claude_monitor.py --reset-hour 23
```
**Strategy**:
- Plan heavy coding sessions around reset times
- Use late resets to span midnight work sessions
- Monitor burn rate during peak hours
#### 🔄 Heavy User with Variable Limits
**Scenario**: Your token limits seem to change, and you're not sure of your exact plan.
```bash
# Auto-detect your highest previous usage
claude-monitor --plan custom_max
# Monitor with custom scheduling
claude-monitor --plan custom_max --reset-hour 6
```
**Approach**:
- Let auto-detection find your real limits
- Monitor for a week to understand patterns
- Note when limits change or reset
#### 🌍 International User
**Scenario**: You're working across different timezones or traveling.
```bash
# US East Coast
claude-monitor --timezone America/New_York
# Europe
claude-monitor --timezone Europe/London
# Asia Pacific
claude-monitor --timezone Asia/Singapore
# UTC for international team coordination
claude-monitor --timezone UTC --reset-hour 12
```
#### ⚡ Quick Check
**Scenario**: You just want to see current status without configuration.
```bash
# Just run it with defaults
claude-monitor
# Press Ctrl+C after checking status
```
#### 📊 Usage Analysis Views
**Scenario**: Analyzing your token usage patterns over different time periods.
```bash
# View daily usage breakdown with detailed statistics
claude-monitor --view daily
# Analyze monthly token consumption trends
claude-monitor --view monthly --plan max20
# Export daily usage data to log file for analysis
claude-monitor --view daily --log-file ~/daily-usage.log
# Review usage in different timezone
claude-monitor --view daily --timezone America/New_York
```
**Use Cases**:
- **Realtime**: Live monitoring of current session and burn rate
- **Daily**: Analyze daily consumption patterns and identify peak usage days
- **Monthly**: Long-term trend analysis and monthly budget planning
### Plan Selection Strategies
#### How to Choose Your Plan
**Start with Default (Recommended for New Users)**
```bash
# Pro plan detection with auto-switching
claude-monitor
```
- Monitor will detect if you exceed Pro limits
- Automatically switches to custom_max if needed
- Shows notification when switching occurs
**Known Subscription Users**
```bash
# If you know you have Max5
claude-monitor --plan max5
# If you know you have Max20
claude-monitor --plan max20
```
**Unknown Limits**
```bash
# Auto-detect from previous usage
claude-monitor --plan custom_max
```
### Best Practices
#### Setup Best Practices
1. **Start Early in Sessions**
```bash
# Begin monitoring when starting Claude work (uv installation)
claude-monitor
# Or development mode
./claude_monitor.py
```
- Gives accurate session tracking from the start
- Better burn rate calculations
- Early warning for limit approaches
2. **Use Modern Installation (Recommended)**
```bash
# Easy installation and updates with uv
uv tool install claude-monitor
claude-monitor --plan max5
```
- Clean system installation
- Easy updates and maintenance
- Available from anywhere
3. **Custom Shell Alias (Legacy Setup)**
```bash
# Add to ~/.bashrc or ~/.zshrc (only for development setup)
alias claude-monitor='cd ~/Claude-Code-Usage-Monitor && source venv/bin/activate && ./claude_monitor.py'
```
#### Usage Best Practices
1. **Monitor Burn Rate Velocity**
- Watch for sudden spikes in token consumption
- Adjust coding intensity based on remaining time
- Plan big refactors around session resets
2. **Strategic Session Planning**
```bash
# Plan heavy usage around reset times
claude-monitor --reset-hour 9
```
- Schedule large tasks after resets
- Use lighter tasks when approaching limits
- Leverage multiple overlapping sessions
3. **Timezone Awareness**
```bash
# Always use your actual timezone
claude-monitor --timezone Europe/Warsaw
```
- Accurate reset time predictions
- Better planning for work schedules
- Correct session expiration estimates
#### Optimization Tips
1. **Terminal Setup**
- Use terminals with at least 80 character width
- Enable color support for better visual feedback (check COLORTERM environment variable)
- Consider dedicated terminal window for monitoring
- Use terminals with truecolor support for best theme experience
2. **Workflow Integration**
```bash
# Start monitoring with your development session (uv installation)
tmux new-session -d -s claude-monitor 'claude-monitor'
# Or development mode
tmux new-session -d -s claude-monitor './claude_monitor.py'
# Check status anytime
tmux attach -t claude-monitor
```
3. **Multi-Session Strategy**
- Remember sessions last exactly 5 hours
- You can have multiple overlapping sessions
- Plan work across session boundaries
#### Real-World Workflows
**Large Project Development**
```bash
# Setup for sustained development
claude-monitor --plan max20 --reset-hour 8 --timezone America/New_York
```
**Daily Routine**:
1. **8:00 AM**: Fresh tokens, start major features
2. **10:00 AM**: Check burn rate, adjust intensity
3. **12:00 PM**: Monitor for afternoon session planning
4. **2:00 PM**: New session window, tackle complex problems
5. **4:00 PM**: Light tasks, prepare for evening session
**Learning & Experimentation**
```bash
# Flexible setup for learning
claude-monitor --plan pro
```
**Sprint Development**
```bash
# High-intensity development setup
claude-monitor --plan max20 --reset-hour 6
```
## 🔧 Development Installation
For contributors and developers who want to work with the source code:
### Quick Start (Development/Testing)
```bash
# Clone the repository
git clone https://github.com/Maciek-roboblog/Claude-Code-Usage-Monitor.git
cd Claude-Code-Usage-Monitor
# Install in development mode
pip install -e .
# Run from source
python -m claude_monitor
```
### v3.0.0 Testing Features
The new version includes a comprehensive test suite:
- **100+ test cases** with full coverage
- **Unit tests** for all components
- **Integration tests** for end-to-end workflows
- **Performance tests** with benchmarking
- **Mock objects** for isolated testing
```bash
# Run tests
cd src/
python -m pytest
# Run with coverage
python -m pytest --cov=claude_monitor --cov-report=html
# Run specific test modules
python -m pytest tests/test_analysis.py -v
```
### Prerequisites
1. **Python 3.9+** installed on your system
2. **Git** for cloning the repository
### Virtual Environment Setup
#### Why Use Virtual Environment?
Using a virtual environment is **strongly recommended** because:
- **🛡️ Isolation**: Keeps your system Python clean and prevents dependency conflicts
- **📦 Portability**: Easy to replicate the exact environment on different machines
- **🔄 Version Control**: Lock specific versions of dependencies for stability
- **🧹 Clean Uninstall**: Simply delete the virtual environment folder to remove everything
- **👥 Team Collaboration**: Everyone uses the same Python and package versions
#### Installing virtualenv (if needed)
If you don't have venv module available:
```bash
# Ubuntu/Debian
sudo apt-get update
sudo apt-get install python3-venv
# Fedora/RHEL/CentOS
sudo dnf install python3-venv
# macOS (usually comes with Python)
# If not available, install Python via Homebrew:
brew install python3
# Windows (usually comes with Python)
# If not available, reinstall Python from python.org
# Make sure to check "Add Python to PATH" during installation
```
Alternatively, use the virtualenv package:
```bash
# Install virtualenv via pip
pip install virtualenv
# Then create virtual environment with:
virtualenv venv
# instead of: python3 -m venv venv
```
#### Step-by-Step Setup
```bash
# 1. Clone the repository
git clone https://github.com/Maciek-roboblog/Claude-Code-Usage-Monitor.git
cd Claude-Code-Usage-Monitor
# 2. Create virtual environment
python3 -m venv venv
# Or if using virtualenv package:
# virtualenv venv
# 3. Activate virtual environment
# On Linux/Mac:
source venv/bin/activate
# On Windows:
# venv\Scripts\activate
# 4. Install Python dependencies
pip install pytz
pip install rich>=13.0.0
# 5. Make script executable (Linux/Mac only)
chmod +x claude_monitor.py
# 6. Run the monitor
python claude_monitor.py
```
#### Daily Usage
After initial setup, you only need:
```bash
# Navigate to project directory
cd Claude-Code-Usage-Monitor
# Activate virtual environment
source venv/bin/activate # Linux/Mac
# venv\Scripts\activate # Windows
# Run monitor
./claude_monitor.py # Linux/Mac
# python claude_monitor.py # Windows
# When done, deactivate
deactivate
```
#### Pro Tip: Shell Alias
Create an alias for quick access:
```bash
# Add to ~/.bashrc or ~/.zshrc
alias claude-monitor='cd ~/Claude-Code-Usage-Monitor && source venv/bin/activate && ./claude_monitor.py'
# Then just run:
claude-monitor
```
## Troubleshooting
### Installation Issues
#### "externally-managed-environment" Error
On modern Linux distributions (Ubuntu 23.04+, Debian 12+, Fedora 38+), you may encounter:
```
error: externally-managed-environment
× This environment is externally managed
```
**Solutions (in order of preference):**
1. **Use uv (Recommended)**
```bash
# Install uv first
curl -LsSf https://astral.sh/uv/install.sh | sh
# Then install with uv
uv tool install claude-monitor
```
2. **Use pipx (Isolated Environment)**
```bash
# Install pipx
sudo apt install pipx # Ubuntu/Debian
# or
python3 -m pip install --user pipx
# Install claude-monitor
pipx install claude-monitor
```
3. **Use virtual environment**
```bash
python3 -m venv myenv
source myenv/bin/activate
pip install claude-monitor
```
4. **Force installation (Not Recommended)**
```bash
pip install --user claude-monitor --break-system-packages
```
⚠️ **Warning**: This bypasses system protection and may cause conflicts. We strongly recommend using a virtual environment instead.
#### Command Not Found After pip Install
If claude-monitor command is not found after pip installation:
1. **Check if it's a PATH issue**
```bash
# Look for the warning message during pip install:
# WARNING: The script claude-monitor is installed in '/home/username/.local/bin' which is not on PATH
```
2. **Add to PATH**
```bash
# Add this to ~/.bashrc or ~/.zshrc
echo 'export PATH="$HOME/.local/bin:$PATH"' >> ~/.bashrc
# Reload shell
source ~/.bashrc # or source ~/.zshrc
```
3. **Verify installation location**
```bash
# Find where pip installed the script
pip show -f claude-monitor | grep claude-monitor
```
4. **Run directly with Python**
```bash
python3 -m claude_monitor
```
#### Python Version Conflicts
If you have multiple Python versions:
1. **Check Python version**
```bash
python3 --version
pip3 --version
```
2. **Use specific Python version**
```bash
python3.11 -m pip install claude-monitor
python3.11 -m claude_monitor
```
3. **Use uv (handles Python versions automatically)**
```bash
uv tool install claude-monitor
```
### Runtime Issues
#### No active session found
If you encounter the error No active session found, please follow these steps:
1. **Initial Test**:
Launch Claude Code and send at least two messages. In some cases, the session may not initialize correctly on the first attempt, but it resolves after a few interactions.
2. **Configuration Path**:
If the issue persists, consider specifying a custom configuration path. By default, Claude Code uses ~/.config/claude. You may need to adjust this path depending on your environment.
```bash
CLAUDE_CONFIG_DIR=~/.config/claude ./claude_monitor.py
```
## 📞 Contact
Have questions, suggestions, or want to collaborate? Feel free to reach out!
**📧 Email**: [maciek@roboblog.eu](mailto:maciek@roboblog.eu)
Whether you need help with setup, have feature requests, found a bug, or want to discuss potential improvements, don't hesitate to get in touch. I'm always happy to help and hear from users of the Claude Code Usage Monitor!
## 📚 Additional Documentation
- **[Development Roadmap](DEVELOPMENT.md)** - ML features, PyPI package, Docker plans
- **[Contributing Guide](CONTRIBUTING.md)** - How to contribute, development guidelines
- **[Troubleshooting](TROUBLESHOOTING.md)** - Common issues and solutions
## 📝 License
[MIT License](LICENSE) - feel free to use and modify as needed.
## 🤝 Contributors
- [@adawalli](https://github.com/adawalli)
- [@taylorwilsdon](https://github.com/taylorwilsdon)
- [@moneroexamples](https://github.com/moneroexamples)
Want to contribute? Check out our [Contributing Guide](CONTRIBUTING.md)!
## 🙏 Acknowledgments
### Sponsors
A special thanks to our supporters who help keep this project going:
**Ed** - *Buy Me Coffee Supporter*
> "I appreciate sharing your work with the world. It helps keep me on track with my day. Quality readme, and really good stuff all around!"
## Star History
[](https://www.star-history.com/#Maciek-roboblog/Claude-Code-Usage-Monitor&Date)
---
<div align="center">
**⭐ Star this repo if you find it useful! ⭐**
[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)
</div>
================================================
FILE: RELEASE.md
================================================
# Release Process
This document describes the release process for Claude Code Usage Monitor.
## Automated Release (GitHub Actions)
Releases are automatically triggered when changes are pushed to the `main` branch. The GitHub Actions workflow will:
1. Extract the version from `pyproject.toml`
2. Check if a git tag for this version already exists
3. If not, it will:
- Create a new git tag
- Extract release notes from `CHANGELOG.md`
- Create a GitHub release
- Build and publish the package to PyPI
### Prerequisites for Automated Release
1. **PyPI API Token**: Must be configured as a GitHub secret named `PYPI_API_TOKEN`
- Generate at: https://pypi.org/manage/account/token/
- Add to repository secrets: Settings → Secrets and variables → Actions → New repository secret
2. **Publishing Permissions**: Ensure GitHub Actions has permissions to create releases
- Settings → Actions → General → Workflow permissions → Read and write permissions
## Manual Release Process
If automated release fails or for special cases, follow these steps:
### 1. Prepare Release
```bash
# Ensure you're on main branch with latest changes
git checkout main
git pull origin main
# Run tests and linting
uv sync --extra dev
uv run ruff check .
uv run ruff format --check .
```
### 2. Update Version
Edit `pyproject.toml` and update the version:
```toml
version = "1.0.9" # Update to your new version
```
### 3. Update CHANGELOG.md
Add a new section at the top of `CHANGELOG.md`:
```markdown
## [1.0.9] - 2025-06-21
### Added
- Description of new features
### Changed
- Description of changes
### Fixed
- Description of fixes
[1.0.9]: https://github.com/Maciek-roboblog/Claude-Code-Usage-Monitor/releases/tag/v1.0.9
```
### 4. Commit Version Changes
```bash
git add pyproject.toml CHANGELOG.md
git commit -m "Bump version to 1.0.9"
git push origin main
```
### 5. Create Git Tag
```bash
# Create annotated tag
git tag -a v1.0.9 -m "Release v1.0.9"
# Push tag to GitHub
git push origin v1.0.9
```
### 6. Build Package
```bash
# Clean previous builds
rm -rf dist/
# Build with uv
uv build
# Verify build artifacts
ls -la dist/
# Should show:
# - claude_monitor-1.0.9-py3-none-any.whl
# - claude_monitor-1.0.9.tar.gz
```
### 7. Create GitHub Release
1. Go to: https://github.com/Maciek-roboblog/Claude-Code-Usage-Monitor/releases/new
2. Choose tag: `v1.0.9`
3. Release title: `Release v1.0.9`
4. Copy the relevant section from CHANGELOG.md to the description
5. Attach the built artifacts from `dist/` (optional)
6. Click "Publish release"
### 8. Publish to PyPI
```bash
# Install twine if needed
uv tool install twine
# Upload to PyPI (will prompt for credentials)
uv tool run twine upload dist/*
# Or with API token
uv tool run twine upload dist/* --username __token__ --password <your-pypi-token>
```
### 9. Verify Release
1. Check PyPI: https://pypi.org/project/claude-monitor/
2. Test installation:
```bash
# In a new environment
uv tool install claude-monitor
claude-monitor --version
# Test all command aliases
cmonitor --version
ccm --version
```
## Version Numbering
We follow semantic versioning (SemVer):
- **MAJOR.MINOR.PATCH** (e.g., 1.0.9)
- **MAJOR**: Incompatible API changes
- **MINOR**: New functionality in a backward-compatible manner
- **PATCH**: Backward-compatible bug fixes
## Troubleshooting
### GitHub Actions Release Failed
1. Check Actions tab for error logs
2. Common issues:
- Missing or invalid `PYPI_API_TOKEN`
- Version already exists on PyPI
- Malformed CHANGELOG.md
### PyPI Upload Failed
1. **Authentication Error**: Check your PyPI token
2. **Version Exists**: Version numbers cannot be reused on PyPI
3. **Package Name Taken**: The package name might be reserved
### Tag Already Exists
```bash
# Delete local tag
git tag -d v1.0.9
# Delete remote tag
git push --delete origin v1.0.9
# Recreate tag
git tag -a v1.0.9 -m "Release v1.0.9"
git push origin v1.0.9
```
## Release Checklist
- [ ] All tests pass
- [ ] Code is properly formatted (ruff)
- [ ] Version updated in `pyproject.toml`
- [ ] CHANGELOG.md updated with release notes
- [ ] Changes committed and pushed to main
- [ ] Git tag created and pushed
- [ ] GitHub release created
- [ ] Package published to PyPI
- [ ] Installation tested in clean environment
================================================
FILE: TROUBLESHOOTING.md
================================================
# 🐛 Troubleshooting Guide - Claude Monitor v3.0.0
**⚠️ This guide is specifically for Claude Monitor v3.0.0** - If you're using an older version, please upgrade first.
## 🚨 Quick Fixes
### Most Common v3.0.0 Issues
| Problem | Quick Fix |
|---------|-----------|
| `command not found: claude-monitor` | Add `~/.local/bin` to PATH or use `python -m claude_monitor` |
| `externally-managed-environment` | Use `uv tool install claude-monitor` instead of pip |
| No Claude data found | Ensure you have active Claude Code sessions with recent messages |
| Validation errors | Check configuration with `claude-monitor --help` |
| Display issues | Terminal width must be 80+ characters |
| Theme detection problems | Use `--theme dark` or `--theme light` explicitly |
## 🔧 Installation Issues (v3.0.0)
### Package Name Change
**v3.0.0 Breaking Change**: Package name changed from `claude-usage-monitor` to `claude-monitor`
```bash
# OLD (deprecated)
pip install claude-usage-monitor
# NEW (v3.0.0)
pip install claude-monitor
uv tool install claude-monitor
```
### "externally-managed-environment" Error
**Common on Ubuntu 23.04+, Debian 12+, Fedora 38+**
**Solutions (in order of preference)**:
1. **Use uv (Recommended)**:
```bash
# Install uv
curl -LsSf https://astral.sh/uv/install.sh | sh
source ~/.bashrc
# Install claude-monitor
uv tool install claude-monitor
claude-monitor
```
2. **Use pipx**:
```bash
# Install pipx
sudo apt install pipx # Ubuntu/Debian
pipx install claude-monitor
claude-monitor
```
3. **Use virtual environment**:
```bash
python3 -m venv venv
source venv/bin/activate
pip install claude-monitor
claude-monitor
```
### Command Not Found After Installation
**Issue**: `claude-monitor` command not found
**Solutions**:
1. **Check PATH**:
```bash
# Add to ~/.bashrc or ~/.zshrc
echo 'export PATH="$HOME/.local/bin:$PATH"' >> ~/.bashrc
source ~/.bashrc
```
2. **Use Python module**:
```bash
python -m claude_monitor
```
3. **Check installation**:
```bash
pip show claude-monitor
which claude-monitor
```
### Python Version Requirements
**v3.0.0 requires Python 3.9+**
```bash
# Check Python version
python3 --version
# If too old, upgrade Python or use specific version
python3.11 -m pip install claude-monitor
python3.11 -m claude_monitor
```
### Dependency Installation Issues
**Missing dependencies error**:
```bash
# Manual installation of core dependencies
pip install pytz>=2023.3 rich>=13.7.0 pydantic>=2.0.0
pip install pydantic-settings>=2.0.0 numpy>=1.21.0
```
## 💾 Data and Configuration Issues
### No Claude Data Directory Found
**Error**: `No Claude data directory found`
**Causes and Solutions**:
1. **Default data path doesn't exist**:
```bash
# Check if directory exists
ls ~/.claude/projects
# Start Claude Code session first
# Go to claude.ai/code and send messages
```
2. **Permission issues**:
```bash
# Check permissions
ls -la ~/.claude/
# Fix permissions if needed
chmod 755 ~/.claude/projects
```
3. **Custom data path**:
```bash
# If Claude uses different path, set environment variable
export CLAUDE_CONFIG_DIR=/path/to/your/claude/config
claude-monitor
```
### JSONL File Processing Errors
**Error**: `Failed to parse JSON line in {file}: {error}`
**Solutions**:
1. **Corrupted files**: Let monitor skip malformed lines (automatic)
2. **Check file integrity**:
```bash
# Validate JSONL files
find ~/.claude/projects -name "*.jsonl" -exec python -c "
import json
with open('{}') as f:
for i, line in enumerate(f, 1):
try: json.loads(line)
except: print(f'Error in {}: line {i}')
" \;
```
### Session Detection Issues
**Error**: `No active session found`
**Debugging steps**:
1. **Verify Claude Code usage**:
- Must use claude.ai/code (not regular Claude)
- Send at least 2-3 messages
- Wait 30 seconds after last message
2. **Check data freshness**:
```bash
# Check recent files
find ~/.claude/projects -name "*.jsonl" -mtime -1 -ls
```
3. **Manual data verification**:
```bash
# Enable debug logging
claude-monitor --debug --log-file /tmp/claude-debug.log
# Check logs
tail -f /tmp/claude-debug.log
```
## ⚙️ Configuration Validation Errors
### Invalid Plan Configuration
**Error**: `Invalid plan: {value}. Must be one of: pro, max5, max20, custom`
**Valid options**:
```bash
# Correct plan names (case-insensitive)
claude-monitor --plan pro # 44k tokens
claude-monitor --plan max5 # 88k tokens
claude-monitor --plan max20 # 220k tokens
claude-monitor --plan custom # P90 auto-detection
```
### Invalid Theme Settings
**Error**: `Invalid theme: {value}. Must be one of: light, dark, classic, auto`
**Solutions**:
```bash
# Force specific theme
claude-monitor --theme dark
claude-monitor --theme light
# Debug theme detection
claude-monitor --debug
```
### Timezone Validation Errors
**Error**: `Invalid timezone: {value}`
**Solutions**:
```bash
# Use auto-detection (default)
claude-monitor --timezone auto
# Valid timezone examples
claude-monitor --timezone UTC
claude-monitor --timezone America/New_York
claude-monitor --timezone Europe/London
claude-monitor --timezone Asia/Tokyo
# List available timezones
python -c "import pytz; print('\n'.join(sorted(pytz.all_timezones)))" | grep America
```
### Numeric Range Validation Errors
**Common validation failures**:
```bash
# Refresh rate: must be 1-60 seconds
claude-monitor --refresh-rate 5 # Valid
claude-monitor --refresh-rate 0 # Invalid: below minimum
# Display refresh rate: must be 0.1-20 Hz
claude-monitor --refresh-per-second 1.0 # Valid
claude-monitor --refresh-per-second 25 # Invalid: above maximum
# Reset hour: must be 0-23
claude-monitor --reset-hour 9 # Valid
claude-monitor --reset-hour 24 # Invalid: out of range
# Custom token limit: must be positive
claude-monitor --plan custom --custom-limit-tokens 50000 # Valid
claude-monitor --plan custom --custom-limit-tokens 0 # Invalid
```
## 🖥️ Display and Terminal Issues
### Terminal Width Too Narrow
**Issue**: Overlapping text, garbled display
**Solutions**:
```bash
# Check terminal width
tput cols # Should be 80+
# Resize terminal window or use scrolling
claude-monitor | less -S
```
### Theme Detection Problems
**Issue**: Wrong colors, poor contrast
**Debug theme detection**:
```bash
# Check environment variables
echo $COLORFGBG
echo $TERM
echo $COLORTERM
# Force theme explicitly
claude-monitor --theme dark # For dark terminals
claude-monitor --theme light # For light terminals
```
**SSH/Remote sessions**:
```bash
# Theme detection may fail over SSH
claude-monitor --theme dark # Usually safer for SSH
```
### Missing Colors or Emojis
**Issue**: Plain text output, no colors
**Solutions**:
```bash
# Check terminal capabilities
echo $TERM
echo $COLORTERM
# Force color output
export FORCE_COLOR=1
claude-monitor
# Try different terminal
# iTerm2, Windows Terminal, or modern Linux terminals work best
```
### Cursor Remains Hidden After Exit
**Issue**: Terminal cursor invisible after Ctrl+C
**Quick fix**:
```bash
# Restore cursor
printf '\033[?25h'
# Or reset terminal completely
reset
```
## 🔄 Runtime and Performance Issues
### Monitor Startup Timeout
**Error**: `Timeout waiting for initial data`
**Causes and solutions**:
1. **Slow data loading**:
```bash
# Use custom timeout
# (Note: Not directly configurable, but data loads faster on subsequent runs)
# Check if Claude data exists
ls -la ~/.claude/projects/*.jsonl
```
2. **Large data files**:
```bash
# Monitor memory usage
top -p $(pgrep -f claude_monitor)
# Use quick start mode (automatically enabled)
claude-monitor # Loads only last 24 hours initially
```
### High CPU or Memory Usage
**Issue**: Monitor consuming too many resources
**Solutions**:
```bash
# Reduce refresh rate
claude-monitor --refresh-rate 30 # Data refresh every 30s
claude-monitor --refresh-per-second 0.5 # Display refresh at 0.5 Hz
# Monitor resource usage
htop | grep claude-monitor
```
### Thread and Callback Errors
**Error**: `Callback error: {error}` or `Session callback error: {error}`
**Debug approach**:
```bash
# Enable detailed logging
claude-monitor --debug --log-file /tmp/debug.log
# Check thread status
ps -T -p $(pgrep -f claude_monitor)
```
## 🔍 Advanced Debugging
### Enable Debug Mode
```bash
# Full debug output
claude-monitor --debug
# Debug with file logging
claude-monitor --debug --log-file ~/.claude-monitor/logs/debug.log
# Check logs
tail -f ~/.claude-monitor/logs/debug.log
```
### Validate Configuration
```bash
# Test configuration without starting monitor
python -c "
from claude_monitor.core.settings import Settings
try:
settings = Settings.load_with_last_used(['--plan', 'custom'])
print('Configuration valid')
print(f'Plan: {settings.plan}')
print(f'Theme: {settings.theme}')
print(f'Timezone: {settings.timezone}')
except Exception as e:
print(f'Configuration error: {e}')
"
```
### Check Data Path Discovery
```bash
# Test data path discovery
python -c "
from claude_monitor.cli.main import discover_claude_data_paths
paths = discover_claude_data_paths()
print(f'Found paths: {paths}')
for path in paths:
print(f' {path}: {len(list(path.glob(\"*.jsonl\")))} JSONL files')
"
```
### Validate JSONL Data Structure
```bash
# Check data structure
python -c "
from claude_monitor.data.reader import load_usage_entries
try:
entries, raw = load_usage_entries(include_raw=True)
print(f'Loaded {len(entries)} entries')
if entries:
print(f'Latest entry: {entries[-1].timestamp}')
print(f'Total tokens: {entries[-1].input_tokens + entries[-1].output_tokens}')
except Exception as e:
print(f'Data loading error: {e}')
"
```
### Test Pydantic Settings
```bash
# Test settings validation
python -c "
from claude_monitor.core.settings import Settings
from pydantic import ValidationError
test_cases = [
['--plan', 'invalid'],
['--theme', 'invalid'],
['--timezone', 'Invalid/Zone'],
['--refresh-rate', '0'],
['--refresh-per-second', '25'],
['--reset-hour', '24']
]
for case in test_cases:
try:
Settings.load_with_last_used(case)
print(f'{case}: Valid')
except ValidationError as e:
print(f'{case}: {e.errors()[0][\"msg\"]}')
except Exception as e:
print(f'{case}: {e}')
"
```
## 🆘 Getting Help
### Before Reporting Issues
1. **Check this guide first**
2. **Try with debug mode**: `claude-monitor --debug`
3. **Verify installation**: `pip show claude-monitor`
4. **Test with minimal config**: `claude-monitor --clear`
### Information to Include in Bug Reports
```bash
# System information
uname -a # Linux/Mac
systeminfo # Windows
# Python and package versions
python --version
pip show claude-monitor
# Installation method
which claude-monitor
echo $PATH
# Configuration test
claude-monitor --help
# Debug output (if possible)
claude-monitor --debug | head -20
```
### Issue Template
```markdown
**Problem**: Brief description
**Environment**:
- OS: [Ubuntu 24.04 / Windows 11 / macOS 14]
- Python: [3.11.0]
- Installation: [uv/pip/pipx/source]
- Version: [3.0.0]
**Steps to Reproduce**:
1. Command: `claude-monitor --plan custom`
2. Expected: ...
3. Actual: ...
**Error Output**:
```
Paste error messages here
```
**Debug Information**:
```
Output from: claude-monitor --debug | head -20
```
```
### Where to Get Help
1. **GitHub Issues**: [Create new issue](https://github.com/Maciek-roboblog/Claude-Code-Usage-Monitor/issues/new)
2. **Email**: [maciek@roboblog.eu](mailto:maciek@roboblog.eu)
3. **Documentation**: [README.md](README.md)
## 🔄 Complete Reset
If all else fails:
```bash
# 1. Uninstall completely
pip uninstall claude-monitor
uv tool uninstall claude-monitor # if using uv
pipx uninstall claude-monitor # if using pipx
# 2. Clear all configuration
rm -rf ~/.claude-monitor/
# 3. Clear Python cache
find . -name "*.pyc" -delete 2>/dev/null
find . -name "__pycache__" -delete 2>/dev/null
# 4. Fresh installation (choose one)
uv tool install claude-monitor # Recommended
# OR
pipx install claude-monitor # Alternative
# OR
python -m venv venv && source venv/bin/activate && pip install claude-monitor
# 5. Test installation
claude-monitor --help
claude-monitor --version
```
---
**Still having issues?** Don't hesitate to [create an issue](https://github.com/Maciek-roboblog/Claude-Code-Usage-Monitor/issues/new) with the **[v3.0.0]** tag in the title!
================================================
FILE: VERSION_MANAGEMENT.md
================================================
# Version Management System
## Overview
The Claude Code Usage Monitor uses a centralized version management system that eliminates version duplication and ensures consistency across the entire codebase.
## Single Source of Truth
**`pyproject.toml`** is the **only** place where the version number is defined:
```toml
[project]
version = "3.0.0"
```
## How It Works
### 1. Version Detection (`src/claude_monitor/_version.py`)
The version is retrieved using a two-tier fallback system:
1. **Primary**: Read from package metadata (when installed)
```python
importlib.metadata.version("claude-monitor")
```
2. **Fallback**: Read directly from `pyproject.toml` (development mode)
```python
# Uses tomllib (Python 3.11+) or tomli (Python < 3.11)
```
### 2. Module Import (`src/claude_monitor/__init__.py`)
```python
from claude_monitor._version import __version__
```
### 3. Usage Throughout Codebase
All modules import version from the main package:
```python
from claude_monitor import __version__
```
## Benefits
✅ **Single Source of Truth**: Version defined only in `pyproject.toml`
✅ **No Duplication**: Eliminates hardcoded versions in `__init__.py` files
✅ **Automatic Sync**: Version updates automatically propagate everywhere
✅ **Development Support**: Works both in installed and development environments
✅ **Build Integration**: Seamlessly integrates with build and release processes
## Dependencies
- **Python 3.11+**: Uses built-in `tomllib`
- **Python < 3.11**: Uses `tomli>=1.2.0` (automatically installed)
## Testing
Comprehensive test suite in `src/tests/test_version.py`:
- Version import consistency
- Fallback mechanism testing
- Integration with `pyproject.toml`
- Format validation
## Migration
### Before (Problems)
```python
# Multiple version definitions - sync issues!
# src/claude_monitor/__init__.py
__version__ = "2.5.0"
# pyproject.toml
version = "3.0.0" # Different version!
```
### After (Solution)
```python
# src/claude_monitor/__init__.py
from claude_monitor._version import __version__ # Always in sync!
# pyproject.toml
version = "3.0.0" # Single source of truth
```
## Release Process
1. **Update version in `pyproject.toml`** only
2. **All other files automatically reflect the new version**
3. **No manual updates needed anywhere else**
## Best Practices
- ✅ Update version only in `pyproject.toml`
- ✅ Use `from claude_monitor import __version__` in all modules
- ❌ Never hardcode version strings in source code
- ❌ Never define `__version__` in `__init__.py` files
This system ensures version consistency and eliminates the maintenance burden of keeping multiple version definitions synchronized.
================================================
FILE: pyproject.toml
================================================
# Automatically refactored pyproject.toml with best practices
[build-system]
requires = ["setuptools>=61.0.0", "wheel"]
build-backend = "setuptools.build_meta"
[project]
name = "claude-monitor"
version = "3.1.0"
description = "A real-time terminal monitoring tool for Claude Code token usage with advanced analytics and Rich UI"
readme = "README.md"
license = { text = "MIT" }
requires-python = ">=3.9"
authors = [{ name = "Maciek", email = "maciek@roboblog.eu" }]
maintainers = [{ name = "Maciek", email = "maciek@roboblog.eu" }]
keywords = [
"ai", "analytics", "claude", "dashboard",
"developer-tools", "monitoring", "rich",
"terminal", "token", "usage"
]
classifiers = [
"Development Status :: 5 - Production/Stable",
"Environment :: Console",
"Environment :: Console :: Curses",
"Intended Audience :: Developers",
"Topic :: Software Development :: Debuggers",
"Topic :: Software Development :: Libraries :: Python Modules",
"Topic :: System :: Monitoring",
"Topic :: Terminals",
"Topic :: Utilities",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3 :: Only",
"License :: OSI Approved :: MIT License",
"Operating System :: OS Independent",
"Operating System :: POSIX :: Linux",
"Operating System :: MacOS",
# "Operating System :: Microsoft :: Windows",
"Typing :: Typed"
]
dependencies = [
"numpy>=1.21.0",
"pydantic>=2.0.0",
"pydantic-settings>=2.0.0",
"pyyaml>=6.0",
"pytz>=2023.3",
"rich>=13.7.0",
"tomli>=1.2.0; python_version < '3.11'",
"tzdata; sys_platform == 'win32'"
]
[project.optional-dependencies]
dev = [
"black>=24.0.0",
"isort>=5.13.0",
"mypy>=1.13.0",
"pre-commit>=4.0.0",
"pytest>=8.0.0",
"pytest-asyncio>=0.24.0",
"pytest-benchmark>=4.0.0",
"pytest-cov>=6.0.0",
"pytest-mock>=3.14.0",
"pytest-xdist>=3.6.0",
"ruff>=0.12.0",
"build>=0.10.0",
"twine>=4.0.0"
]
test = [
"pytest>=8.0.0",
"pytest-cov>=6.0.0",
"pytest-mock>=3.14.0",
"pytest-asyncio>=0.24.0",
"pytest-benchmark>=4.0.0"
]
[project.urls]
homepage = "https://github.com/Maciek-roboblog/Claude-Code-Usage-Monitor"
repository = "https://github.com/Maciek-roboblog/Claude-Code-Usage-Monitor.git"
documentation = "https://github.com/Maciek-roboblog/Claude-Code-Usage-Monitor#readme"
issues = "https://github.com/Maciek-roboblog/Claude-Code-Usage-Monitor/issues"
changelog = "https://github.com/Maciek-roboblog/Claude-Code-Usage-Monitor/blob/main/CHANGELOG.md"
"Release Notes" = "https://github.com/Maciek-roboblog/Claude-Code-Usage-Monitor/releases"
"Discussions" = "https://github.com/Maciek-roboblog/Claude-Code-Usage-Monitor/discussions"
[project.scripts]
claude-monitor = "claude_monitor.__main__:main"
claude-code-monitor = "claude_monitor.__main__:main"
cmonitor = "claude_monitor.__main__:main"
ccmonitor = "claude_monitor.__main__:main"
ccm = "claude_monitor.__main__:main"
[tool.setuptools.packages.find]
where = ["src"]
include = ["claude_monitor*"]
exclude = ["tests*", "src/tests*"]
[tool.setuptools.package-data]
claude_monitor = ["py.typed"]
[tool.black]
line-length = 88
target-version = ["py39", "py310", "py311", "py312"]
skip-string-normalization = false
include = '\.pyi?$'
extend-exclude = '''
/(
\.eggs
| \.git
| \.hg
| \.tox
| \.venv
| build
| dist
)/
'''
[tool.isort]
profile = "black"
line_length = 88
known_first_party = ["claude_monitor"]
force_single_line = true
atomic = true
include_trailing_comma = true
lines_after_imports = 2
lines_between_types = 1
use_parentheses = true
src_paths = ["src"]
skip_glob = ["*/migrations/*", "*/venv/*", "*/build/*", "*/dist/*"]
[tool.ruff]
line-length = 88
target-version = "py39"
[tool.ruff.lint]
select = ["E", "W", "F", "I"] # pycodestyle + Pyflakes + isort
ignore = ["E501"] # Line length handled by formatter
[tool.ruff.format]
quote-style = "double"
[tool.mypy]
python_version = "3.9"
warn_return_any = true # Catch unintended Any returns
warn_no_return = true # Ensure functions return as expected
strict_optional = true # Disallow None where not annotated
disable_error_code = [
"attr-defined", # Attribute existence
"name-defined", # Name resolution
"import", # Import errors
"misc", # Misc issues
]
[tool.pytest.ini_options]
minversion = "7.0"
testpaths = ["src/tests"]
python_files = ["test_*.py","*_test.py"]
python_classes = ["Test*"]
python_functions = ["test_*"]
addopts = [
"--strict-markers","--strict-config","--color=yes","--tb=short",
"--cov=claude_monitor","--cov-report=term-missing","--cov-report=html",
"--cov-report=xml","--cov-fail-under=70","--no-cov-on-fail","-ra","-q",
"-m","not integration"
]
markers = [
"slow: marks tests as slow (deselect with '-m \"not slow\"')",
"unit: marks tests as unit tests",
"integration: marks tests as integration tests",
"benchmark: marks tests as benchmarks",
"network: marks tests as requiring network access",
"subprocess: marks tests as requiring subprocess"
]
filterwarnings = [
"error",
"ignore::UserWarning",
"ignore::DeprecationWarning",
"ignore::PendingDeprecationWarning"
]
[tool.coverage.run]
branch = true
source = ["src/claude_monitor"]
omit = ["*/tests/*","*/test_*","*/__main__.py","*/conftest.py"]
[tool.coverage.report]
exclude_lines = [
"pragma: no cover",
"def __repr__",
"if self.debug:",
"if settings.DEBUG",
"raise AssertionError",
"raise NotImplementedError",
"if 0:",
"if __name__ == .__main__.:",
"class .*\\bProtocol\\):",
"@(abc\\.)?abstractmethod"
]
show_missing = true
skip_empty = false
precision = 2
[tool.coverage.html]
directory = "htmlcov"
[tool.coverage.xml]
output = "coverage.xml"
================================================
FILE: src/claude_monitor/__init__.py
================================================
"""Claude Monitor - Real-time token usage monitoring for Claude AI"""
from claude_monitor._version import __version__
__all__ = ["__version__"]
================================================
FILE: src/claude_monitor/__main__.py
================================================
#!/usr/bin/env python3
"""Module execution entry point for Claude Monitor.
Allows running the package as a module: python -m claude_monitor
"""
import sys
from typing import NoReturn
from .cli.main import main
def _main() -> NoReturn:
"""Entry point that properly handles exit codes and never returns."""
exit_code = main()
sys.exit(exit_code)
if __name__ == "__main__":
_main()
================================================
FILE: src/claude_monitor/_version.py
================================================
"""Version management utilities.
This module provides centralized version management that reads from pyproject.toml
as the single source of truth, avoiding version duplication across the codebase.
"""
import importlib.metadata
import sys
from pathlib import Path
from typing import Any, Dict, Optional, Union
def get_version() -> str:
"""Get version from package metadata.
This reads the version from the installed package metadata,
which is set from pyproject.toml during build/installation.
Returns:
Version string (e.g., "3.0.0")
"""
try:
return importlib.metadata.version("claude-monitor")
except importlib.metadata.PackageNotFoundError:
# Fallback for development environments where package isn't installed
return _get_version_from_pyproject()
def _get_version_from_pyproject() -> str:
"""Fallback: read version directly from pyproject.toml.
This is used when the package isn't installed (e.g., development mode).
Returns:
Version string or "unknown" if cannot be determined
"""
try:
# Python 3.11+
import tomllib
except ImportError:
try:
# Python < 3.11 fallback
import tomli as tomllib # type: ignore[import-untyped]
except ImportError:
# No TOML library available
return "unknown"
try:
# Find pyproject.toml - go up from this file's directory
current_dir = Path(__file__).parent
for _ in range(5): # Max 5 levels up
pyproject_path = current_dir / "pyproject.toml"
if pyproject_path.exists():
with open(pyproject_path, "rb") as f:
data: Dict[str, Any] = tomllib.load(f)
project_data: Dict[str, Any] = data.get("project", {})
version: str = project_data.get("version", "unknown")
return version
current_dir = current_dir.parent
return "unknown"
except Exception:
return "unknown"
def get_package_info() -> Dict[str, Optional[str]]:
"""Get comprehensive package information.
Returns:
Dictionary containing version, name, and metadata
"""
try:
metadata = importlib.metadata.metadata("claude-monitor")
return {
"version": get_version(),
"name": metadata.get("Name"),
"author": metadata.get("Author"),
"author_email": metadata.get("Author-email"),
"description": metadata.get("Summary"),
"home_page": metadata.get("Home-page"),
"license": metadata.get("License"),
}
except importlib.metadata.PackageNotFoundError:
return {
"version": _get_version_from_pyproject(),
"name": "claude-monitor",
"author": None,
"author_email": None,
"description": None,
"home_page": None,
"license": None,
}
def get_version_info() -> Dict[str, Any]:
"""Get detailed version and system information.
Returns:
Dictionary containing version, Python version, and system info
"""
return {
"version": get_version(),
"python_version": sys.version,
"python_version_info": {
"major": sys.version_info.major,
"minor": sys.version_info.minor,
"micro": sys.version_info.micro,
},
"platform": sys.platform,
"executable": sys.executable,
"package_info": get_package_info(),
}
def find_project_root(start_path: Optional[Union[str, Path]] = None) -> Optional[Path]:
"""Find the project root directory containing pyproject.toml.
Args:
start_path: Starting directory for search (defaults to current file location)
Returns:
Path to project root or None if not found
"""
if start_path is None:
current_dir = Path(__file__).parent
else:
current_dir = Path(start_path).resolve()
# Search up to 10 levels to find pyproject.toml
for _ in range(10):
if (current_dir / "pyproject.toml").exists():
return current_dir
parent = current_dir.parent
if parent == current_dir: # Reached filesystem root
break
current_dir = parent
return None
# Module-level version constant
__version__: str = get_version()
================================================
FILE: src/claude_monitor/cli/__init__.py
================================================
"""Claude Monitor CLI package."""
from .main import main
__all__ = ["main"]
================================================
FILE: src/claude_monitor/cli/bootstrap.py
================================================
"""Bootstrap utilities for CLI initialization."""
import logging
import os
import sys
from logging import Handler
from pathlib import Path
from typing import List, Optional
from claude_monitor.utils.time_utils import TimezoneHandler
def setup_logging(
level: str = "INFO", log_file: Optional[Path] = None, disable_console: bool = False
) -> None:
"""Configure logging for the application.
Args:
level: Log level (DEBUG, INFO, WARNING, ERROR, CRITICAL)
log_file: Optional file path for logging
disable_console: If True, disable console logging (useful for monitor mode)
"""
log_level = getattr(logging, level.upper(), logging.INFO)
handlers: List[Handler] = []
if not disable_console:
handlers.append(logging.StreamHandler(sys.stdout))
if log_file:
handlers.append(logging.FileHandler(log_file))
if not handlers:
handlers.append(logging.NullHandler())
logging.basicConfig(
level=log_level,
format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
handlers=handlers,
)
def setup_environment() -> None:
"""Initialize environment variables and system settings."""
if sys.stdout.encoding != "utf-8":
if hasattr(sys.stdout, "reconfigure"):
sys.stdout.reconfigure(encoding="utf-8") # type: ignore[attr-defined]
os.environ.setdefault(
"CLAUDE_MONITOR_CONFIG", str(Path.home() / ".claude-monitor" / "config.yaml")
)
os.environ.setdefault(
"CLAUDE_MONITOR_CACHE_DIR", str(Path.home() / ".claude-monitor" / "cache")
)
def init_timezone(timezone: str = "Europe/Warsaw") -> TimezoneHandler:
"""Initialize timezone handler.
Args:
timezone: Timezone string (e.g. "Europe/Warsaw", "UTC")
Returns:
Configured TimezoneHandler instance
"""
tz_handler = TimezoneHandler()
if timezone != "Europe/Warsaw":
tz_handler.set_timezone(timezone)
return tz_handler
def ensure_directories() -> None:
"""Ensure required directories exist."""
dirs = [
Path.home() / ".claude-monitor",
Path.home() / ".claude-monitor" / "cache",
Path.home() / ".claude-monitor" / "logs",
Path.home() / ".claude-monitor" / "reports",
]
for directory in dirs:
directory.mkdir(parents=True, exist_ok=True)
================================================
FILE: src/claude_monitor/cli/main.py
================================================
"""Simplified CLI entry point using pydantic-settings."""
import argparse
import contextlib
import logging
import signal
import sys
import time
import traceback
from pathlib import Path
from typing import Any, Callable, Dict, List, NoReturn, Optional, Union
from rich.console import Console
from claude_monitor import __version__
from claude_monitor.cli.bootstrap import (
ensure_directories,
init_timezone,
setup_environment,
setup_logging,
)
from claude_monitor.core.plans import Plans, PlanType, get_token_limit
from claude_monitor.core.settings import Settings
from claude_monitor.data.aggregator import UsageAggregator
from claude_monitor.data.analysis import analyze_usage
from claude_monitor.error_handling import report_error
from claude_monitor.monitoring.orchestrator import MonitoringOrchestrator
from claude_monitor.terminal.manager import (
enter_alternate_screen,
handle_cleanup_and_exit,
handle_error_and_exit,
restore_terminal,
setup_terminal,
)
from claude_monitor.terminal.themes import get_themed_console, print_themed
from claude_monitor.ui.display_controller import DisplayController
from claude_monitor.ui.table_views import TableViewsController
# Type aliases for CLI callbacks
DataUpdateCallback = Callable[[Dict[str, Any]], None]
SessionChangeCallback = Callable[[str, str, Optional[Dict[str, Any]]], None]
def get_standard_claude_paths() -> List[str]:
"""Get list of standard Claude data directory paths to check."""
return ["~/.claude/projects", "~/.config/claude/projects"]
def discover_claude_data_paths(custom_paths: Optional[List[str]] = None) -> List[Path]:
"""Discover all available Claude data directories.
Args:
custom_paths: Optional list of custom paths to check instead of standard ones
Returns:
List of Path objects for existing Claude data directories
"""
paths_to_check: List[str] = (
[str(p) for p in custom_paths] if custom_paths else get_standard_claude_paths()
)
discovered_paths: List[Path] = []
for path_str in paths_to_check:
path = Path(path_str).expanduser().resolve()
if path.exists() and path.is_dir():
discovered_paths.append(path)
return discovered_paths
def main(argv: Optional[List[str]] = None) -> int:
"""Main entry point with direct pydantic-settings integration."""
if argv is None:
argv = sys.argv[1:]
if "--version" in argv or "-v" in argv:
print(f"claude-monitor {__version__}")
return 0
try:
settings = Settings.load_with_last_used(argv)
setup_environment()
ensure_directories()
if settings.log_file:
setup_logging(settings.log_level, settings.log_file, disable_console=True)
else:
setup_logging(settings.log_level, disable_console=True)
init_timezone(settings.timezone)
args = settings.to_namespace()
_run_monitoring(args)
return 0
except KeyboardInterrupt:
print("\n\nMonitoring stopped by user.")
return 0
except Exception as e:
logger = logging.getLogger(__name__)
logger.error(f"Monitor failed: {e}", exc_info=True)
traceback.print_exc()
return 1
def _run_monitoring(args: argparse.Namespace) -> None:
"""Main monitoring implementation without facade."""
view_mode = getattr(args, "view", "realtime")
if hasattr(args, "theme") and args.theme:
console = get_themed_console(force_theme=args.theme.lower())
else:
console = get_themed_console()
old_terminal_settings = setup_terminal()
live_display_active: bool = False
try:
data_paths: List[Path] = discover_claude_data_paths()
if not data_paths:
print_themed("No Claude data directory found", style="error")
return
data_path: Path = data_paths[0]
logger = logging.getLogger(__name__)
logger.info(f"Using data path: {data_path}")
# Handle different view modes
if view_mode in ["daily", "monthly"]:
_run_table_view(args, data_path, view_mode, console)
return
token_limit: int = _get_initial_token_limit(args, str(data_path))
display_controller = DisplayController()
display_controller.live_manager._console = console
refresh_per_second: float = getattr(args, "refresh_per_second", 0.75)
logger.info(
f"Display refresh rate: {refresh_per_second} Hz ({1000 / refresh_per_second:.0f}ms)"
)
logger.info(f"Data refresh rate: {args.refresh_rate} seconds")
live_display = display_controller.live_manager.create_live_display(
auto_refresh=True, console=console, refresh_per_second=refresh_per_second
)
loading_display = display_controller.create_loading_display(
args.plan, args.timezone
)
enter_alternate_screen()
live_display_active = False
try:
# Enter live context and show loading screen immediately
live_display.__enter__()
live_display_active = True
live_display.update(loading_display)
orchestrator = MonitoringOrchestrator(
update_interval=(
args.refresh_rate if hasattr(args, "refresh_rate") else 10
),
data_path=str(data_path),
)
orchestrator.set_args(args)
# Setup monitoring callback
def on_data_update(monitoring_data: Dict[str, Any]) -> None:
"""Handle data updates from orchestrator."""
try:
data: Dict[str, Any] = monitoring_data.get("data", {})
blocks: List[Dict[str, Any]] = data.get("blocks", [])
logger.debug(f"Display data has {len(blocks)} blocks")
if blocks:
active_blocks: List[Dict[str, Any]] = [
b for b in blocks if b.get("isActive")
]
logger.debug(f"Active blocks: {len(active_blocks)}")
if active_blocks:
total_tokens: int = active_blocks[0].get("totalTokens", 0)
logger.debug(f"Active block tokens: {total_tokens}")
renderable = display_controller.create_data_display(
data, args, monitoring_data.get("token_limit", token_limit)
)
if live_display:
live_display.update(renderable)
except Exception as e:
logger.error(f"Display update error: {e}", exc_info=True)
report_error(
exception=e,
component="cli_main",
context_name="display_update_error",
)
# Register callbacks
orchestrator.register_update_callback(on_data_update)
# Optional: Register session change callback
def on_session_change(
event_type: str, session_id: str, session_data: Optional[Dict[str, Any]]
) -> None:
"""Handle session changes."""
if event_type == "session_start":
logger.info(f"New session detected: {session_id}")
elif event_type == "session_end":
logger.info(f"Session ended: {session_id}")
orchestrator.register_session_callback(on_session_change)
# Start monitoring
orchestrator.start()
# Wait for initial data
logger.info("Waiting for initial data...")
if not orchestrator.wait_for_initial_data(timeout=10.0):
logger.warning("Timeout waiting for initial data")
# Main loop - live display is already active
# Use signal.pause() for more efficient waiting
try:
signal.pause()
except AttributeError:
# Fallback for Windows which doesn't support signal.pause()
while True:
time.sleep(1)
finally:
# Stop monitoring first
if "orchestrator" in locals():
orchestrator.stop()
# Exit live display context if it was activated
if live_display_active:
with contextlib.suppress(Exception):
live_display.__exit__(None, None, None)
except KeyboardInterrupt:
# Clean exit from live display if it's active
if "live_display" in locals():
with contextlib.suppress(Exception):
live_display.__exit__(None, None, None)
handle_cleanup_and_exit(old_terminal_settings)
except Exception as e:
# Clean exit from live display if it's active
if "live_display" in locals():
with contextlib.suppress(Exception):
live_display.__exit__(None, None, None)
handle_error_and_exit(old_terminal_settings, e)
finally:
restore_terminal(old_terminal_settings)
def _get_initial_token_limit(
args: argparse.Namespace, data_path: Union[str, Path]
) -> int:
"""Get initial token limit for the plan."""
logger = logging.getLogger(__name__)
plan: str = getattr(args, "plan", PlanType.PRO.value)
# For custom plans, check if custom_limit_tokens is provided first
if plan == "custom":
# If custom_limit_tokens is explicitly set, use it
if hasattr(args, "custom_limit_tokens") and args.custom_limit_tokens:
custom_limit = int(args.custom_limit_tokens)
print_themed(
f"Using custom token limit: {custom_limit:,} tokens",
style="info",
)
return custom_limit
# Otherwise, analyze usage data to calculate P90
print_themed("Analyzing usage data to determine cost limits...", style="info")
try:
# Use quick start mode for faster initial load
usage_data: Optional[Dict[str, Any]] = analyze_usage(
hours_back=96 * 2,
quick_start=False,
use_cache=False,
data_path=str(data_path),
)
if usage_data and "blocks" in usage_data:
blocks: List[Dict[str, Any]] = usage_data["blocks"]
token_limit: int = get_token_limit(plan, blocks)
print_themed(
f"P90 session limit calculated: {token_limit:,} tokens",
style="info",
)
return token_limit
except Exception as e:
logger.warning(f"Failed to analyze usage data: {e}")
# Fallback to default limit
print_themed("Using default limit as fallback", style="warning")
return Plans.DEFAULT_TOKEN_LIMIT
# For standard plans, just get the limit
return get_token_limit(plan)
def handle_application_error(
exception: Exception,
component: str = "cli_main",
exit_code: int = 1,
) -> NoReturn:
"""Handle application-level errors with proper logging and exit.
Args:
exception: The exception that occurred
component: Component where the error occurred
exit_code: Exit code to use when terminating
"""
logger = logging.getLogger(__name__)
# Log the error with traceback
logger.error(f"Application error in {component}: {exception}", exc_info=True)
# Report to error handling system
from claude_monitor.error_handling import report_application_startup_error
report_application_startup_error(
exception=exception,
component=component,
additional_context={
"exit_code": exit_code,
"args": sys.argv,
},
)
# Print user-friendly error message
print(f"\nError: {exception}", file=sys.stderr)
print("For more details, check the log files.", file=sys.stderr)
sys.exit(exit_code)
def validate_cli_environment() -> Optional[str]:
"""Validate the CLI environment and return error message if invalid.
Returns:
Error message if validation fails, None if successful
"""
try:
# Check Python version compatibility
if sys.version_info < (3, 8):
return f"Python 3.8+ required, found {sys.version_info.major}.{sys.version_info.minor}"
# Check for required dependencies
required_modules = ["rich", "pydantic", "watchdog"]
missing_modules: List[str] = []
for module in required_modules:
try:
__import__(module)
except ImportError:
missing_modules.append(module)
if missing_modules:
return f"Missing required modules: {', '.join(missing_modules)}"
return None
except Exception as e:
return f"Environment validation failed: {e}"
def _run_table_view(
args: argparse.Namespace, data_path: Path, view_mode: str, console: Console
) -> None:
"""Run table view mode (daily/monthly)."""
logger = logging.getLogger(__name__)
try:
# Create aggregator with appropriate mode
aggregator = UsageAggregator(
data_path=str(data_path),
aggregation_mode=view_mode,
timezone=args.timezone,
)
# Create table controller
controller = TableViewsController(console=console)
# Get aggregated data
logger.info(f"Loading {view_mode} usage data...")
aggregated_data = aggregator.aggregate()
if not aggregated_data:
print_themed(f"No usage data found for {view_mode} view", style="warning")
return
# Display the table
controller.display_aggregated_view(
data=aggregated_data,
view_mode=view_mode,
timezone=args.timezone,
plan=args.plan,
token_limit=_get_initial_token_limit(args, data_path),
)
# Wait for user to press Ctrl+C
print_themed("\nPress Ctrl+C to exit", style="info")
try:
# Use signal.pause() for more efficient waiting
try:
signal.pause()
except AttributeError:
# Fallback for Windows which doesn't support signal.pause()
while True:
time.sleep(1)
except KeyboardInterrupt:
print_themed("\nExiting...", style="info")
except Exception as e:
logger.error(f"Error in table view: {e}", exc_info=True)
print_themed(f"Error displaying {view_mode} view: {e}", style="error")
if __name__ == "__main__":
sys.exit(main())
================================================
FILE: src/claude_monitor/core/__init__.py
================================================
"""Core package for Claude Monitor.
This module provides the core functionality for Claude usage monitoring,
including models, calculations, pricing, and session management.
"""
__all__: list[str] = []
================================================
FILE: src/claude_monitor/core/calculations.py
================================================
"""Burn rate and cost calculations for Claude Monitor."""
import logging
from datetime import datetime, timedelta, timezone
from typing import Any, Dict, List, Optional, Protocol
from claude_monitor.core.models import (
BurnRate,
TokenCounts,
UsageProjection,
)
from claude_monitor.core.p90_calculator import P90Calculator
from claude_monitor.error_handling import report_error
from claude_monitor.utils.time_utils import TimezoneHandler
logger: logging.Logger = logging.getLogger(__name__)
_p90_calculator: P90Calculator = P90Calculator()
class BlockLike(Protocol):
"""Protocol for objects that behave like session blocks."""
is_active: bool
duration_minutes: float
token_counts: TokenCounts
cost_usd: float
end_time: datetime
class BurnRateCalculator:
"""Calculates burn rates and usage projections for session blocks."""
def calculate_burn_rate(self, block: BlockLike) -> Optional[BurnRate]:
"""Calculate current consumption rate for active blocks."""
if not block.is_active or block.duration_minutes < 1:
return None
total_tokens = (
block.token_counts.input_tokens
+ block.token_counts.output_tokens
+ block.token_counts.cache_creation_tokens
+ block.token_counts.cache_read_tokens
)
if total_tokens == 0:
return None
tokens_per_minute = total_tokens / block.duration_minutes
cost_per_hour = (
(block.cost_usd / block.duration_minutes) * 60
if block.duration_minutes > 0
else 0
)
return BurnRate(
tokens_per_minute=tokens_per_minute, cost_per_hour=cost_per_hour
)
def project_block_usage(self, block: BlockLike) -> Optional[UsageProjection]:
"""Project total usage if current rate continues."""
burn_rate = self.calculate_burn_rate(block)
if not burn_rate:
return None
now = datetime.now(timezone.utc)
remaining_seconds = (block.end_time - now).total_seconds()
if remaining_seconds <= 0:
return None
remaining_minutes = remaining_seconds / 60
remaining_hours = remaining_minutes / 60
current_tokens = (
block.token_counts.input_tokens
+ block.token_counts.output_tokens
+ block.token_counts.cache_creation_tokens
+ block.token_counts.cache_read_tokens
)
current_cost = block.cost_usd
projected_additional_tokens = burn_rate.tokens_per_minute * remaining_minutes
projected_total_tokens = current_tokens + projected_additional_tokens
projected_additional_cost = burn_rate.cost_per_hour * remaining_hours
projected_total_cost = current_cost + projected_additional_cost
return UsageProjection(
projected_total_tokens=int(projected_total_tokens),
projected_total_cost=projected_total_cost,
remaining_minutes=int(remaining_minutes),
)
def calculate_hourly_burn_rate(
blocks: List[Dict[str, Any]], current_time: datetime
) -> float:
"""Calculate burn rate based on all sessions in the last hour."""
if not blocks:
return 0.0
one_hour_ago = current_time - timedelta(hours=1)
total_tokens = _calculate_total_tokens_in_hour(blocks, one_hour_ago, current_time)
return total_tokens / 60.0 if total_tokens > 0 else 0.0
def _calculate_total_tokens_in_hour(
blocks: List[Dict[str, Any]], one_hour_ago: datetime, current_time: datetime
) -> float:
"""Calculate total tokens for all blocks in the last hour."""
total_tokens = 0.0
for block in blocks:
total_tokens += _process_block_for_burn_rate(block, one_hour_ago, current_time)
return total_tokens
def _process_block_for_burn_rate(
block: Dict[str, Any], one_hour_ago: datetime, current_time: datetime
) -> float:
"""Process a single block for burn rate calculation."""
start_time = _parse_block_start_time(block)
if not start_time or block.get("isGap", False):
return 0
session_actual_end = _determine_session_end_time(block, current_time)
if session_actual_end < one_hour_ago:
return 0
return _calculate_tokens_in_hour(
block, start_time, session_actual_end, one_hour_ago, current_time
)
def _parse_block_start_time(block: Dict[str, Any]) -> Optional[datetime]:
"""Parse start time from block with error handling."""
start_time_str = block.get("startTime")
if not start_time_str:
return None
tz_handler = TimezoneHandler()
try:
start_time = tz_handler.parse_timestamp(start_time_str)
return tz_handler.ensure_utc(start_time)
except (ValueError, TypeError, AttributeError) as e:
_log_timestamp_error(e, start_time_str, block.get("id"), "start_time")
return None
def _determine_session_end_time(
block: Dict[str, Any], current_time: datetime
) -> datetime:
"""Determine session end time based on block status."""
if block.get("isActive", False):
return current_time
actual_end_str = block.get("actualEndTime")
if actual_end_str:
tz_handler = TimezoneHandler()
try:
session_actual_end = tz_handler.parse_timestamp(actual_end_str)
return tz_handler.ensure_utc(session_actual_end)
except (ValueError, TypeError, AttributeError) as e:
_log_timestamp_error(e, actual_end_str, block.get("id"), "actual_end_time")
return current_time
def _calculate_tokens_in_hour(
block: Dict[str, Any],
start_time: datetime,
session_actual_end: datetime,
one_hour_ago: datetime,
current_time: datetime,
) -> float:
"""Calculate tokens used within the last hour for this session."""
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:
return 0
total_session_duration = (session_actual_end - start_time).total_seconds() / 60
hour_duration = (session_end_in_hour - session_start_in_hour).total_seconds() / 60
if total_session_duration > 0:
session_tokens = block.get("totalTokens", 0)
return session_tokens * (hour_duration / total_session_duration)
return 0
def _log_timestamp_error(
exception: Exception,
timestamp_str: str,
block_id: Optional[str],
timestamp_type: str,
) -> None:
"""Log timestamp parsing errors with context."""
logging.debug(f"Failed to parse {timestamp_type} '{timestamp_str}': {exception}")
report_error(
exception=exception,
component="burn_rate_calculator",
context_name="timestamp_error",
context_data={f"{timestamp_type}_str": timestamp_str, "block_id": block_id},
)
================================================
FILE: src/claude_monitor/core/data_processors.py
================================================
"""Centralized data processing utilities for Claude Monitor.
This module provides unified data processing functionality to eliminate
code duplication across different components.
"""
from datetime import datetime
from typing import Any, Dict, List, Optional, Union
from claude_monitor.utils.time_utils import TimezoneHandler
class TimestampProcessor:
"""Unified timestamp parsing and processing utilities."""
def __init__(self, timezone_handler: Optional[TimezoneHandler] = None) -> None:
"""Initialize with optional timezone handler."""
self.timezone_handler: TimezoneHandler = timezone_handler or TimezoneHandler()
def parse_timestamp(
self, timestamp_value: Union[str, int, float, datetime, None]
) -> Optional[datetime]:
"""Parse timestamp from various formats to UTC datetime.
Args:
timestamp_value: Timestamp in various formats (str, int, float, datetime)
Returns:
Parsed UTC datetime or None if parsing fails
"""
if timestamp_value is None:
return None
try:
if isinstance(timestamp_value, datetime):
return self.timezone_handler.ensure_timezone(timestamp_value)
if isinstance(timestamp_value, str):
if timestamp_value.endswith("Z"):
timestamp_value = timestamp_value[:-1] + "+00:00"
try:
dt = datetime.fromisoformat(timestamp_value)
return self.timezone_handler.ensure_timezone(dt)
except ValueError:
pass
for fmt in ["%Y-%m-%dT%H:%M:%S.%f", "%Y-%m-%dT%H:%M:%S"]:
try:
dt = datetime.strptime(timestamp_value, fmt)
return self.timezone_handler.ensure_timezone(dt)
except ValueError:
continue
if isinstance(timestamp_value, (int, float)):
dt = datetime.fromtimestamp(timestamp_value)
return self.timezone_handler.ensure_timezone(dt)
except Exception:
pass
return None
class TokenExtractor:
"""Unified token extraction utilities."""
@staticmethod
def extract_tokens(data: Dict[str, Any]) -> Dict[str, int]:
"""Extract token counts from data in standardized format.
Args:
data: Data dictionary with token information
Returns:
Dictionary with standardized token keys and counts
"""
import logging
logger = logging.getLogger(__name__)
tokens: Dict[str, int] = {
"input_tokens": 0,
"output_tokens": 0,
"cache_creation_tokens": 0,
"cache_read_tokens": 0,
"total_tokens": 0,
}
token_sources: List[Dict[str, Any]] = []
is_assistant: bool = data.get("type") == "assistant"
if is_assistant:
if (
"message" in data
and isinstance(data["message"], dict)
and "usage" in data["message"]
):
token_sources.append(data["message"]["usage"])
if "usage" in data:
token_sources.append(data["usage"])
token_sources.append(data)
else:
if "usage" in data:
token_sources.append(data["usage"])
if (
"message" in data
and isinstance(data["message"], dict)
and "usage" in data["message"]
):
token_sources.append(data["message"]["usage"])
token_sources.append(data)
logger.debug(f"TokenExtractor: Checking {len(token_sources)} token sources")
for source in token_sources:
if not isinstance(source, dict):
continue
input_tokens = (
source.get("input_tokens", 0)
or source.get("inputTokens", 0)
or source.get("prompt_tokens", 0)
or 0
)
output_tokens = (
source.get("output_tokens", 0)
or source.get("outputTokens", 0)
or source.get("completion_tokens", 0)
or 0
)
cache_creation = (
source.get("cache_creation_tokens", 0)
or source.get("cache_creation_input_tokens", 0)
or source.get("cacheCreationInputTokens", 0)
or 0
)
cache_read = (
source.get("cache_read_input_tokens", 0)
or source.get("cache_read_tokens", 0)
or source.get("cacheReadInputTokens", 0)
or 0
)
if input_tokens > 0 or output_tokens > 0:
tokens.update(
{
"input_tokens": int(input_tokens),
"output_tokens": int(output_tokens),
"cache_creation_tokens": int(cache_creation),
"cache_read_tokens": int(cache_read),
"total_tokens": int(
input_tokens + output_tokens + cache_creation + cache_read
),
}
)
logger.debug(
f"TokenExtractor: Found tokens - input={input_tokens}, output={output_tokens}, cache_creation={cache_creation}, cache_read={cache_read}"
)
break
logger.debug(
f"TokenExtractor: No valid tokens in source: {list(source.keys()) if isinstance(source, dict) else 'not a dict'}"
)
return tokens
class DataConverter:
"""Unified data conversion utilities."""
@staticmethod
def flatten_nested_dict(data: Dict[str, Any], prefix: str = "") -> Dict[str, Any]:
"""Flatten nested dictionary structure.
Args:
data: Nested dictionary
prefix: Prefix for flattened keys
Returns:
Flattened dictionary
"""
result: Dict[str, Any] = {}
for key, value in data.items():
new_key = f"{prefix}.{key}" if prefix else key
if isinstance(value, dict):
result.update(DataConverter.flatten_nested_dict(value, new_key))
else:
result[new_key] = value
return result
@staticmethod
def extract_model_name(
data: Dict[str, Any], default: str = "claude-3-5-sonnet"
) -> str:
"""Extract model name from various data sources.
Args:
data: Data containing model information
default: Default model name if not found
Returns:
Extracted model name
"""
model_candidates: List[Optional[Any]] = [
data.get("message", {}).get("model"),
data.get("model"),
data.get("Model"),
data.get("usage", {}).get("model"),
data.get("request", {}).get("model"),
]
for candidate in model_candidates:
if candidate and isinstance(candidate, str):
return candidate
return default
@staticmethod
def to_serializable(obj: Any) -> Any:
"""Convert object to JSON-serializable format.
Args:
obj: Object to convert
Returns:
JSON-serializable representation
"""
if isinstance(obj, datetime):
return obj.isoformat()
if isinstance(obj, dict):
return {k: DataConverter.to_serializable(v) for k, v in obj.items()}
if isinstance(obj, (list, tuple)):
return [DataConverter.to_serializable(item) for item in obj]
return obj
================================================
FILE: src/claude_monitor/core/models.py
================================================
"""Data models for Claude Monitor.
Core data structures for usage tracking, session management, and token calculations.
"""
from dataclasses import dataclass, field
from datetime import datetime
from enum import Enum
from typing import Any, Dict, List, Optional
class CostMode(Enum):
"""Cost calculation modes for token usage analysis."""
AUTO = "auto"
CACHED = "cached"
CALCULATED = "calculate"
@dataclass
class UsageEntry:
"""Individual usage record from Claude usage data."""
timestamp: datetime
input_tokens: int
output_tokens: int
cache_creation_tokens: int = 0
cache_read_tokens: int = 0
cost_usd: float = 0.0
model: str = ""
message_id: str = ""
request_id: str = ""
@dataclass
class TokenCounts:
"""Token aggregation structure with computed totals."""
input_tokens: int = 0
output_tokens: int = 0
cache_creation_tokens: int = 0
cache_read_tokens: int = 0
@property
def total_tokens(self) -> int:
"""Get total tokens across all types."""
return (
self.input_tokens
+ self.output_tokens
+ self.cache_creation_tokens
+ self.cache_read_tokens
)
@dataclass
class BurnRate:
"""Token consumption rate metrics."""
tokens_per_minute: float
cost_per_hour: float
@dataclass
class UsageProjection:
"""Usage projection calculations for active blocks."""
projected_total_tokens: int
projected_total_cost: float
remaining_minutes: float
@dataclass
class SessionBlock:
"""Aggregated session block representing a 5-hour period."""
id: str
start_time: datetime
end_time: datetime
entries: List[UsageEntry] = field(default_factory=list)
token_counts: TokenCounts = field(default_factory=TokenCounts)
is_active: bool = False
is_gap: bool = False
burn_rate: Optional[BurnRate] = None
actual_end_time: Optional[datetime] = None
per_model_stats: Dict[str, Dict[str, Any]] = field(default_factory=dict)
models: List[str] = field(default_factory=list)
sent_messages_count: int = 0
cost_usd: float = 0.0
limit_messages: List[Dict[str, Any]] = field(default_factory=list)
projection_data: Optional[Dict[str, Any]] = None
burn_rate_snapshot: Optional[BurnRate] = None
@property
def total_tokens(self) -> int:
"""Get total tokens from token_counts."""
return self.token_counts.total_tokens
@property
def total_cost(self) -> float:
"""Get total cost - alias for cost_usd."""
return self.cost_usd
@property
def duration_minutes(self) -> float:
"""Get duration in minutes."""
if self.actual_end_time:
duration = (self.actual_end_time - self.start_time).total_seconds() / 60
else:
duration = (self.end_time - self.start_time).total_seconds() / 60
return max(duration, 1.0)
def normalize_model_name(model: str) -> str:
"""Normalize model name for consistent usage across the application.
Handles various model name formats and maps them to standard keys.
(Moved from utils/model_utils.py)
Args:
model: Raw model name from usage data
Returns:
Normalized model key
Examples:
>>> normalize_model_name("claude-3-opus-20240229")
'claude-3-opus'
>>> normalize_model_name("Claude 3.5 Sonnet")
'claude-3-5-sonnet'
"""
if not model:
return ""
model_lower = model.lower()
if (
"claude-opus-4-" in model_lower
or "claude-sonnet-4-" in model_lower
or "claude-haiku-4-" in model_lower
or "sonnet-4-" in model_lower
or "opus-4-" in model_lower
or "haiku-4-" in model_lower
):
return model_lower
if "opus" in model_lower:
if "4-" in model_lower:
return model_lower
return "claude-3-opus"
if "sonnet" in model_lower:
if "4-" in model_lower:
return model_lower
if "3.5" in model_lower or "3-5" in model_lower:
return "claude-3-5-sonnet"
return "claude-3-sonnet"
if "haiku" in model_lower:
if "3.5" in model_lower or "3-5" in model_lower:
return "claude-3-5-haiku"
return "claude-3-haiku"
return model
================================================
FILE: src/claude_monitor/core/p90_calculator.py
================================================
import time
from collections.abc import Sequence
from dataclasses import dataclass
from functools import lru_cache
from statistics import quantiles
from typing import Any, Callable, Dict, List, Optional, Tuple
@dataclass(frozen=True)
class P90Config:
common_limits: Sequence[int]
limit_threshold: float
default_min_limit: int
cache_ttl_seconds: int
def _did_hit_limit(tokens: int, common_limits: Sequence[int], threshold: float) -> bool:
return any(tokens >= limit * threshold for limit in common_limits)
def _extract_sessions(
blocks: Sequence[Dict[str, Any]], filter_fn: Callable[[Dict[str, Any]], bool]
) -> List[int]:
return [
block["totalTokens"]
for block in blocks
if filter_fn(block) and block.get("totalTokens", 0) > 0
]
def _calculate_p90_from_blocks(blocks: Sequence[Dict[str, Any]], cfg: P90Config) -> int:
hits = _extract_sessions(
blocks,
lambda b: (
not b.get("isGap", False)
and not b.get("isActive", False)
and _did_hit_limit(
b.get("totalTokens", 0), cfg.common_limits, cfg.limit_threshold
)
),
)
if not hits:
hits = _extract_sessions(
blocks, lambda b: not b.get("isGap", False) and not b.get("isActive", False)
)
if not hits:
return cfg.default_min_limit
q: float = quantiles(hits, n=10)[8]
return max(int(q), cfg.default_min_limit)
class P90Calculator:
def __init__(self, config: Optional[P90Config] = None) -> None:
if config is None:
from claude_monitor.core.plans import (
COMMON_TOKEN_LIMITS,
DEFAULT_TOKEN_LIMIT,
LIMIT_DETECTION_THRESHOLD,
)
config = P90Config(
common_limits=COMMON_TOKEN_LIMITS,
limit_threshold=LIMIT_DETECTION_THRESHOLD,
default_min_limit=DEFAULT_TOKEN_LIMIT,
cache_ttl_seconds=60 * 60,
)
self._cfg: P90Config = config
@lru_cache(maxsize=1)
def _cached_calc(
self, key: int, blocks_tuple: Tuple[Tuple[bool, bool, int], ...]
) -> int:
blocks: List[Dict[str, Any]] = [
{"isGap": g, "isActive": a, "totalTokens": t} for g, a, t in blocks_tuple
]
return _calculate_p90_from_blocks(blocks, self._cfg)
def calculate_p90_limit(
self,
blocks: Optional[List[Dict[str, Any]]] = None,
use_cache: bool = True,
) -> Optional[int]:
if not blocks:
return None
if not use_cache:
return _calculate_p90_from_blocks(blocks, self._cfg)
ttl: int = self._cfg.cache_ttl_seconds
expire_key: int = int(time.time() // ttl)
blocks_tuple: Tuple[Tuple[bool, bool, int], ...] = tuple(
(
b.get("isGap", False),
b.get("isActive", False),
b.get("totalTokens", 0),
)
for b in blocks
)
return self._cached_calc(expire_key, blocks_tuple)
================================================
FILE: src/claude_monitor/core/plans.py
================================================
"""Centralized plan configuration for Claude Monitor.
All plan limits (token, message, cost) live in one place (PLAN_LIMITS).
Shared constants (defaults, common limits, threshold) are exposed on the Plans class.
"""
from dataclasses import dataclass
from enum import Enum
from typing import Any, Dict, List, Optional
class PlanType(Enum):
"""Available Claude subscription plan types."""
PRO = "pro"
MAX5 = "max5"
MAX20 = "max20"
CUSTOM = "custom"
@classmethod
def from_string(cls, value: str) -> "PlanType":
"""Case-insensitive creation of PlanType from a string."""
try:
return cls(value.lower())
except ValueError:
raise ValueError(f"Unknown plan type: {value}")
@dataclass(frozen=True)
class PlanConfig:
"""Immutable configuration for a Claude subscription plan."""
name: str
token_limit: int
cost_limit: float
message_limit: int
display_name: str
@property
def formatted_token_limit(self) -> str:
"""Human-readable token limit (e.g., '19k' instead of '19000')."""
if self.token_limit >= 1_000:
return f"{self.token_limit // 1_000}k"
return str(self.token_limit)
PLAN_LIMITS: Dict[PlanType, Dict[str, Any]] = {
PlanType.PRO: {
"token_limit": 19_000,
"cost_limit": 18.0,
"message_limit": 250,
"display_name": "Pro",
},
PlanType.MAX5: {
"token_limit": 88_000,
"cost_limit": 35.0,
"message_limit": 1_000,
"display_name": "Max5",
},
PlanType.MAX20: {
"token_limit": 220_000,
"cost_limit": 140.0,
"message_limit": 2_000,
"display_name": "Max20",
},
PlanType.CUSTOM: {
"token_limit": 44_000,
"cost_limit": 50.0,
"message_limit": 250,
"display_name": "Custom",
},
}
_DEFAULTS: Dict[str, Any] = {
"token_limit": PLAN_LIMITS[PlanType.PRO]["token_limit"],
"cost_limit": PLAN_LIMITS[PlanType.CUSTOM]["cost_limit"],
"message_limit": PLAN_LIMITS[PlanType.PRO]["message_limit"],
}
class Plans:
"""Registry and shared constants for all plan configurations."""
DEFAULT_TOKEN_LIMIT: int = _DEFAULTS["token_limit"]
DEFAULT_COST_LIMIT: float = _DEFAULTS["cost_limit"]
DEFAULT_MESSAGE_LIMIT: int = _DEFAULTS["message_limit"]
COMMON_TOKEN_LIMITS: List[int] = [19_000, 88_000, 220_000, 880_000]
LIMIT_DETECTION_THRESHOLD: float = 0.95
@classmethod
def _build_config(cls, plan_type: PlanType) -> PlanConfig:
"""Instantiate PlanConfig from the PLAN_LIMITS dictionary."""
data = PLAN_LIMITS[plan_type]
return PlanConfig(
name=plan_type.value,
token_limit=data["token_limit"],
cost_limit=data["cost_limit"],
message_limit=data["message_limit"],
display_name=data["display_name"],
)
@classmethod
def all_plans(cls) -> Dict[PlanType, PlanConfig]:
"""Return a copy of all available plan configurations."""
return {pt: cls._build_config(pt) for pt in PLAN_LIMITS}
@classmethod
def get_plan(cls, plan_type: PlanType) -> PlanConfig:
"""Get configuration for a specific PlanType."""
return cls._build_config(plan_type)
@classmethod
def get_plan_by_name(cls, name: str) -> Optional[PlanConfig]:
"""Get PlanConfig by its string name (case-insensitive)."""
try:
pt = PlanType.from_string(name)
return cls.get_plan(pt)
except ValueError:
return None
@classmethod
def get_token_limit(
cls, plan: str, blocks: Optional[List[Dict[str, Any]]] = None
) -> int:
"""
Get the token limit for a plan.
For "custom" plans, if `blocks` are provided, compute the P90 limit.
Otherwise, return the predefined limit or default.
"""
cfg = cls.get_plan_by_name(plan)
if cfg is None:
return cls.DEFAULT_TOKEN_LIMIT
if cfg.name == PlanType.CUSTOM.value and blocks:
from claude_monitor.core.p90_calculator import P90Calculator
p90_limit = P90Calculator().calculate_p90_limit(blocks)
if p90_limit:
return p90_limit
return cfg.token_limit
@classmethod
def get_cost_limit(cls, plan: str) -> float:
"""Get the cost limit for a plan, or default if invalid."""
cfg = cls.get_plan_by_name(plan)
return cfg.cost_limit if cfg else cls.DEFAULT_COST_LIMIT
@classmethod
def get_message_limit(cls, plan: str) -> int:
"""Get the message limit for a plan, or default if invalid."""
cfg = cls.get_plan_by_name(plan)
return cfg.message_limit if cfg else cls.DEFAULT_MESSAGE_LIMIT
@classmethod
def is_valid_plan(cls, plan: str) -> bool:
"""Check whether a given plan name is recognized."""
return cls.get_plan_by_name(plan) is not None
TOKEN_LIMITS: Dict[str, int] = {
plan.value: config.token_limit
for plan, config in Plans.all_plans().items()
if plan != PlanType.CUSTOM
}
DEFAULT_TOKEN_LIMIT: int = Plans.DEFAULT_TOKEN_LIMIT
COMMON_TOKEN_LIMITS: List[int] = Plans.COMMON_TOKEN_LIMITS
LIMIT_DETECTION_THRESHOLD: float = Plans.LIMIT_DETECTION_THRESHOLD
COST_LIMITS: Dict[str, float] = {
plan.value: config.cost_limit
for plan, config in Plans.all_plans().items()
if plan != PlanType.CUSTOM
}
DEFAULT_COST_LIMIT: float = Plans.DEFAULT_COST_LIMIT
def get_token_limit(plan: str, blocks: Optional[List[Dict[str, Any]]] = None) -> int:
"""Get token limit for a plan, using P90 for custom plans.
Args:
plan: Plan type ('pro', 'max5', 'max20', 'custom')
blocks: Optional session blocks for custom P90 calculation
Returns:
Token limit for the plan
"""
return Plans.get_token_limit(plan, blocks)
def get_cost_limit(plan: str) -> float:
"""Get standard cost limit for a plan.
Args:
plan: Plan type ('pro', 'max5', 'max20', 'custom')
Returns:
Cost limit for the plan in USD
"""
return Plans.get_cost_limit(plan)
================================================
FILE: src/claude_monitor/core/pricing.py
================================================
"""Pricing calculations for Claude models.
This module provides the PricingCalculator class for calculating costs
based on token usage and model pricing. It supports all Claude model types
(Opus, Sonnet, Haiku) and provides both simple and detailed cost calculations
with caching.
"""
from typing import Any, Dict, Optional
from claude_monitor.core.models import CostMode, TokenCounts, normalize_model_name
class PricingCalculator:
"""Calculates costs based on model pricing with caching support.
This class provides methods for calculating costs for individual models/tokens
as well as detailed cost breakdowns for collections of usage entries.
It supports custom pricing configurations and caches calculations for performance.
Features:
- Configurable pricing (from config or custom)
- Fallback hardcoded pricing for robustness
- Caching for performance
- Support for all token types including cache
- Backward compatible with both APIs
"""
FALLBACK_PRICING: Dict[str, Dict[str, float]] = {
"opus": {
"input": 15.0,
"output": 75.0,
"cache_creation": 18.75,
"cache_read": 1.5,
},
"sonnet": {
"input": 3.0,
"output": 15.0,
"cache_creation": 3.75,
"cache_read": 0.3,
},
"haiku": {
"input": 0.25,
"output": 1.25,
"cache_creation": 0.3,
"cache_read": 0.03,
},
}
def __init__(
self, custom_pricing: Optional[Dict[str, Dict[str, float]]] = None
) -> None:
"""Initialize with optional custom pricing.
Args:
custom_pricing: Optional custom pricing dictionary to override defaults.
Should follow same structure as MODEL_PRICING.
"""
# Use fallback pricing if no custom pricing provided
self.pricing: Dict[str, Dict[str, float]] = custom_pricing or {
"claude-3-opus": self.FALLBACK_PRICING["opus"],
"claude-3-sonnet": self.FALLBACK_PRICING["sonnet"],
"claude-3-haiku": self.FALLBACK_PRICING["haiku"],
"claude-3-5-sonnet": self.FALLBACK_PRICING["sonnet"],
"claude-3-5-haiku": self.FALLBACK_PRICING["haiku"],
"claude-sonnet-4-20250514": self.FALLBACK_PRICING["sonnet"],
"claude-opus-4-20250514": self.FALLBACK_PRICING["opus"],
}
self._cost_cache: Dict[str, float] = {}
def calculate_cost(
self,
model: str,
input_tokens: int = 0,
output_tokens: int = 0,
cache_creation_tokens: int = 0,
cache_read_tokens: int = 0,
tokens: Optional[TokenCounts] = None,
strict: bool = False,
) -> float:
"""Calculate cost with flexible API supporting both signatures.
Args:
model: Model name
input_tokens: Number of input tokens (ignored if tokens provided)
output_tokens: Number of output tokens (ignored if tokens provided)
cache_creation_tokens: Number of cache creation tokens
cache_read_tokens: Number of cache read tokens
tokens: Optional TokenCounts object (takes precedence)
Returns:
Total cost in USD
"""
# Handle synthetic model
if model == "<synthetic>":
return 0.0
# Support TokenCounts object
if tokens is not None:
input_tokens = tokens.input_tokens
output_tokens = tokens.output_tokens
cache_creation_tokens = tokens.cache_creation_tokens
cache_read_tokens = tokens.cache_read_tokens
# Create cache key
cache_key = (
f"{model}:{input_tokens}:{output_tokens}:"
f"{cache_creation_tokens}:{cache_read_tokens}"
)
# Check cache
if cache_key in self._cost_cache:
return self._cost_cache[cache_key]
# Get pricing for model
pricing = self._get_pricing_for_model(model, strict=strict)
# Calculate costs (pricing is per million tokens)
cost = (
(input_tokens / 1_000_000) * pricing["input"]
+ (output_tokens / 1_000_000) * pricing["output"]
+ (cache_creation_tokens / 1_000_000)
* pricing.get("cache_creation", pricing["input"] * 1.25)
+ (cache_read_tokens / 1_000_000)
* pricing.get("cache_read", pricing["input"] * 0.1)
)
# Round to 6 decimal places
cost = round(cost, 6)
# Cache result
self._cost_cache[cache_key] = cost
return cost
def _get_pricing_for_model(
self, model: str, strict: bool = False
) -> Dict[str, float]:
"""Get pricing for a model with optional fallback logic.
Args:
model: Model name
strict: If True, raise KeyError for unknown models
Returns:
Pricing dictionary with input/output/cache costs
Raises:
KeyError: If strict=True and model is unknown
"""
# Try normalized model name first
normalized = normalize_model_name(model)
# Check configured pricing
if normalized in self.pricing:
pricing = self.pricing[normalized]
# Ensure cache pricing exists
if "cache_creation" not in pricing:
pricing["cache_creation"] = pricing["input"] * 1.25
if "cache_read" not in pricing:
pricing["cache_read"] = pricing["input"] * 0.1
return pricing
# Check original model name
if model in self.pricing:
pricing = self.pricing[model]
if "cache_creation" not in pricing:
pricing["cache_creation"] = pricing["input"] * 1.25
if "cache_read" not in pricing:
pricing["cache_read"] = pricing["input"] * 0.1
return pricing
# If strict mode, raise KeyError for unknown models
if strict:
raise KeyError(f"Unknown model: {model}")
# Fallback to hardcoded pricing based on model type
model_lower = model.lower()
if "opus" in model_lower:
return self.FALLBACK_PRICING["opus"]
if "haiku" in model_lower:
return self.FALLBACK_PRICING["haiku"]
# Default to Sonnet pricing
return self.FALLBACK_PRICING["sonnet"]
def calculate_cost_for_entry(
self, entry_data: Dict[str, Any], mode: CostMode
) -> float:
"""Calculate cost for a single entry (backward compatibility).
Args:
entry_data: Entry data dictionary
mode: Cost mode (for backward compatibility)
Returns:
Cost in USD
"""
# If cost is present and mode is cached, use it
if mode.value == "cached":
cost_value = entry_data.get("costUSD") or entry_data.get("cost_usd")
if cost_value is not None:
return float(cost_value)
# Otherwise calculate from tokens
model = entry_data.get("model") or entry_data.get("Model")
if not model:
raise KeyError("Missing 'model' key in entry_data")
# Extract token counts with different possible keys
input_tokens = entry_data.get("inputTokens", 0) or entry_data.get(
"input_tokens", 0
)
output_tokens = entry_data.get("outputTokens", 0) or entry_data.get(
"output_tokens", 0
)
cache_creation = entry_data.get(
"cacheCreationInputTokens", 0
) or entry_data.get("cache_creation_tokens", 0)
cache_read = (
entry_data.get("cacheReadInputTokens", 0)
or entry_data.get("cache_read_input_tokens", 0)
or entry_data.get("cache_read_tokens", 0)
)
return self.calculate_cost(
model=model,
input_tokens=input_tokens,
output_tokens=output_tokens,
cache_creation_tokens=cache_creation,
cache_read_tokens=cache_read,
)
================================================
FILE: src/claude_monitor/core/settings.py
================================================
"""Simplified settings management with CLI and last used params only."""
import argparse
import json
import logging
from datetime import datetime
from pathlib import Path
from typing import Any, Dict, List, Literal, Optional, Tuple
import pytz
from pydantic import Field, field_validator
from pydantic_settings import BaseSettings, SettingsConfigDict
from claude_monitor import __version__
logger = logging.getLogger(__name__)
class LastUsedParams:
"""Manages last used parameters persistence (moved from last_used.py)."""
def __init__(self, config_dir: Optional[Path] = None) -> None:
"""Initialize with config directory."""
self.config_dir = config_dir or Path.home() / ".claude-monitor"
self.params_file = self.config_dir / "last_used.json"
def save(self, settings: "Settings") -> None:
"""Save current settings as last used."""
try:
params = {
"theme": settings.theme,
"timezone": settings.timezone,
"time_format": settings.time_format,
"refresh_rate": settings.refresh_rate,
"reset_hour": settings.reset_hour,
"view": settings.view,
"timestamp": datetime.now().isoformat(),
}
if settings.custom_limit_tokens:
params["custom_limit_tokens"] = settings.custom_limit_tokens
self.config_dir.mkdir(parents=True, exist_ok=True)
temp_file = self.params_file.with_suffix(".tmp")
with open(temp_file, "w") as f:
json.dump(params, f, indent=2)
temp_file.replace(self.params_file)
logger.debug(f"Saved last used params to {self.params_file}")
except Exception as e:
logger.warning(f"Failed to save last used params: {e}")
def load(self) -> Dict[str, Any]:
"""Load last used parameters."""
if not self.params_file.exists():
return {}
try:
with open(self.params_file) as f:
params = json.load(f)
params.pop("timestamp", None)
logger.debug(f"Loaded last used params from {self.params_file}")
return params
except Exception as e:
logger.warning(f"Failed to load last used params: {e}")
return {}
def clear(self) -> None:
"""Clear last used parameters."""
try:
if self.params_file.exists():
self.params_file.unlink()
logger.debug("Cleared last used params")
except Exception as e:
logger.warning(f"Failed to clear last used params: {e}")
def exists(self) -> bool:
"""Check if last used params exist."""
return self.params_file.exists()
class Settings(BaseSettings):
"""claude-monitor - Real-time token usage monitoring for Claude AI"""
model_config = SettingsConfigDict(
env_file=None,
env_prefix="",
case_sensitive=False,
validate_default=True,
extra="ignore",
cli_parse_args=True,
cli_prog_name="claude-monitor",
cli_kebab_case=True,
cli_implicit_flags=True,
)
plan: Literal["pro", "max5", "max20", "custom"] = Field(
default="custom",
description="Plan type (pro, max5, max20, custom)",
)
view: Literal["realtime", "daily", "monthly", "session"] = Field(
default="realtime",
description="View mode (realtime, daily, monthly, session)",
)
@staticmethod
def _get_system_timezone() -> str:
"""Lazy import to avoid circular dependencies."""
from claude_monitor.utils.time_utils import get_system_timezone
return get_system_timezone()
@staticmethod
def _get_system_time_format() -> str:
"""Lazy import to avoid circular dependencies."""
from claude_monitor.utils.time_utils import get_system_time_format
return get_system_time_format()
timezone: str = Field(
default="auto",
description="Timezone for display (auto-detected from system). Examples: UTC, America/New_York, Europe/London, Europe/Warsaw, Asia/Tokyo, Australia/Sydney",
)
time_format: str = Field(
default="auto",
description="Time format (12h or 24h, auto-detected from system)",
)
theme: Literal["light", "dark", "classic", "auto"] = Field(
default="auto",
description="Display theme (light, dark, classic, auto)",
)
custom_limit_tokens: Optional[int] = Field(
default=None, gt=0, description="Token limit for custom plan"
)
refresh_rate: int = Field(
default=10, ge=1, le=60, description="Refresh rate in seconds"
)
refresh_per_second: float = Field(
default=0.75,
ge=0.1,
le=20.0,
description="Display refresh rate per second (0.1-20 Hz). Higher values use more CPU",
)
reset_hour: Optional[int] = Field(
default=None, ge=0, le=23, description="Reset hour for daily limits (0-23)"
)
log_level: str = Field(default="INFO", description="Logging level")
log_file: Optional[Path] = Field(default=None, description="Log file path")
debug: bool = Field(
default=False,
description="Enable debug logging (equivalent to --log-level DEBUG)",
)
version: bool = Field(default=False, description="Show version information")
clear: bool = Field(default=False, description="Clear saved configuration")
@field_validator("plan", mode="before")
@classmethod
def validate_plan(cls, v: Any) -> str:
"""Validate and normalize plan value."""
if isinstance(v, str):
v_lower = v.lower()
valid_plans = ["pro", "max5", "max20", "custom"]
if v_lower in valid_plans:
return v_lower
raise ValueError(
f"Invalid plan: {v}. Must be one of: {', '.join(valid_plans)}"
)
return v
@field_validator("view", mode="before")
@classmethod
def validate_view(cls, v: Any) -> str:
"""Validate and normalize view value."""
if isinstance(v, str):
v_lower = v.lower()
valid_views = ["realtime", "daily", "monthly", "session"]
if v_lower in valid_views:
return v_lower
raise ValueError(
f"Invalid view: {v}. Must be one of: {', '.join(valid_views)}"
)
return v
@field_validator("theme", mode="before")
@classmethod
def validate_theme(cls, v: Any) -> str:
"""Validate and normalize theme value."""
if isinstance(v, str):
v_lower = v.lower()
valid_themes = ["light", "dark", "classic", "auto"]
if v_lower in valid_themes:
return v_lower
raise ValueError(
f"Invalid theme: {v}. Must be one of: {', '.join(valid_themes)}"
)
return v
@field_validator("timezone")
@classmethod
def validate_timezone(cls, v: str) -> str:
"""Validate timezone."""
if v not in ["local", "auto"] and v not in pytz.all_timezones:
raise ValueError(f"Invalid timezone: {v}")
return v
@field_validator("time_format")
@classmethod
def validate_time_format(cls, v: str) -> str:
"""Validate time format."""
if v not in ["12h", "24h", "auto"]:
raise ValueError(
f"Invalid time format: {v}. Must be '12h', '24h', or 'auto'"
)
return v
@field_validator("log_level")
@classmethod
def validate_log_level(cls, v: str) -> str:
"""Validate log level."""
valid_levels = ["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"]
v_upper = v.upper()
if v_upper not in valid_levels:
raise ValueError(f"Invalid log level: {v}")
return v_upper
@classmethod
def settings_customise_sources(
cls,
settings_cls: Any,
init_settings: Any,
env_settings: Any,
dotenv_settings: Any,
file_secret_settings: Any,
) -> Tuple[Any, ...]:
"""Custom sources - only init and last used."""
_ = (
settings_cls,
env_settings,
dotenv_settings,
file_secret_settings,
)
return (init_settings,)
@classmethod
def load_with_last_used(cls, argv: Optional[List[str]] = None) -> "Settings":
"""Load settings with last used params support (default behavior)."""
if argv and "--version" in argv:
print(f"claude-monitor {__version__}")
import sys
sys.exit(0)
clear_config = argv and "--clear" in argv
if clear_config:
last_used = LastUsedParams()
last_used.clear()
settings = cls(_cli_parse_args=argv)
else:
last_used = LastUsedParams()
last_params = last_used.load()
settings = cls(_cli_parse_args=argv)
cli_provided_fields = set()
if argv:
for _i, arg in enumerate(argv):
if arg.startswith("--"):
field_name = arg[2:].replace("-", "_")
if field_name in cls.model_fields:
cli_provided_fields.add(field_name)
for key, value in last_params.items():
if key == "plan":
continue
if not hasattr(settings, key):
continue
if key not in cli_provided_fields:
setattr(settings, key, value)
if (
"plan" in cli_provided_fields
and settings.plan == "custom"
and "custom_limit_tokens" not in cli_provided_fields
):
settings.custom_limit_tokens = None
if settings.timezone == "auto":
settings.timezone = cls._get_system_timezone()
if settings.time_format == "auto":
settings.time_format = cls._get_system_time_format()
if settings.debug:
settings.log_level = "DEBUG"
if settings.theme == "auto" or (
"theme" not in cli_provided_fields and not clear_config
):
from claude_monitor.terminal.themes import (
BackgroundDetector,
BackgroundType,
)
detector = BackgroundDetector()
detected_bg = detector.detect_background()
if detected_bg == BackgroundType.LIGHT:
settings.theme = "light"
elif detected_bg == BackgroundType.DARK:
settings.theme = "dark"
else:
settings.theme = "auto"
if not clear_config:
last_used = LastUsedParams()
last_used.save(settings)
return settings
def to_namespace(self) -> argparse.Namespace:
"""Convert to argparse.Namespace for compatibility."""
args = argparse.Namespace()
args.plan = self.plan
args.view = self.view
args.timezone = self.timezone
args.theme = self.theme
args.refresh_rate = self.refresh_rate
args.refresh_per_second = self.refresh_per_second
args.reset_hour = self.reset_hour
args.custom_limit_tokens = self.custom_limit_tokens
args.time_format = self.time_format
args.log_level = self.log_level
args.log_file = str(self.log_file) if self.log_file else None
args.version = self.version
return args
================================================
FILE: src/claude_monitor/data/__init__.py
================================================
"""Data package for Claude Monitor."""
# Import directly from modules without facade
__all__: list[str] = []
================================================
FILE: src/claude_monitor/data/aggregator.py
================================================
"""Data aggregator for daily and monthly statistics.
This module provides functionality to aggregate Claude usage data
by day and month, similar to ccusage's functionality.
"""
import logging
from collections import defaultdict
from dataclasses import dataclass, field
from datetime import datetime
from typing import Any, Callable, Dict, List, Optional
from claude_monitor.core.models import SessionBlock, UsageEntry, normalize_model_name
from claude_monitor.utils.time_utils import TimezoneHandler
logger = logging.getLogger(__name__)
@dataclass
class AggregatedStats:
"""Statistics for aggregated usage data."""
input_tokens: int = 0
output_tokens: int = 0
cache_creation_tokens: int = 0
cache_read_tokens: int = 0
cost: float = 0.0
count: int = 0
def add_entry(self, entry: UsageEntry) -> None:
"""Add an entry's statistics to this aggregate."""
self.input_tokens += entry.input_tokens
self.output_tokens += entry.output_tokens
self.cache_creation_tokens += entry.cache_creation_tokens
self.cache_read_tokens += entry.cache_read_tokens
self.cost += entry.cost_usd
self.count += 1
def to_dict(self) -> Dict[str, Any]:
"""Convert to dictionary format."""
return {
"input_tokens": self.input_tokens,
"output_tokens": self.output_tokens,
"cache_creation_tokens": self.cache_creation_tokens,
"cache_read_tokens": self.cache_read_tokens,
"cost": self.cost,
"count": self.count,
}
@dataclass
class AggregatedPeriod:
"""Aggregated data for a time period (day or month)."""
period_key: str
stats: AggregatedStats = field(default_factory=AggregatedStats)
models_used: set = field(default_factory=set)
model_breakdowns: Dict[str, AggregatedStats] = field(
default_factory=lambda: defaultdict(AggregatedStats)
)
def add_entry(self, entry: UsageEntry) -> None:
"""Add an entry to this period's aggregate."""
# Add to overall stats
self.stats.add_entry(entry)
# Track model
model = normalize_model_name(entry.model) if entry.model else "unknown"
self.models_used.add(model)
# Add to model-specific stats
self.model_breakdowns[model].add_entry(entry)
def to_dict(self, period_type: str) -> Dict[str, Any]:
"""Convert to dictionary format for display."""
result = {
period_type: self.period_key,
"input_tokens": self.stats.input_tokens,
"output_tokens": self.stats.output_tokens,
"cache_creation_tokens": self.stats.cache_creation_tokens,
"cache_read_tokens": self.stats.cache_read_tokens,
"total_cost": self.stats.cost,
"models_used": sorted(list(self.models_used)),
"model_breakdowns": {
model: stats.to_dict() for model, stats in self.model_breakdowns.items()
},
"entries_count": self.stats.count,
}
return result
class UsageAggregator:
"""Aggregates usage data for daily and monthly reports."""
def __init__(
self, data_path: str, aggregation_mode: str = "daily", timezone: str = "UTC"
):
"""Initialize the aggregator.
Args:
data_path: Path to the data directory
aggregation_mode: Mode of aggregation ('daily' or 'monthly')
timezone: Timezone string for date formatting
"""
self.data_path = data_path
self.aggregation_mode = aggregation_mode
self.timezone = timezone
self.timezone_handler = TimezoneHandler()
def _aggregate_by_period(
self,
entries: List[UsageEntry],
period_key_func: Callable[[datetime], str],
period_type: str,
start_date: Optional[datetime] = None,
end_date: Optional[datetime] = None,
) -> List[Dict[str, Any]]:
"""Generic aggregation by time period.
Args:
entries: List of usage entries
period_key_func: Function to extract period key from timestamp
period_type: Type of period ('date' or 'month')
start_date: Optional start date filter
end_date: Optional end date filter
Returns:
List of aggregated data dictionaries
"""
period_data: Dict[str, AggregatedPeriod] = {}
for entry in entries:
# Apply date filters
if start_date and entry.timestamp < start_date:
continue
if end_date and entry.timestamp > end_date:
continue
# Get period key
period_key = period_key_func(entry.timestamp)
# Get or create period aggregate
if period_key not in period_data:
period_data[period_key] = AggregatedPeriod(period_key)
# Add entry to period
period_data[period_key].add_entry(entry)
# Convert to list and sort
result = []
for period_key in sorted(period_data.keys()):
period = period_data[period_key]
result.append(period.to_dict(period_type))
return result
def aggregate_daily(
self,
entries: List[UsageEntry],
start_date: Optional[datetime] = None,
end_date: Optional[datetime] = None,
) -> List[Dict[str, Any]]:
"""Aggregate usage data by day.
Args:
entries: List of usage entries
start_date: Optional start date filter
end_date: Optional end date filter
Returns:
List of daily aggregated data
"""
return self._aggregate_by_period(
entries,
lambda timestamp: timestamp.strftime("%Y-%m-%d"),
"date",
start_date,
end_date,
)
def aggregate_monthly(
self,
entries: List[UsageEntry],
start_date: Optional[datetime] = None,
end_date: Optional[datetime] = None,
) -> List[Dict[str, Any]]:
"""Aggregate usage data by month.
Args:
entries: List of usage entries
start_date: Optional start date filter
end_date: Optional end date filter
Returns:
List of monthly aggregated data
"""
return self._aggregate_by_period(
entries,
lambda timestamp: timestamp.strftime("%Y-%m"),
"month",
start_date,
end_date,
)
def aggregate_from_blocks(
self, blocks: List[SessionBlock], view_type: str = "daily"
) -> List[Dict[str, Any]]:
"""Aggregate data from session blocks.
Args:
blocks: List of session blocks
view_type: Type of aggregation ('daily' or 'monthly')
Returns:
List of aggregated data
"""
# Validate view type
if view_type not in ["daily", "monthly"]:
raise ValueError(
f"Invalid view type: {view_type}. Must be 'daily' or 'monthly'"
)
# Extract all entries from blocks
all_entries = []
for block in blocks:
if not block.is_gap:
all_entries.extend(block.entries)
# Aggregate based on view type
if view_type == "daily":
return self.aggregate_daily(all_entries)
else:
return self.aggregate_monthly(all_entries)
def calculate_totals(self, aggregated_data: List[Dict[str, Any]]) -> Dict[str, Any]:
"""Calculate totals from aggregated data.
Args:
aggregated_data: List of aggregated daily or monthly data
Returns:
Dictionary with total statistics
"""
total_stats = AggregatedStats()
for data in aggregated_data:
total_stats.input_tokens += data.get("input_tokens", 0)
total_stats.output_tokens += data.get("output_tokens", 0)
total_stats.cache_creation_tokens += data.get("cache_creation_tokens", 0)
total_stats.cache_read_tokens += data.get("cache_read_tokens", 0)
total_stats.cost += data.get("total_cost", 0.0)
total_stats.count += data.get("entries_count", 0)
return {
"input_tokens": total_stats.input_tokens,
"output_tokens": total_stats.output_tokens,
"cache_creation_tokens": total_stats.cache_creation_tokens,
"cache_read_tokens": total_stats.cache_read_tokens,
"total_tokens": (
total_stats.input_tokens
+ total_stats.output_tokens
+ total_stats.cache_creation_tokens
+ total_stats.cache_read_tokens
),
"total_cost": total_stats.cost,
"entries_count": total_stats.count,
}
def aggregate(self) -> List[Dict[str, Any]]:
"""Main aggregation method that reads data and returns aggregated results.
Returns:
List of aggregated data based on aggregation_mode
"""
from claude_monitor.data.reader import load_usage_entries
logger.info(f"Starting aggregation in {self.aggregation_mode} mode")
# Load usage entries
gitextract_6dymq9k3/
├── .gitattributes
├── .github/
│ ├── FUNDING.yml
│ └── workflows/
│ ├── lint.yml
│ ├── release.yml
│ ├── test.yml
│ └── version-bump.yml
├── .gitignore
├── .pre-commit-config.yaml
├── CHANGELOG.md
├── CONTRIBUTING.md
├── DEVELOPMENT.md
├── LICENSE
├── README.md
├── RELEASE.md
├── TROUBLESHOOTING.md
├── VERSION_MANAGEMENT.md
├── pyproject.toml
└── src/
├── claude_monitor/
│ ├── __init__.py
│ ├── __main__.py
│ ├── _version.py
│ ├── cli/
│ │ ├── __init__.py
│ │ ├── bootstrap.py
│ │ └── main.py
│ ├── core/
│ │ ├── __init__.py
│ │ ├── calculations.py
│ │ ├── data_processors.py
│ │ ├── models.py
│ │ ├── p90_calculator.py
│ │ ├── plans.py
│ │ ├── pricing.py
│ │ └── settings.py
│ ├── data/
│ │ ├── __init__.py
│ │ ├── aggregator.py
│ │ ├── analysis.py
│ │ ├── analyzer.py
│ │ └── reader.py
│ ├── error_handling.py
│ ├── monitoring/
│ │ ├── __init__.py
│ │ ├── data_manager.py
│ │ ├── orchestrator.py
│ │ └── session_monitor.py
│ ├── terminal/
│ │ ├── __init__.py
│ │ ├── manager.py
│ │ └── themes.py
│ ├── ui/
│ │ ├── __init__.py
│ │ ├── components.py
│ │ ├── display_controller.py
│ │ ├── layouts.py
│ │ ├── progress_bars.py
│ │ ├── session_display.py
│ │ └── table_views.py
│ └── utils/
│ ├── __init__.py
│ ├── formatting.py
│ ├── model_utils.py
│ ├── notifications.py
│ ├── time_utils.py
│ └── timezone.py
└── tests/
├── __init__.py
├── conftest.py
├── examples/
│ └── api_examples.py
├── run_tests.py
├── test_aggregator.py
├── test_analysis.py
├── test_calculations.py
├── test_cli_main.py
├── test_data_reader.py
├── test_display_controller.py
├── test_error_handling.py
├── test_formatting.py
├── test_monitoring_orchestrator.py
├── test_pricing.py
├── test_session_analyzer.py
├── test_settings.py
├── test_table_views.py
├── test_time_utils.py
├── test_timezone.py
└── test_version.py
SYMBOL INDEX (1032 symbols across 51 files)
FILE: src/claude_monitor/__main__.py
function _main (line 13) | def _main() -> NoReturn:
FILE: src/claude_monitor/_version.py
function get_version (line 13) | def get_version() -> str:
function _get_version_from_pyproject (line 29) | def _get_version_from_pyproject() -> str:
function get_package_info (line 66) | def get_package_info() -> Dict[str, Optional[str]]:
function get_version_info (line 95) | def get_version_info() -> Dict[str, Any]:
function find_project_root (line 115) | def find_project_root(start_path: Optional[Union[str, Path]] = None) -> ...
FILE: src/claude_monitor/cli/bootstrap.py
function setup_logging (line 13) | def setup_logging(
function setup_environment (line 41) | def setup_environment() -> None:
function init_timezone (line 55) | def init_timezone(timezone: str = "Europe/Warsaw") -> TimezoneHandler:
function ensure_directories (line 70) | def ensure_directories() -> None:
FILE: src/claude_monitor/cli/main.py
function get_standard_claude_paths (line 44) | def get_standard_claude_paths() -> List[str]:
function discover_claude_data_paths (line 49) | def discover_claude_data_paths(custom_paths: Optional[List[str]] = None)...
function main (line 72) | def main(argv: Optional[List[str]] = None) -> int:
function _run_monitoring (line 110) | def _run_monitoring(args: argparse.Namespace) -> None:
function _get_initial_token_limit (line 263) | def _get_initial_token_limit(
function handle_application_error (line 315) | def handle_application_error(
function validate_cli_environment (line 351) | def validate_cli_environment() -> Optional[str]:
function _run_table_view (line 381) | def _run_table_view(
FILE: src/claude_monitor/core/calculations.py
class BlockLike (line 21) | class BlockLike(Protocol):
class BurnRateCalculator (line 31) | class BurnRateCalculator:
method calculate_burn_rate (line 34) | def calculate_burn_rate(self, block: BlockLike) -> Optional[BurnRate]:
method project_block_usage (line 59) | def project_block_usage(self, block: BlockLike) -> Optional[UsageProje...
function calculate_hourly_burn_rate (line 94) | def calculate_hourly_burn_rate(
function _calculate_total_tokens_in_hour (line 107) | def _calculate_total_tokens_in_hour(
function _process_block_for_burn_rate (line 117) | def _process_block_for_burn_rate(
function _parse_block_start_time (line 134) | def _parse_block_start_time(block: Dict[str, Any]) -> Optional[datetime]:
function _determine_session_end_time (line 149) | def _determine_session_end_time(
function _calculate_tokens_in_hour (line 167) | def _calculate_tokens_in_hour(
function _log_timestamp_error (line 190) | def _log_timestamp_error(
FILE: src/claude_monitor/core/data_processors.py
class TimestampProcessor (line 13) | class TimestampProcessor:
method __init__ (line 16) | def __init__(self, timezone_handler: Optional[TimezoneHandler] = None)...
method parse_timestamp (line 20) | def parse_timestamp(
class TokenExtractor (line 65) | class TokenExtractor:
method extract_tokens (line 69) | def extract_tokens(data: Dict[str, Any]) -> Dict[str, int]:
class DataConverter (line 172) | class DataConverter:
method flatten_nested_dict (line 176) | def flatten_nested_dict(data: Dict[str, Any], prefix: str = "") -> Dic...
method extract_model_name (line 199) | def extract_model_name(
method to_serializable (line 226) | def to_serializable(obj: Any) -> Any:
FILE: src/claude_monitor/core/models.py
class CostMode (line 11) | class CostMode(Enum):
class UsageEntry (line 20) | class UsageEntry:
class TokenCounts (line 35) | class TokenCounts:
method total_tokens (line 44) | def total_tokens(self) -> int:
class BurnRate (line 55) | class BurnRate:
class UsageProjection (line 63) | class UsageProjection:
class SessionBlock (line 72) | class SessionBlock:
method total_tokens (line 93) | def total_tokens(self) -> int:
method total_cost (line 98) | def total_cost(self) -> float:
method duration_minutes (line 103) | def duration_minutes(self) -> float:
function normalize_model_name (line 112) | def normalize_model_name(model: str) -> str:
FILE: src/claude_monitor/core/p90_calculator.py
class P90Config (line 10) | class P90Config:
function _did_hit_limit (line 17) | def _did_hit_limit(tokens: int, common_limits: Sequence[int], threshold:...
function _extract_sessions (line 21) | def _extract_sessions(
function _calculate_p90_from_blocks (line 31) | def _calculate_p90_from_blocks(blocks: Sequence[Dict[str, Any]], cfg: P9...
class P90Calculator (line 52) | class P90Calculator:
method __init__ (line 53) | def __init__(self, config: Optional[P90Config] = None) -> None:
method _cached_calc (line 70) | def _cached_calc(
method calculate_p90_limit (line 78) | def calculate_p90_limit(
FILE: src/claude_monitor/core/plans.py
class PlanType (line 12) | class PlanType(Enum):
method from_string (line 21) | def from_string(cls, value: str) -> "PlanType":
class PlanConfig (line 30) | class PlanConfig:
method formatted_token_limit (line 40) | def formatted_token_limit(self) -> str:
class Plans (line 81) | class Plans:
method _build_config (line 91) | def _build_config(cls, plan_type: PlanType) -> PlanConfig:
method all_plans (line 103) | def all_plans(cls) -> Dict[PlanType, PlanConfig]:
method get_plan (line 108) | def get_plan(cls, plan_type: PlanType) -> PlanConfig:
method get_plan_by_name (line 113) | def get_plan_by_name(cls, name: str) -> Optional[PlanConfig]:
method get_token_limit (line 122) | def get_token_limit(
method get_cost_limit (line 145) | def get_cost_limit(cls, plan: str) -> float:
method get_message_limit (line 151) | def get_message_limit(cls, plan: str) -> int:
method is_valid_plan (line 157) | def is_valid_plan(cls, plan: str) -> bool:
function get_token_limit (line 181) | def get_token_limit(plan: str, blocks: Optional[List[Dict[str, Any]]] = ...
function get_cost_limit (line 194) | def get_cost_limit(plan: str) -> float:
FILE: src/claude_monitor/core/pricing.py
class PricingCalculator (line 14) | class PricingCalculator:
method __init__ (line 50) | def __init__(
method calculate_cost (line 71) | def calculate_cost(
method _get_pricing_for_model (line 135) | def _get_pricing_for_model(
method calculate_cost_for_entry (line 185) | def calculate_cost_for_entry(
FILE: src/claude_monitor/core/settings.py
class LastUsedParams (line 19) | class LastUsedParams:
method __init__ (line 22) | def __init__(self, config_dir: Optional[Path] = None) -> None:
method save (line 27) | def save(self, settings: "Settings") -> None:
method load (line 55) | def load(self) -> Dict[str, Any]:
method clear (line 73) | def clear(self) -> None:
method exists (line 82) | def exists(self) -> bool:
class Settings (line 87) | class Settings(BaseSettings):
method _get_system_timezone (line 113) | def _get_system_timezone() -> str:
method _get_system_time_format (line 120) | def _get_system_time_format() -> str:
method validate_plan (line 175) | def validate_plan(cls, v: Any) -> str:
method validate_view (line 189) | def validate_view(cls, v: Any) -> str:
method validate_theme (line 203) | def validate_theme(cls, v: Any) -> str:
method validate_timezone (line 217) | def validate_timezone(cls, v: str) -> str:
method validate_time_format (line 225) | def validate_time_format(cls, v: str) -> str:
method validate_log_level (line 235) | def validate_log_level(cls, v: str) -> str:
method settings_customise_sources (line 244) | def settings_customise_sources(
method load_with_last_used (line 262) | def load_with_last_used(cls, argv: Optional[List[str]] = None) -> "Set...
method to_namespace (line 337) | def to_namespace(self) -> argparse.Namespace:
FILE: src/claude_monitor/data/aggregator.py
class AggregatedStats (line 20) | class AggregatedStats:
method add_entry (line 30) | def add_entry(self, entry: UsageEntry) -> None:
method to_dict (line 39) | def to_dict(self) -> Dict[str, Any]:
class AggregatedPeriod (line 52) | class AggregatedPeriod:
method add_entry (line 62) | def add_entry(self, entry: UsageEntry) -> None:
method to_dict (line 74) | def to_dict(self, period_type: str) -> Dict[str, Any]:
class UsageAggregator (line 92) | class UsageAggregator:
method __init__ (line 95) | def __init__(
method _aggregate_by_period (line 110) | def _aggregate_by_period(
method aggregate_daily (line 157) | def aggregate_daily(
method aggregate_monthly (line 181) | def aggregate_monthly(
method aggregate_from_blocks (line 205) | def aggregate_from_blocks(
method calculate_totals (line 235) | def calculate_totals(self, aggregated_data: List[Dict[str, Any]]) -> D...
method aggregate (line 269) | def aggregate(self) -> List[Dict[str, Any]]:
FILE: src/claude_monitor/data/analysis.py
function analyze_usage (line 18) | def analyze_usage(
function _process_burn_rates (line 103) | def _process_burn_rates(
function _create_result (line 121) | def _create_result(
function _is_limit_in_block_timerange (line 139) | def _is_limit_in_block_timerange(
function _format_limit_info (line 151) | def _format_limit_info(limit_info: Dict[str, Any]) -> Dict[str, Any]:
function _convert_blocks_to_dict_format (line 165) | def _convert_blocks_to_dict_format(blocks: List[SessionBlock]) -> List[D...
function _create_base_block_dict (line 177) | def _create_base_block_dict(block: SessionBlock) -> Dict[str, Any]:
function _format_block_entries (line 206) | def _format_block_entries(entries: List[UsageEntry]) -> List[Dict[str, A...
function _add_optional_block_data (line 224) | def _add_optional_block_data(block: SessionBlock, block_dict: Dict[str, ...
FILE: src/claude_monitor/data/analyzer.py
class SessionAnalyzer (line 22) | class SessionAnalyzer:
method __init__ (line 25) | def __init__(self, session_duration_hours: int = 5):
method transform_to_blocks (line 35) | def transform_to_blocks(self, entries: List[UsageEntry]) -> List[Sessi...
method detect_limits (line 81) | def detect_limits(self, raw_entries: List[Dict[str, Any]]) -> List[Dic...
method _should_create_new_block (line 99) | def _should_create_new_block(self, block: SessionBlock, entry: UsageEn...
method _round_to_hour (line 109) | def _round_to_hour(self, timestamp: datetime) -> datetime:
method _create_new_block (line 118) | def _create_new_block(self, entry: UsageEntry) -> SessionBlock:
method _add_entry_to_block (line 133) | def _add_entry_to_block(self, block: SessionBlock, entry: UsageEntry) ...
method _finalize_block (line 174) | def _finalize_block(self, block: SessionBlock) -> None:
method _check_for_gap (line 182) | def _check_for_gap(
method _mark_active_blocks (line 209) | def _mark_active_blocks(self, blocks: List[SessionBlock]) -> None:
method _detect_single_limit (line 219) | def _detect_single_limit(
method _process_system_message (line 232) | def _process_system_message(
method _process_user_message (line 278) | def _process_user_message(
method _process_tool_result (line 296) | def _process_tool_result(
method _extract_block_context (line 331) | def _extract_block_context(
method _is_opus_limit (line 351) | def _is_opus_limit(self, content_lower: str) -> bool:
method _extract_wait_time (line 362) | def _extract_wait_time(
method _parse_reset_timestamp (line 373) | def _parse_reset_timestamp(self, text: str) -> Optional[datetime]:
FILE: src/claude_monitor/data/reader.py
function load_usage_entries (line 32) | def load_usage_entries(
function load_all_raw_entries (line 87) | def load_all_raw_entries(data_path: Optional[str] = None) -> List[Dict[s...
function _find_jsonl_files (line 117) | def _find_jsonl_files(data_path: Path) -> List[Path]:
function _process_single_file (line 125) | def _process_single_file(
function _should_process_entry (line 192) | def _should_process_entry(
function _create_unique_hash (line 211) | def _create_unique_hash(data: Dict[str, Any]) -> Optional[str]:
function _update_processed_hashes (line 223) | def _update_processed_hashes(data: Dict[str, Any], processed_hashes: Set...
function _map_to_usage_entry (line 230) | def _map_to_usage_entry(
class UsageEntryMapper (line 280) | class UsageEntryMapper:
method __init__ (line 288) | def __init__(
method map (line 295) | def map(self, data: Dict[str, Any], mode: CostMode) -> Optional[UsageE...
method _has_valid_tokens (line 301) | def _has_valid_tokens(self, tokens: Dict[str, int]) -> bool:
method _extract_timestamp (line 305) | def _extract_timestamp(self, data: Dict[str, Any]) -> Optional[datetime]:
method _extract_model (line 312) | def _extract_model(self, data: Dict[str, Any]) -> str:
method _extract_metadata (line 316) | def _extract_metadata(self, data: Dict[str, Any]) -> Dict[str, str]:
FILE: src/claude_monitor/error_handling.py
class ErrorLevel (line 14) | class ErrorLevel(str, Enum):
function report_error (line 21) | def report_error(
function report_file_error (line 56) | def report_file_error(
function get_error_context (line 87) | def get_error_context() -> Dict[str, Any]:
function report_application_startup_error (line 102) | def report_application_startup_error(
function report_configuration_error (line 128) | def report_configuration_error(
FILE: src/claude_monitor/monitoring/data_manager.py
class DataManager (line 13) | class DataManager:
method __init__ (line 16) | def __init__(
method get_data (line 38) | def get_data(self, force_refresh: bool = False) -> Optional[Dict[str, ...
method invalidate_cache (line 112) | def invalidate_cache(self) -> None:
method _is_cache_valid (line 118) | def _is_cache_valid(self) -> bool:
method _set_cache (line 126) | def _set_cache(self, data: Dict[str, Any]) -> None:
method cache_age (line 132) | def cache_age(self) -> float:
method last_error (line 139) | def last_error(self) -> Optional[str]:
method last_successful_fetch_time (line 144) | def last_successful_fetch_time(self) -> Optional[float]:
FILE: src/claude_monitor/monitoring/orchestrator.py
class MonitoringOrchestrator (line 16) | class MonitoringOrchestrator:
method __init__ (line 19) | def __init__(
method start (line 41) | def start(self) -> None:
method stop (line 57) | def stop(self) -> None:
method set_args (line 72) | def set_args(self, args: Any) -> None:
method register_update_callback (line 80) | def register_update_callback(
method register_session_callback (line 92) | def register_session_callback(
method force_refresh (line 102) | def force_refresh(self) -> Optional[Dict[str, Any]]:
method wait_for_initial_data (line 110) | def wait_for_initial_data(self, timeout: float = 10.0) -> bool:
method _monitoring_loop (line 121) | def _monitoring_loop(self) -> None:
method _fetch_and_process_data (line 139) | def _fetch_and_process_data(
method _calculate_token_limit (line 212) | def _calculate_token_limit(self, data: Dict[str, Any]) -> int:
FILE: src/claude_monitor/monitoring/session_monitor.py
class SessionMonitor (line 9) | class SessionMonitor:
method __init__ (line 12) | def __init__(self) -> None:
method update (line 20) | def update(self, data: Dict[str, Any]) -> Tuple[bool, List[str]]:
method validate_data (line 57) | def validate_data(self, data: Any) -> Tuple[bool, List[str]]:
method _validate_block (line 86) | def _validate_block(self, block: Any, index: int) -> List[str]:
method _on_session_change (line 120) | def _on_session_change(
method _on_session_end (line 150) | def _on_session_end(self, session_id: str) -> None:
method register_callback (line 164) | def register_callback(
method unregister_callback (line 175) | def unregister_callback(
method current_session_id (line 187) | def current_session_id(self) -> Optional[str]:
method session_count (line 192) | def session_count(self) -> int:
method session_history (line 197) | def session_history(self) -> List[Dict[str, Any]]:
FILE: src/claude_monitor/terminal/manager.py
function setup_terminal (line 22) | def setup_terminal() -> Optional[List[Any]]:
function restore_terminal (line 42) | def restore_terminal(old_settings: Optional[List[Any]]) -> None:
function enter_alternate_screen (line 58) | def enter_alternate_screen() -> None:
function handle_cleanup_and_exit (line 70) | def handle_cleanup_and_exit(
function handle_error_and_exit (line 84) | def handle_error_and_exit(
FILE: src/claude_monitor/terminal/themes.py
class BackgroundType (line 26) | class BackgroundType(Enum):
class ThemeConfig (line 35) | class ThemeConfig:
method get_color (line 50) | def get_color(self, key: str, default: str = "default") -> str:
class AdaptiveColorScheme (line 63) | class AdaptiveColorScheme:
method get_light_background_theme (line 73) | def get_light_background_theme() -> Theme:
method get_dark_background_theme (line 130) | def get_dark_background_theme() -> Theme:
method get_classic_theme (line 187) | def get_classic_theme() -> Theme:
class BackgroundDetector (line 243) | class BackgroundDetector:
method detect_background (line 251) | def detect_background() -> BackgroundType:
method _check_colorfgbg (line 281) | def _check_colorfgbg() -> BackgroundType:
method _check_environment_hints (line 309) | def _check_environment_hints() -> BackgroundType:
method _query_background_color (line 340) | def _query_background_color() -> BackgroundType:
class ThemeManager (line 452) | class ThemeManager:
method __init__ (line 455) | def __init__(self):
method _load_themes (line 461) | def _load_themes(self) -> Dict[str, ThemeConfig]:
method _get_symbols_for_theme (line 500) | def _get_symbols_for_theme(
method auto_detect_theme (line 532) | def auto_detect_theme(self) -> str:
method get_theme (line 551) | def get_theme(
method get_console (line 581) | def get_console(
method get_current_theme (line 596) | def get_current_theme(self) -> Optional[ThemeConfig]:
function get_cost_style (line 629) | def get_cost_style(cost: float) -> str:
function get_velocity_indicator (line 644) | def get_velocity_indicator(burn_rate: float) -> Dict[str, str]:
function get_theme (line 665) | def get_theme(name: Optional[str] = None) -> Theme:
function get_themed_console (line 678) | def get_themed_console(force_theme: Optional[Union[str, bool]] = None) -...
function print_themed (line 692) | def print_themed(text: str, style: str = "info") -> None:
FILE: src/claude_monitor/ui/components.py
class VelocityIndicator (line 14) | class VelocityIndicator:
method get_velocity_emoji (line 18) | def get_velocity_emoji(burn_rate: float) -> str:
method get_velocity_description (line 31) | def get_velocity_description(burn_rate: float) -> str:
method render (line 44) | def render(burn_rate: float, include_description: bool = False) -> str:
class CostIndicator (line 61) | class CostIndicator:
method render (line 65) | def render(cost: float, currency: str = "USD") -> str:
class ErrorDisplayComponent (line 80) | class ErrorDisplayComponent:
method __init__ (line 83) | def __init__(self) -> None:
method format_error_screen (line 86) | def format_error_screen(
class LoadingScreenComponent (line 114) | class LoadingScreenComponent:
method __init__ (line 117) | def __init__(self) -> None:
method create_loading_screen (line 120) | def create_loading_screen(
method create_loading_screen_renderable (line 161) | def create_loading_screen_renderable(
class AdvancedCustomLimitDisplay (line 184) | class AdvancedCustomLimitDisplay:
method __init__ (line 187) | def __init__(self, console: Console) -> None:
method _collect_session_data (line 190) | def _collect_session_data(
method _is_limit_session (line 235) | def _is_limit_session(self, session: Dict[str, Any]) -> bool:
method _calculate_session_percentiles (line 250) | def _calculate_session_percentiles(
function format_error_screen (line 297) | def format_error_screen(
FILE: src/claude_monitor/ui/display_controller.py
class DisplayController (line 35) | class DisplayController:
method __init__ (line 38) | def __init__(self) -> None:
method _extract_session_data (line 52) | def _extract_session_data(self, active_block: Dict[str, Any]) -> Dict[...
method _calculate_token_limits (line 64) | def _calculate_token_limits(self, args: Any, token_limit: int) -> Tupl...
method _calculate_time_data (line 74) | def _calculate_time_data(
method _calculate_cost_predictions (line 80) | def _calculate_cost_predictions(
method _check_notifications (line 98) | def _check_notifications(
method _format_display_times (line 151) | def _format_display_times(
method create_data_display (line 198) | def create_data_display(
method _process_active_session_data (line 304) | def _process_active_session_data(
method _calculate_model_distribution (line 395) | def _calculate_model_distribution(
method create_loading_display (line 438) | def create_loading_display(
method create_error_display (line 457) | def create_error_display(
method create_live_context (line 472) | def create_live_context(self) -> Live:
method set_screen_dimensions (line 480) | def set_screen_dimensions(self, width: int, height: int) -> None:
class LiveDisplayManager (line 490) | class LiveDisplayManager:
method __init__ (line 493) | def __init__(self, console: Optional[Console] = None) -> None:
method create_live_display (line 503) | def create_live_display(
class ScreenBufferManager (line 531) | class ScreenBufferManager:
method __init__ (line 534) | def __init__(self) -> None:
method create_screen_renderable (line 538) | def create_screen_renderable(self, screen_buffer: List[str]) -> Group:
function create_screen_renderable (line 565) | def create_screen_renderable(screen_buffer: List[str]) -> Group:
class SessionCalculator (line 574) | class SessionCalculator:
method __init__ (line 578) | def __init__(self) -> None:
method calculate_time_data (line 582) | def calculate_time_data(
method calculate_cost_predictions (line 631) | def calculate_cost_predictions(
FILE: src/claude_monitor/ui/layouts.py
class HeaderManager (line 13) | class HeaderManager:
method __init__ (line 21) | def __init__(self) -> None:
method create_header (line 26) | def create_header(
class ScreenManager (line 50) | class ScreenManager:
method __init__ (line 58) | def __init__(self) -> None:
method set_screen_dimensions (line 67) | def set_screen_dimensions(self, width: int, height: int) -> None:
method set_margins (line 77) | def set_margins(
method create_full_screen_layout (line 93) | def create_full_screen_layout(
FILE: src/claude_monitor/ui/progress_bars.py
class ModelStatsDict (line 15) | class ModelStatsDict(TypedDict, total=False):
class ProgressBarStyleConfig (line 24) | class ProgressBarStyleConfig(TypedDict, total=False):
class ThresholdConfig (line 33) | class ThresholdConfig(TypedDict):
class ProgressBarRenderer (line 40) | class ProgressBarRenderer(Protocol):
method render (line 43) | def render(self, *args: Any, **kwargs: Any) -> str:
class BaseProgressBar (line 48) | class BaseProgressBar(ABC):
method __init__ (line 61) | def __init__(self, width: int = 50) -> None:
method _validate_width (line 70) | def _validate_width(self) -> None:
method _calculate_filled_segments (line 81) | def _calculate_filled_segments(
method _render_bar (line 96) | def _render_bar(
method _format_percentage (line 126) | def _format_percentage(self, percentage: float, precision: int = 1) ->...
method _get_color_style_by_threshold (line 138) | def _get_color_style_by_threshold(
method render (line 156) | def render(self, *args, **kwargs) -> str:
class TokenProgressBar (line 166) | class TokenProgressBar(BaseProgressBar):
method render (line 185) | def render(self, percentage: float) -> str:
class TimeProgressBar (line 224) | class TimeProgressBar(BaseProgressBar):
method render (line 227) | def render(self, elapsed_minutes: float, total_minutes: float) -> str:
class ModelUsageBar (line 253) | class ModelUsageBar(BaseProgressBar):
method render (line 256) | def render(self, per_model_stats: dict[str, Any]) -> str:
FILE: src/claude_monitor/ui/session_display.py
class SessionDisplayData (line 27) | class SessionDisplayData:
class SessionDisplayComponent (line 55) | class SessionDisplayComponent:
method __init__ (line 58) | def __init__(self):
method _render_wide_progress_bar (line 64) | def _render_wide_progress_bar(self, percentage: float) -> str:
method format_active_session_screen_v2 (line 97) | def format_active_session_screen_v2(self, data: SessionDisplayData) ->...
method format_active_session_screen (line 131) | def format_active_session_screen(
method _add_notifications (line 336) | def _add_notifications(
method format_no_active_session_screen (line 378) | def format_no_active_session_screen(
FILE: src/claude_monitor/ui/table_views.py
class TableViewsController (line 22) | class TableViewsController:
method __init__ (line 25) | def __init__(self, console: Optional[Console] = None):
method _create_base_table (line 42) | def _create_base_table(
method _add_data_rows (line 87) | def _add_data_rows(
method _add_totals_row (line 117) | def _add_totals_row(self, table: Table, totals: Dict[str, Any]) -> None:
method create_daily_table (line 141) | def create_daily_table(
method create_monthly_table (line 172) | def create_monthly_table(
method create_summary_panel (line 203) | def create_summary_panel(
method _format_models (line 239) | def _format_models(self, models: List[str]) -> str:
method create_no_data_display (line 264) | def create_no_data_display(self, view_type: str) -> Panel:
method create_aggregate_table (line 290) | def create_aggregate_table(
method display_aggregated_view (line 318) | def display_aggregated_view(
FILE: src/claude_monitor/utils/formatting.py
function format_number (line 16) | def format_number(value: Union[int, float], decimals: int = 0) -> str:
function format_currency (line 31) | def format_currency(amount: float, currency: str = "USD") -> str:
function format_time (line 50) | def format_time(minutes: float) -> str:
function format_display_time (line 66) | def format_display_time(
function _get_pref (line 86) | def _get_pref(args: Any) -> bool:
FILE: src/claude_monitor/utils/model_utils.py
function normalize_model_name (line 14) | def normalize_model_name(model: str) -> str:
function get_model_display_name (line 30) | def get_model_display_name(model: str) -> str:
function is_claude_model (line 52) | def is_claude_model(model: str) -> bool:
function get_model_generation (line 65) | def get_model_generation(model: str) -> str:
FILE: src/claude_monitor/utils/notifications.py
class NotificationManager (line 9) | class NotificationManager:
method __init__ (line 12) | def __init__(self, config_dir: Path) -> None:
method _load_states (line 24) | def _load_states(self) -> Dict[str, Dict[str, Union[bool, Optional[dat...
method _save_states (line 54) | def _save_states(self) -> None:
method should_notify (line 78) | def should_notify(self, key: str, cooldown_hours: Union[int, float] = ...
method mark_notified (line 100) | def mark_notified(self, key: str) -> None:
method get_notification_state (line 106) | def get_notification_state(
method is_notification_active (line 116) | def is_notification_active(self, key: str) -> bool:
FILE: src/claude_monitor/utils/time_utils.py
function get_timezone_location (line 23) | def get_timezone_location(
class TimeFormatDetector (line 129) | class TimeFormatDetector:
method detect_from_cli (line 159) | def detect_from_cli(cls, args: Any) -> Optional[bool]:
method detect_from_timezone (line 173) | def detect_from_timezone(cls, timezone_name: str) -> Optional[bool]:
method detect_from_locale (line 195) | def detect_from_locale(cls) -> bool:
method detect_from_system (line 213) | def detect_from_system(cls) -> str:
method get_preference (line 268) | def get_preference(
class SystemTimeDetector (line 284) | class SystemTimeDetector:
method get_timezone (line 288) | def get_timezone() -> str:
method get_time_format (line 343) | def get_time_format() -> str:
class TimezoneHandler (line 348) | class TimezoneHandler:
method __init__ (line 351) | def __init__(self, default_tz: str = "UTC") -> None:
method _validate_and_get_tz (line 355) | def _validate_and_get_tz(self, tz_name: str) -> BaseTzInfo:
method parse_timestamp (line 363) | def parse_timestamp(self, timestamp_str: str) -> Optional[datetime]:
method ensure_utc (line 406) | def ensure_utc(self, dt: datetime) -> datetime:
method ensure_timezone (line 412) | def ensure_timezone(self, dt: datetime) -> datetime:
method validate_timezone (line 418) | def validate_timezone(self, tz_name: str) -> bool:
method convert_to_timezone (line 426) | def convert_to_timezone(self, dt: datetime, tz_name: str) -> datetime:
method set_timezone (line 433) | def set_timezone(self, tz_name: str) -> None:
method to_utc (line 437) | def to_utc(self, dt: datetime) -> datetime:
method to_timezone (line 441) | def to_timezone(self, dt: datetime, tz_name: Optional[str] = None) -> ...
method format_datetime (line 447) | def format_datetime(self, dt: datetime, use_12_hour: Optional[bool] = ...
function get_time_format_preference (line 461) | def get_time_format_preference(args: Any = None) -> bool:
function get_system_timezone (line 466) | def get_system_timezone() -> str:
function get_system_time_format (line 471) | def get_system_time_format() -> str:
function format_time (line 476) | def format_time(minutes: Union[int, float]) -> str:
function percentage (line 487) | def percentage(part: float, whole: float, decimal_places: int = 1) -> fl...
function format_display_time (line 504) | def format_display_time(
FILE: src/claude_monitor/utils/timezone.py
function _detect_timezone_time_preference (line 16) | def _detect_timezone_time_preference(args: Any = None) -> bool:
function parse_timestamp (line 31) | def parse_timestamp(timestamp_str: str, default_tz: str = "UTC") -> Opti...
function ensure_utc (line 45) | def ensure_utc(dt: datetime, default_tz: str = "UTC") -> datetime:
function validate_timezone (line 59) | def validate_timezone(tz_name: str) -> bool:
function convert_to_timezone (line 72) | def convert_to_timezone(
FILE: src/tests/conftest.py
function mock_pricing_calculator (line 13) | def mock_pricing_calculator() -> Mock:
function mock_timezone_handler (line 21) | def mock_timezone_handler() -> Mock:
function sample_usage_entry (line 32) | def sample_usage_entry() -> UsageEntry:
function sample_valid_data (line 48) | def sample_valid_data() -> Dict[str, Any]:
function sample_assistant_data (line 68) | def sample_assistant_data() -> Dict[str, Any]:
function sample_user_data (line 88) | def sample_user_data() -> Dict[str, Any]:
function sample_malformed_data (line 106) | def sample_malformed_data() -> Dict[str, Any]:
function sample_minimal_data (line 116) | def sample_minimal_data() -> Dict[str, Any]:
function sample_empty_tokens_data (line 126) | def sample_empty_tokens_data() -> Dict[str, Any]:
function sample_duplicate_data (line 141) | def sample_duplicate_data() -> List[Dict[str, Any]]:
function all_cost_modes (line 166) | def all_cost_modes() -> List[CostMode]:
function sample_cutoff_time (line 172) | def sample_cutoff_time() -> datetime:
function sample_processed_hashes (line 178) | def sample_processed_hashes() -> Set[str]:
function mock_file_reader (line 184) | def mock_file_reader() -> Mock:
function mock_data_filter (line 207) | def mock_data_filter() -> Mock:
function mock_usage_entry_mapper (line 219) | def mock_usage_entry_mapper() -> Mock:
function mock_data_processor (line 237) | def mock_data_processor() -> Mock:
function mock_data_manager (line 264) | def mock_data_manager() -> Mock:
function mock_session_monitor (line 285) | def mock_session_monitor() -> Mock:
function sample_monitoring_data (line 303) | def sample_monitoring_data() -> Dict[str, Any]:
function sample_session_data (line 326) | def sample_session_data() -> Dict[str, Any]:
function sample_invalid_monitoring_data (line 338) | def sample_invalid_monitoring_data() -> Dict[str, Any]:
function mock_orchestrator_args (line 353) | def mock_orchestrator_args() -> Mock:
FILE: src/tests/examples/api_examples.py
function analyze_usage_with_metadata (line 15) | def analyze_usage_with_metadata(
function analyze_usage_json (line 27) | def analyze_usage_json(hours_back=96, use_cache=True, data_path=None, in...
function get_usage_summary (line 35) | def get_usage_summary(hours_back=96, use_cache=True, data_path=None):
function print_usage_json (line 44) | def print_usage_json(hours_back=96, use_cache=True, data_path=None):
function print_usage_summary (line 52) | def print_usage_summary(hours_back=96, use_cache=True, data_path=None):
function _create_summary_stats (line 78) | def _create_summary_stats(blocks):
function example_basic_usage (line 112) | def example_basic_usage():
function example_advanced_usage (line 152) | def example_advanced_usage():
function example_json_output (line 188) | def example_json_output():
function example_usage_summary (line 213) | def example_usage_summary():
function example_custom_data_path (line 242) | def example_custom_data_path():
function example_direct_import (line 266) | def example_direct_import():
function example_error_handling (line 283) | def example_error_handling():
function example_print_functions (line 304) | def example_print_functions():
function example_compatibility_check (line 323) | def example_compatibility_check():
function run_all_examples (line 367) | def run_all_examples():
FILE: src/tests/run_tests.py
function run_tests (line 10) | def run_tests() -> int:
FILE: src/tests/test_aggregator.py
class TestAggregatedStats (line 16) | class TestAggregatedStats:
method test_init_default_values (line 19) | def test_init_default_values(self) -> None:
method test_add_entry_single (line 29) | def test_add_entry_single(self, sample_usage_entry: UsageEntry) -> None:
method test_add_entry_multiple (line 41) | def test_add_entry_multiple(self) -> None:
method test_to_dict (line 80) | def test_to_dict(self) -> None:
class TestAggregatedPeriod (line 103) | class TestAggregatedPeriod:
method test_init_default_values (line 106) | def test_init_default_values(self) -> None:
method test_add_entry_single (line 116) | def test_add_entry_single(self, sample_usage_entry: UsageEntry) -> None:
method test_add_entry_multiple_models (line 135) | def test_add_entry_multiple_models(self) -> None:
method test_add_entry_with_unknown_model (line 199) | def test_add_entry_with_unknown_model(self) -> None:
method test_to_dict_daily (line 220) | def test_to_dict_daily(self) -> None:
method test_to_dict_monthly (line 262) | def test_to_dict_monthly(self) -> None:
class TestUsageAggregator (line 282) | class TestUsageAggregator:
method aggregator (line 286) | def aggregator(self, tmp_path) -> UsageAggregator:
method sample_entries (line 291) | def sample_entries(self) -> List[UsageEntry]:
method test_aggregate_daily_basic (line 328) | def test_aggregate_daily_basic(
method test_aggregate_daily_with_date_filter (line 346) | def test_aggregate_daily_with_date_filter(
method test_aggregate_monthly_basic (line 362) | def test_aggregate_monthly_basic(
method test_aggregate_monthly_with_date_filter (line 391) | def test_aggregate_monthly_with_date_filter(
method test_aggregate_from_blocks_daily (line 403) | def test_aggregate_from_blocks_daily(
method test_aggregate_from_blocks_monthly (line 441) | def test_aggregate_from_blocks_monthly(
method test_aggregate_from_blocks_invalid_view_type (line 461) | def test_aggregate_from_blocks_invalid_view_type(
method test_calculate_totals_empty (line 478) | def test_calculate_totals_empty(self, aggregator: UsageAggregator) -> ...
method test_calculate_totals_with_data (line 490) | def test_calculate_totals_with_data(self, aggregator: UsageAggregator)...
method test_aggregate_daily_empty_entries (line 525) | def test_aggregate_daily_empty_entries(self, aggregator: UsageAggregat...
method test_aggregate_monthly_empty_entries (line 530) | def test_aggregate_monthly_empty_entries(self, aggregator: UsageAggreg...
method test_period_sorting (line 535) | def test_period_sorting(self, aggregator: UsageAggregator) -> None:
FILE: src/tests/test_analysis.py
class TestAnalyzeUsage (line 27) | class TestAnalyzeUsage:
method test_analyze_usage_basic (line 33) | def test_analyze_usage_basic(
method test_analyze_usage_quick_start_no_hours (line 80) | def test_analyze_usage_quick_start_no_hours(
method test_analyze_usage_quick_start_with_hours (line 102) | def test_analyze_usage_quick_start_with_hours(
method test_analyze_usage_with_limits (line 124) | def test_analyze_usage_with_limits(
method test_analyze_usage_no_raw_entries (line 169) | def test_analyze_usage_no_raw_entries(
class TestProcessBurnRates (line 204) | class TestProcessBurnRates:
method test_process_burn_rates_active_block (line 207) | def test_process_burn_rates_active_block(self) -> None:
method test_process_burn_rates_no_burn_rate (line 248) | def test_process_burn_rates_no_burn_rate(self) -> None:
method test_process_burn_rates_no_projection (line 266) | def test_process_burn_rates_no_projection(self) -> None:
class TestCreateResult (line 287) | class TestCreateResult:
method test_create_result_basic (line 291) | def test_create_result_basic(self, mock_convert: Mock) -> None:
method test_create_result_empty (line 320) | def test_create_result_empty(self) -> None:
class TestLimitFunctions (line 333) | class TestLimitFunctions:
method test_is_limit_in_block_timerange_within_range (line 336) | def test_is_limit_in_block_timerange_within_range(self) -> None:
method test_is_limit_in_block_timerange_outside_range (line 348) | def test_is_limit_in_block_timerange_outside_range(self) -> None:
method test_is_limit_in_block_timerange_no_timezone (line 360) | def test_is_limit_in_block_timerange_no_timezone(self) -> None:
method test_format_limit_info_complete (line 372) | def test_format_limit_info_complete(self) -> None:
method test_format_limit_info_no_reset_time (line 390) | def test_format_limit_info_no_reset_time(self) -> None:
class TestBlockConversion (line 408) | class TestBlockConversion:
method test_format_block_entries (line 411) | def test_format_block_entries(self) -> None:
method test_create_base_block_dict (line 452) | def test_create_base_block_dict(self) -> None:
method test_add_optional_block_data_all_fields (line 511) | def test_add_optional_block_data_all_fields(self) -> None:
method test_add_optional_block_data_no_fields (line 540) | def test_add_optional_block_data_no_fields(self) -> None:
method test_convert_blocks_to_dict_format (line 560) | def test_convert_blocks_to_dict_format(
FILE: src/tests/test_calculations.py
class TestBurnRateCalculator (line 18) | class TestBurnRateCalculator:
method calculator (line 22) | def calculator(self) -> BurnRateCalculator:
method mock_active_block (line 27) | def mock_active_block(self) -> Mock:
method mock_inactive_block (line 43) | def mock_inactive_block(self) -> Mock:
method test_calculate_burn_rate_active_block (line 52) | def test_calculate_burn_rate_active_block(
method test_calculate_burn_rate_inactive_block (line 65) | def test_calculate_burn_rate_inactive_block(
method test_calculate_burn_rate_zero_duration (line 72) | def test_calculate_burn_rate_zero_duration(
method test_calculate_burn_rate_no_tokens (line 80) | def test_calculate_burn_rate_no_tokens(
method test_calculate_burn_rate_edge_case_small_duration (line 93) | def test_calculate_burn_rate_edge_case_small_duration(
method test_project_block_usage_success (line 104) | def test_project_block_usage_success(
method test_project_block_usage_no_remaining_time (line 129) | def test_project_block_usage_no_remaining_time(
method test_project_block_usage_no_burn_rate (line 145) | def test_project_block_usage_no_burn_rate(
class TestHourlyBurnRateCalculation (line 153) | class TestHourlyBurnRateCalculation:
method current_time (line 157) | def current_time(self) -> datetime:
method mock_blocks (line 162) | def mock_blocks(self) -> List[Dict[str, Any]]:
method test_calculate_hourly_burn_rate_empty_blocks (line 187) | def test_calculate_hourly_burn_rate_empty_blocks(
method test_calculate_hourly_burn_rate_none_blocks (line 194) | def test_calculate_hourly_burn_rate_none_blocks(
method test_calculate_hourly_burn_rate_success (line 202) | def test_calculate_hourly_burn_rate_success(
method test_calculate_hourly_burn_rate_zero_tokens (line 217) | def test_calculate_hourly_burn_rate_zero_tokens(
method test_calculate_total_tokens_in_hour (line 229) | def test_calculate_total_tokens_in_hour(
method test_process_block_for_burn_rate_gap_block (line 246) | def test_process_block_for_burn_rate_gap_block(
method test_process_block_for_burn_rate_invalid_start_time (line 257) | def test_process_block_for_burn_rate_invalid_start_time(
method test_process_block_for_burn_rate_old_session (line 271) | def test_process_block_for_burn_rate_old_session(
class TestCalculationEdgeCases (line 287) | class TestCalculationEdgeCases:
method test_burn_rate_with_negative_duration (line 290) | def test_burn_rate_with_negative_duration(self) -> None:
method test_projection_with_zero_cost (line 303) | def test_projection_with_zero_cost(self) -> None:
method test_very_large_token_counts (line 319) | def test_very_large_token_counts(self) -> None:
class TestP90Calculator (line 342) | class TestP90Calculator:
method test_p90_config_creation (line 345) | def test_p90_config_creation(self) -> None:
method test_did_hit_limit_true (line 361) | def test_did_hit_limit_true(self) -> None:
method test_did_hit_limit_false (line 373) | def test_did_hit_limit_false(self) -> None:
method test_extract_sessions_basic (line 385) | def test_extract_sessions_basic(self) -> None:
method test_extract_sessions_complex_filter (line 405) | def test_extract_sessions_complex_filter(self) -> None:
method test_calculate_p90_from_blocks_with_hits (line 423) | def test_calculate_p90_from_blocks_with_hits(self) -> None:
method test_calculate_p90_from_blocks_no_hits (line 450) | def test_calculate_p90_from_blocks_no_hits(self) -> None:
method test_calculate_p90_from_blocks_empty (line 477) | def test_calculate_p90_from_blocks_empty(self) -> None:
method test_p90_calculator_init (line 502) | def test_p90_calculator_init(self) -> None:
method test_p90_calculator_custom_config (line 513) | def test_p90_calculator_custom_config(self) -> None:
method test_p90_calculator_calculate_basic (line 530) | def test_p90_calculator_calculate_basic(self) -> None:
method test_p90_calculator_calculate_empty (line 547) | def test_p90_calculator_calculate_empty(self) -> None:
method test_p90_calculator_caching (line 557) | def test_p90_calculator_caching(self) -> None:
method test_p90_calculation_edge_cases (line 576) | def test_p90_calculation_edge_cases(self) -> None:
method test_p90_quantiles_calculation (line 604) | def test_p90_quantiles_calculation(self) -> None:
FILE: src/tests/test_cli_main.py
class TestMain (line 9) | class TestMain:
method test_version_flag (line 12) | def test_version_flag(self) -> None:
method test_v_flag (line 20) | def test_v_flag(self) -> None:
method test_keyboard_interrupt_handling (line 29) | def test_keyboard_interrupt_handling(self, mock_load: Mock) -> None:
method test_exception_handling (line 38) | def test_exception_handling(self, mock_load_settings: Mock) -> None:
method test_successful_main_execution (line 47) | def test_successful_main_execution(self, mock_load_settings: Mock) -> ...
class TestFunctions (line 99) | class TestFunctions:
method test_get_standard_claude_paths (line 102) | def test_get_standard_claude_paths(self) -> None:
method test_discover_claude_data_paths_no_paths (line 111) | def test_discover_claude_data_paths_no_paths(self) -> None:
method test_discover_claude_data_paths_with_custom (line 119) | def test_discover_claude_data_paths_with_custom(self) -> None:
FILE: src/tests/test_data_reader.py
class TestLoadUsageEntries (line 32) | class TestLoadUsageEntries:
method test_load_usage_entries_basic (line 37) | def test_load_usage_entries_basic(
method test_load_usage_entries_no_files (line 73) | def test_load_usage_entries_no_files(self, mock_find_files: Mock) -> N...
method test_load_usage_entries_without_raw (line 83) | def test_load_usage_entries_without_raw(
method test_load_usage_entries_sorting (line 104) | def test_load_usage_entries_sorting(
method test_load_usage_entries_with_cutoff_time (line 132) | def test_load_usage_entries_with_cutoff_time(
method test_load_usage_entries_default_path (line 149) | def test_load_usage_entries_default_path(self) -> None:
class TestLoadAllRawEntries (line 160) | class TestLoadAllRawEntries:
method test_load_all_raw_entries_basic (line 164) | def test_load_all_raw_entries_basic(self, mock_find_files: Mock) -> None:
method test_load_all_raw_entries_with_empty_lines (line 182) | def test_load_all_raw_entries_with_empty_lines(self, mock_find_files: ...
method test_load_all_raw_entries_with_invalid_json (line 196) | def test_load_all_raw_entries_with_invalid_json(
method test_load_all_raw_entries_file_error (line 212) | def test_load_all_raw_entries_file_error(self, mock_find_files: Mock) ...
method test_load_all_raw_entries_default_path (line 223) | def test_load_all_raw_entries_default_path(self) -> None:
class TestFindJsonlFiles (line 234) | class TestFindJsonlFiles:
method test_find_jsonl_files_nonexistent_path (line 237) | def test_find_jsonl_files_nonexistent_path(self) -> None:
method test_find_jsonl_files_existing_path (line 244) | def test_find_jsonl_files_existing_path(self) -> None:
class TestProcessSingleFile (line 266) | class TestProcessSingleFile:
method mock_components (line 270) | def mock_components(self) -> Tuple[Mock, Mock]:
method test_process_single_file_valid_data (line 275) | def test_process_single_file_valid_data(
method test_process_single_file_without_raw (line 326) | def test_process_single_file_without_raw(
method test_process_single_file_filtered_entries (line 366) | def test_process_single_file_filtered_entries(self, mock_components):
method test_process_single_file_invalid_json (line 392) | def test_process_single_file_invalid_json(self, mock_components):
method test_process_single_file_read_error (line 418) | def test_process_single_file_read_error(self, mock_components):
method test_process_single_file_mapping_failure (line 438) | def test_process_single_file_mapping_failure(self, mock_components):
class TestShouldProcessEntry (line 466) | class TestShouldProcessEntry:
method timezone_handler (line 470) | def timezone_handler(self) -> Mock:
method test_should_process_entry_no_cutoff_no_hash (line 473) | def test_should_process_entry_no_cutoff_no_hash(
method test_should_process_entry_with_time_filter_pass (line 485) | def test_should_process_entry_with_time_filter_pass(
method test_should_process_entry_with_time_filter_fail (line 509) | def test_should_process_entry_with_time_filter_fail(self, timezone_han...
method test_should_process_entry_with_duplicate_hash (line 526) | def test_should_process_entry_with_duplicate_hash(self, timezone_handl...
method test_should_process_entry_no_timestamp (line 539) | def test_should_process_entry_no_timestamp(self, timezone_handler):
method test_should_process_entry_invalid_timestamp (line 550) | def test_should_process_entry_invalid_timestamp(self, timezone_handler):
class TestCreateUniqueHash (line 571) | class TestCreateUniqueHash:
method test_create_unique_hash_with_message_id_and_request_id (line 574) | def test_create_unique_hash_with_message_id_and_request_id(self) -> None:
method test_create_unique_hash_with_nested_message_id (line 580) | def test_create_unique_hash_with_nested_message_id(self) -> None:
method test_create_unique_hash_missing_message_id (line 586) | def test_create_unique_hash_missing_message_id(self) -> None:
method test_create_unique_hash_missing_request_id (line 592) | def test_create_unique_hash_missing_request_id(self) -> None:
method test_create_unique_hash_invalid_message_structure (line 598) | def test_create_unique_hash_invalid_message_structure(self) -> None:
method test_create_unique_hash_empty_data (line 604) | def test_create_unique_hash_empty_data(self) -> None:
class TestUpdateProcessedHashes (line 611) | class TestUpdateProcessedHashes:
method test_update_processed_hashes_valid_hash (line 614) | def test_update_processed_hashes_valid_hash(self) -> None:
method test_update_processed_hashes_no_hash (line 626) | def test_update_processed_hashes_no_hash(self) -> None:
class TestMapToUsageEntry (line 636) | class TestMapToUsageEntry:
method mock_components (line 640) | def mock_components(self) -> Tuple[Mock, Mock]:
method test_map_to_usage_entry_valid_data (line 645) | def test_map_to_usage_entry_valid_data(
method test_map_to_usage_entry_no_timestamp (line 710) | def test_map_to_usage_entry_no_timestamp(
method test_map_to_usage_entry_no_tokens (line 730) | def test_map_to_usage_entry_no_tokens(self, mock_components):
method test_map_to_usage_entry_exception_handling (line 761) | def test_map_to_usage_entry_exception_handling(self, mock_components):
method test_map_to_usage_entry_minimal_data (line 777) | def test_map_to_usage_entry_minimal_data(self, mock_components):
class TestIntegration (line 824) | class TestIntegration:
method test_full_workflow_integration (line 827) | def test_full_workflow_integration(self) -> None:
method test_error_handling_integration (line 926) | def test_error_handling_integration(self) -> None:
class TestPerformanceAndEdgeCases (line 998) | class TestPerformanceAndEdgeCases:
method test_large_file_processing (line 1001) | def test_large_file_processing(self) -> None:
method test_empty_directory (line 1063) | def test_empty_directory(self) -> None:
method test_memory_efficiency (line 1071) | def test_memory_efficiency(self) -> None:
class TestUsageEntryMapper (line 1101) | class TestUsageEntryMapper:
method mapper_components (line 1105) | def mapper_components(self) -> Tuple[Any, Mock, Mock]:
method test_usage_entry_mapper_init (line 1117) | def test_usage_entry_mapper_init(
method test_usage_entry_mapper_map_success (line 1126) | def test_usage_entry_mapper_map_success(
method test_usage_entry_mapper_map_failure (line 1157) | def test_usage_entry_mapper_map_failure(self, mapper_components):
method test_usage_entry_mapper_has_valid_tokens (line 1168) | def test_usage_entry_mapper_has_valid_tokens(self, mapper_components):
method test_usage_entry_mapper_extract_timestamp (line 1181) | def test_usage_entry_mapper_extract_timestamp(self, mapper_components):
method test_usage_entry_mapper_extract_model (line 1201) | def test_usage_entry_mapper_extract_model(self, mapper_components):
method test_usage_entry_mapper_extract_metadata (line 1216) | def test_usage_entry_mapper_extract_metadata(self, mapper_components):
method test_usage_entry_mapper_extract_metadata_nested (line 1227) | def test_usage_entry_mapper_extract_metadata_nested(self, mapper_compo...
method test_usage_entry_mapper_extract_metadata_defaults (line 1238) | def test_usage_entry_mapper_extract_metadata_defaults(self, mapper_com...
class TestAdditionalEdgeCases (line 1250) | class TestAdditionalEdgeCases:
method test_create_unique_hash_edge_cases (line 1253) | def test_create_unique_hash_edge_cases(self):
method test_should_process_entry_edge_cases (line 1270) | def test_should_process_entry_edge_cases(self):
method test_map_to_usage_entry_error_scenarios (line 1288) | def test_map_to_usage_entry_error_scenarios(self):
method test_load_usage_entries_timezone_handling (line 1346) | def test_load_usage_entries_timezone_handling(self):
method test_process_single_file_empty_file (line 1418) | def test_process_single_file_empty_file(self):
method test_load_usage_entries_cost_modes (line 1441) | def test_load_usage_entries_cost_modes(self):
class TestDataProcessors (line 1504) | class TestDataProcessors:
method test_timestamp_processor_init (line 1507) | def test_timestamp_processor_init(self):
method test_timestamp_processor_parse_datetime (line 1520) | def test_timestamp_processor_parse_datetime(self):
method test_timestamp_processor_parse_string_iso (line 1534) | def test_timestamp_processor_parse_string_iso(self):
method test_timestamp_processor_parse_string_fallback (line 1552) | def test_timestamp_processor_parse_string_fallback(self):
method test_timestamp_processor_parse_numeric (line 1567) | def test_timestamp_processor_parse_numeric(self):
method test_timestamp_processor_parse_invalid (line 1585) | def test_timestamp_processor_parse_invalid(self):
method test_token_extractor_basic_extraction (line 1600) | def test_token_extractor_basic_extraction(self):
method test_token_extractor_usage_field (line 1620) | def test_token_extractor_usage_field(self):
method test_token_extractor_message_usage (line 1632) | def test_token_extractor_message_usage(self):
method test_token_extractor_empty_data (line 1653) | def test_token_extractor_empty_data(self):
method test_data_converter_extract_model_name (line 1665) | def test_data_converter_extract_model_name(self):
method test_data_converter_flatten_nested_dict (line 1691) | def test_data_converter_flatten_nested_dict(self):
method test_data_converter_flatten_with_prefix (line 1712) | def test_data_converter_flatten_with_prefix(self):
method test_data_converter_to_serializable (line 1721) | def test_data_converter_to_serializable(self):
FILE: src/tests/test_display_controller.py
class TestDisplayController (line 17) | class TestDisplayController:
method controller (line 21) | def controller(self) -> Any:
method sample_active_block (line 26) | def sample_active_block(self) -> Dict[str, Any]:
method sample_args (line 46) | def sample_args(self) -> Mock:
method test_init (line 55) | def test_init(self, controller: Any) -> None:
method test_extract_session_data (line 64) | def test_extract_session_data(
method test_calculate_token_limits_standard_plan (line 76) | def test_calculate_token_limits_standard_plan(self, controller, sample...
method test_calculate_token_limits_custom_plan (line 84) | def test_calculate_token_limits_custom_plan(self, controller, sample_a...
method test_calculate_token_limits_custom_plan_no_limit (line 94) | def test_calculate_token_limits_custom_plan_no_limit(self, controller,...
method test_calculate_time_data (line 105) | def test_calculate_time_data(self, mock_burn_rate, controller):
method test_calculate_cost_predictions_valid_plan (line 129) | def test_calculate_cost_predictions_valid_plan(
method test_calculate_cost_predictions_invalid_plan (line 153) | def test_calculate_cost_predictions_invalid_plan(self, controller, sam...
method test_check_notifications_switch_to_custom (line 173) | def test_check_notifications_switch_to_custom(self, controller):
method test_check_notifications_exceed_limit (line 207) | def test_check_notifications_exceed_limit(self, controller):
method test_check_notifications_cost_will_exceed (line 241) | def test_check_notifications_cost_will_exceed(self, controller):
method test_format_display_times (line 271) | def test_format_display_times(
method test_calculate_model_distribution_empty_stats (line 300) | def test_calculate_model_distribution_empty_stats(self, controller):
method test_calculate_model_distribution_valid_stats (line 306) | def test_calculate_model_distribution_valid_stats(self, mock_normalize...
method test_create_data_display_no_data (line 327) | def test_create_data_display_no_data(self, controller, sample_args):
method test_create_data_display_no_active_block (line 334) | def test_create_data_display_no_active_block(self, controller, sample_...
method test_create_data_display_with_active_block (line 346) | def test_create_data_display_with_active_block(
method test_create_loading_display (line 400) | def test_create_loading_display(self, controller):
method test_create_error_display (line 406) | def test_create_error_display(self, controller):
method test_create_live_context (line 412) | def test_create_live_context(self, controller):
method test_set_screen_dimensions (line 418) | def test_set_screen_dimensions(self, controller):
class TestLiveDisplayManager (line 425) | class TestLiveDisplayManager:
method test_init_default (line 428) | def test_init_default(self):
method test_init_with_console (line 436) | def test_init_with_console(self):
method test_create_live_display_default (line 444) | def test_create_live_display_default(self, mock_live_class):
method test_create_live_display_custom (line 461) | def test_create_live_display_custom(self, mock_live_class):
class TestScreenBufferManager (line 481) | class TestScreenBufferManager:
method test_init (line 484) | def test_init(self):
method test_create_screen_renderable (line 493) | def test_create_screen_renderable(self, mock_group, mock_text, mock_ge...
method test_create_screen_renderable_with_objects (line 515) | def test_create_screen_renderable_with_objects(self, mock_group, mock_...
class TestDisplayControllerEdgeCases (line 533) | class TestDisplayControllerEdgeCases:
method controller (line 537) | def controller(self):
method sample_args (line 543) | def sample_args(self):
method test_process_active_session_data_exception_handling (line 552) | def test_process_active_session_data_exception_handling(
method test_format_display_times_invalid_timezone (line 569) | def test_format_display_times_invalid_timezone(self, controller, sampl...
method test_calculate_model_distribution_invalid_stats (line 586) | def test_calculate_model_distribution_invalid_stats(self, controller):
class TestDisplayControllerAdvanced (line 600) | class TestDisplayControllerAdvanced:
method controller (line 604) | def controller(self):
method sample_args_custom (line 610) | def sample_args_custom(self):
method test_create_data_display_custom_plan (line 622) | def test_create_data_display_custom_plan(
method test_create_data_display_exception_handling (line 687) | def test_create_data_display_exception_handling(self, controller):
method test_create_data_display_format_session_exception (line 714) | def test_create_data_display_format_session_exception(self, controller):
method test_process_active_session_data_comprehensive (line 765) | def test_process_active_session_data_comprehensive(self, controller):
class TestSessionCalculator (line 845) | class TestSessionCalculator:
method calculator (line 849) | def calculator(self):
method test_init (line 853) | def test_init(self, calculator):
method test_calculate_time_data_with_start_end (line 857) | def test_calculate_time_data_with_start_end(self, calculator):
method test_calculate_time_data_no_end_time (line 880) | def test_calculate_time_data_no_end_time(self, calculator):
method test_calculate_time_data_no_start_time (line 899) | def test_calculate_time_data_no_start_time(self, calculator):
method test_calculate_cost_predictions_with_cost (line 913) | def test_calculate_cost_predictions_with_cost(self, calculator):
method test_calculate_cost_predictions_no_cost_limit (line 933) | def test_calculate_cost_predictions_no_cost_limit(self, calculator):
method test_calculate_cost_predictions_zero_cost_rate (line 954) | def test_calculate_cost_predictions_zero_cost_rate(self, calculator):
function test_create_screen_renderable_legacy (line 978) | def test_create_screen_renderable_legacy(mock_manager_class):
FILE: src/tests/test_error_handling.py
class TestErrorLevel (line 11) | class TestErrorLevel:
method test_error_level_values (line 14) | def test_error_level_values(self) -> None:
method test_error_level_string_conversion (line 19) | def test_error_level_string_conversion(self) -> None:
class TestReportError (line 25) | class TestReportError:
method sample_exception (line 29) | def sample_exception(self) -> ValueError:
method sample_context_data (line 37) | def sample_context_data(self) -> Dict[str, str]:
method sample_tags (line 46) | def sample_tags(self) -> Dict[str, str]:
method test_report_error_basic (line 51) | def test_report_error_basic(
method test_report_error_with_full_context (line 65) | def test_report_error_with_full_context(
method test_report_error_with_info_level (line 96) | def test_report_error_with_info_level(
method test_report_error_logging_only (line 114) | def test_report_error_logging_only(
method test_report_error_with_context (line 130) | def test_report_error_with_context(
method test_report_error_exception_handling (line 152) | def test_report_error_exception_handling(
method test_report_error_none_exception (line 167) | def test_report_error_none_exception(self) -> None:
method test_report_error_empty_component (line 181) | def test_report_error_empty_component(self, sample_exception: ValueErr...
method test_report_error_no_tags (line 195) | def test_report_error_no_tags(
method test_report_error_no_context (line 209) | def test_report_error_no_context(
method test_report_error_complex_exception (line 228) | def test_report_error_complex_exception(self, mock_get_logger: Mock) -...
method test_report_error_empty_tags_dict (line 247) | def test_report_error_empty_tags_dict(
method test_report_error_special_characters_in_component (line 265) | def test_report_error_special_characters_in_component(
class TestErrorHandlingEdgeCases (line 281) | class TestErrorHandlingEdgeCases:
method test_error_level_equality (line 284) | def test_error_level_equality(self) -> None:
method test_error_level_in_list (line 290) | def test_error_level_in_list(self) -> None:
method test_report_error_with_unicode_data (line 298) | def test_report_error_with_unicode_data(self, mock_get_logger: Mock) -...
FILE: src/tests/test_formatting.py
class TestFormatTime (line 20) | class TestFormatTime:
method test_format_time_less_than_hour (line 23) | def test_format_time_less_than_hour(self) -> None:
method test_format_time_exact_hours (line 30) | def test_format_time_exact_hours(self) -> None:
method test_format_time_hours_and_minutes (line 36) | def test_format_time_hours_and_minutes(self) -> None:
method test_format_time_large_values (line 43) | def test_format_time_large_values(self) -> None:
method test_format_time_float_values (line 49) | def test_format_time_float_values(self) -> None:
class TestFormatCurrency (line 58) | class TestFormatCurrency:
method test_format_usd_default (line 61) | def test_format_usd_default(self) -> None:
method test_format_usd_explicit (line 69) | def test_format_usd_explicit(self) -> None:
method test_format_other_currencies (line 74) | def test_format_other_currencies(self) -> None:
method test_format_currency_edge_cases (line 80) | def test_format_currency_edge_cases(self) -> None:
class TestGetTimeFormatPreference (line 87) | class TestGetTimeFormatPreference:
method test_get_time_format_preference_no_args (line 91) | def test_get_time_format_preference_no_args(self, mock_get_pref: Mock)...
method test_get_time_format_preference_with_args (line 99) | def test_get_time_format_preference_with_args(self, mock_get_pref: Moc...
class TestFormatDisplayTime (line 108) | class TestFormatDisplayTime:
method setUp (line 111) | def setUp(self) -> None:
method test_format_display_time_24h_with_seconds (line 116) | def test_format_display_time_24h_with_seconds(self, mock_pref: Mock) -...
method test_format_display_time_24h_without_seconds (line 124) | def test_format_display_time_24h_without_seconds(self, mock_pref: Mock...
method test_format_display_time_12h_with_seconds (line 132) | def test_format_display_time_12h_with_seconds(self, mock_pref: Mock) -...
method test_format_display_time_12h_without_seconds (line 141) | def test_format_display_time_12h_without_seconds(self, mock_pref: Mock...
method test_format_display_time_auto_preference (line 150) | def test_format_display_time_auto_preference(self, mock_pref: Mock) ->...
method test_format_display_time_platform_compatibility (line 159) | def test_format_display_time_platform_compatibility(self) -> None:
method test_format_display_time_edge_cases (line 173) | def test_format_display_time_edge_cases(self) -> None:
class TestFormattingAdvanced (line 191) | class TestFormattingAdvanced:
method test_format_currency_extensive_edge_cases (line 194) | def test_format_currency_extensive_edge_cases(self) -> None:
method test_format_currency_precision_handling (line 212) | def test_format_currency_precision_handling(self) -> None:
method test_format_currency_international_formats (line 221) | def test_format_currency_international_formats(self) -> None:
method test_format_time_comprehensive_coverage (line 242) | def test_format_time_comprehensive_coverage(self) -> None:
method test_format_time_extreme_values (line 263) | def test_format_time_extreme_values(self) -> None:
method test_format_display_time_comprehensive_platform_support (line 274) | def test_format_display_time_comprehensive_platform_support(self) -> N...
method test_get_time_format_preference_edge_cases (line 297) | def test_get_time_format_preference_edge_cases(self) -> None:
method test_internal_get_pref_function (line 318) | def test_internal_get_pref_function(self) -> None:
class TestFormattingErrorHandling (line 333) | class TestFormattingErrorHandling:
method test_format_currency_error_conditions (line 336) | def test_format_currency_error_conditions(self) -> None:
method test_format_time_error_conditions (line 355) | def test_format_time_error_conditions(self) -> None:
method test_format_display_time_invalid_inputs (line 366) | def test_format_display_time_invalid_inputs(self) -> None:
class TestFormattingPerformance (line 378) | class TestFormattingPerformance:
method test_format_currency_performance_with_large_datasets (line 381) | def test_format_currency_performance_with_large_datasets(self) -> None:
method test_format_time_performance_with_large_datasets (line 397) | def test_format_time_performance_with_large_datasets(self) -> None:
class TestModelUtils (line 414) | class TestModelUtils:
method test_normalize_model_name (line 417) | def test_normalize_model_name(self) -> None:
method test_get_model_display_name (line 436) | def test_get_model_display_name(self) -> None:
method test_is_claude_model (line 449) | def test_is_claude_model(self) -> None:
method test_get_model_generation (line 462) | def test_get_model_generation(self) -> None:
FILE: src/tests/test_monitoring_orchestrator.py
function mock_data_manager (line 15) | def mock_data_manager() -> Mock:
function mock_session_monitor (line 33) | def mock_session_monitor() -> Mock:
function orchestrator (line 43) | def orchestrator(
class TestMonitoringOrchestratorInit (line 60) | class TestMonitoringOrchestratorInit:
method test_init_with_defaults (line 63) | def test_init_with_defaults(self) -> None:
method test_init_with_custom_params (line 81) | def test_init_with_custom_params(self) -> None:
class TestMonitoringOrchestratorLifecycle (line 95) | class TestMonitoringOrchestratorLifecycle:
method test_start_monitoring (line 98) | def test_start_monitoring(self, orchestrator: MonitoringOrchestrator) ...
method test_start_monitoring_already_running (line 112) | def test_start_monitoring_already_running(
method test_stop_monitoring (line 123) | def test_stop_monitoring(self, orchestrator: MonitoringOrchestrator) -...
method test_stop_monitoring_not_running (line 133) | def test_stop_monitoring_not_running(
method test_stop_monitoring_with_timeout (line 143) | def test_stop_monitoring_with_timeout(
class TestMonitoringOrchestratorCallbacks (line 159) | class TestMonitoringOrchestratorCallbacks:
method test_register_update_callback (line 162) | def test_register_update_callback(
method test_register_duplicate_callback (line 172) | def test_register_duplicate_callback(
method test_register_session_callback (line 183) | def test_register_session_callback(
class TestMonitoringOrchestratorDataProcessing (line 194) | class TestMonitoringOrchestratorDataProcessing:
method test_force_refresh (line 197) | def test_force_refresh(self, orchestrator: MonitoringOrchestrator) -> ...
method test_force_refresh_no_data (line 209) | def test_force_refresh_no_data(self, orchestrator: MonitoringOrchestra...
method test_set_args (line 217) | def test_set_args(self, orchestrator: MonitoringOrchestrator) -> None:
method test_wait_for_initial_data_success (line 226) | def test_wait_for_initial_data_success(
method test_wait_for_initial_data_timeout (line 241) | def test_wait_for_initial_data_timeout(
class TestMonitoringOrchestratorMonitoringLoop (line 251) | class TestMonitoringOrchestratorMonitoringLoop:
method test_monitoring_loop_initial_fetch (line 254) | def test_monitoring_loop_initial_fetch(
method test_monitoring_loop_periodic_updates (line 269) | def test_monitoring_loop_periodic_updates(
method test_monitoring_loop_stop_event (line 285) | def test_monitoring_loop_stop_event(
class TestMonitoringOrchestratorFetchAndProcess (line 302) | class TestMonitoringOrchestratorFetchAndProcess:
method test_fetch_and_process_success (line 305) | def test_fetch_and_process_success(
method test_fetch_and_process_no_data (line 341) | def test_fetch_and_process_no_data(
method test_fetch_and_process_validation_failure (line 351) | def test_fetch_and_process_validation_failure(
method test_fetch_and_process_callback_success (line 363) | def test_fetch_and_process_callback_success(
method test_fetch_and_process_callback_error (line 394) | def test_fetch_and_process_callback_error(
method test_fetch_and_process_exception_handling (line 423) | def test_fetch_and_process_exception_handling(
method test_fetch_and_process_first_data_event (line 437) | def test_fetch_and_process_first_data_event(
class TestMonitoringOrchestratorTokenLimitCalculation (line 459) | class TestMonitoringOrchestratorTokenLimitCalculation:
method test_calculate_token_limit_no_args (line 462) | def test_calculate_token_limit_no_args(
method test_calculate_token_limit_pro_plan (line 472) | def test_calculate_token_limit_pro_plan(
method test_calculate_token_limit_custom_plan (line 491) | def test_calculate_token_limit_custom_plan(
method test_calculate_token_limit_exception (line 514) | def test_calculate_token_limit_exception(
class TestMonitoringOrchestratorIntegration (line 533) | class TestMonitoringOrchestratorIntegration:
method test_full_monitoring_cycle (line 536) | def test_full_monitoring_cycle(self, orchestrator: MonitoringOrchestra...
method test_monitoring_with_session_changes (line 586) | def test_monitoring_with_session_changes(
method test_monitoring_error_recovery (line 659) | def test_monitoring_error_recovery(
class TestMonitoringOrchestratorThreadSafety (line 704) | class TestMonitoringOrchestratorThreadSafety:
method test_concurrent_callback_registration (line 707) | def test_concurrent_callback_registration(
method test_concurrent_start_stop (line 733) | def test_concurrent_start_stop(self, orchestrator: MonitoringOrchestra...
class TestMonitoringOrchestratorProperties (line 758) | class TestMonitoringOrchestratorProperties:
method test_last_valid_data_property (line 761) | def test_last_valid_data_property(
method test_monitoring_state_consistency (line 781) | def test_monitoring_state_consistency(
class TestSessionMonitor (line 800) | class TestSessionMonitor:
method test_session_monitor_init (line 803) | def test_session_monitor_init(self) -> None:
method test_session_monitor_update_valid_data (line 813) | def test_session_monitor_update_valid_data(self) -> None:
method test_session_monitor_update_invalid_data (line 836) | def test_session_monitor_update_invalid_data(self) -> None:
method test_session_monitor_validation_empty_data (line 847) | def test_session_monitor_validation_empty_data(self) -> None:
method test_session_monitor_validation_missing_blocks (line 858) | def test_session_monitor_validation_missing_blocks(self) -> None:
method test_session_monitor_validation_invalid_blocks (line 870) | def test_session_monitor_validation_invalid_blocks(self) -> None:
method test_session_monitor_register_callback (line 882) | def test_session_monitor_register_callback(self) -> None:
method test_session_monitor_callback_execution (line 893) | def test_session_monitor_callback_execution(self) -> None:
method test_session_monitor_session_history (line 920) | def test_session_monitor_session_history(self) -> None:
method test_session_monitor_current_session_tracking (line 943) | def test_session_monitor_current_session_tracking(self) -> None:
method test_session_monitor_multiple_blocks (line 966) | def test_session_monitor_multiple_blocks(self) -> None:
method test_session_monitor_no_active_session (line 996) | def test_session_monitor_no_active_session(self) -> None:
FILE: src/tests/test_pricing.py
class TestPricingCalculator (line 11) | class TestPricingCalculator:
method calculator (line 15) | def calculator(self) -> PricingCalculator:
method custom_pricing (line 20) | def custom_pricing(self) -> Dict[str, Dict[str, float]]:
method custom_calculator (line 32) | def custom_calculator(
method sample_entry_data (line 39) | def sample_entry_data(self) -> Dict[str, Union[str, int, None]]:
method token_counts (line 51) | def token_counts(self) -> TokenCounts:
method test_init_default_pricing (line 60) | def test_init_default_pricing(self, calculator: PricingCalculator) -> ...
method test_init_custom_pricing (line 69) | def test_init_custom_pricing(
method test_fallback_pricing_structure (line 78) | def test_fallback_pricing_structure(self, calculator: PricingCalculato...
method test_calculate_cost_claude_3_haiku_basic (line 98) | def test_calculate_cost_claude_3_haiku_basic(
method test_calculate_cost_claude_3_opus_with_cache (line 110) | def test_calculate_cost_claude_3_opus_with_cache(
method test_calculate_cost_claude_3_sonnet (line 131) | def test_calculate_cost_claude_3_sonnet(
method test_calculate_cost_claude_3_5_sonnet (line 142) | def test_calculate_cost_claude_3_5_sonnet(
method test_calculate_cost_with_token_counts_object (line 153) | def test_calculate_cost_with_token_counts_object(
method test_calculate_cost_token_counts_overrides_individual_params (line 167) | def test_calculate_cost_token_counts_overrides_individual_params(
method test_calculate_cost_synthetic_model (line 187) | def test_calculate_cost_synthetic_model(
method test_calculate_cost_unknown_model (line 196) | def test_calculate_cost_unknown_model(self, calculator: PricingCalcula...
method test_calculate_cost_zero_tokens (line 203) | def test_calculate_cost_zero_tokens(self, calculator: PricingCalculato...
method test_calculate_cost_for_entry_auto_mode (line 210) | def test_calculate_cost_for_entry_auto_mode(
method test_calculate_cost_for_entry_cached_mode_with_existing_cost (line 226) | def test_calculate_cost_for_entry_cached_mode_with_existing_cost(
method test_calculate_cost_for_entry_cached_mode_without_existing_cost (line 240) | def test_calculate_cost_for_entry_cached_mode_without_existing_cost(
method test_calculate_cost_for_entry_calculated_mode (line 252) | def test_calculate_cost_for_entry_calculated_mode(
method test_calculate_cost_for_entry_missing_model (line 269) | def test_calculate_cost_for_entry_missing_model(
method test_calculate_cost_for_entry_with_defaults (line 282) | def test_calculate_cost_for_entry_with_defaults(
method test_custom_pricing_calculator (line 294) | def test_custom_pricing_calculator(
method test_cost_calculation_precision (line 305) | def test_cost_calculation_precision(self, calculator: PricingCalculato...
method test_cost_calculation_large_numbers (line 315) | def test_cost_calculation_large_numbers(
method test_all_supported_models (line 328) | def test_all_supported_models(self, calculator: PricingCalculator) -> ...
method test_cache_token_costs (line 347) | def test_cache_token_costs(self, calculator: PricingCalculator) -> None:
method test_model_name_normalization_integration (line 373) | def test_model_name_normalization_integration(
FILE: src/tests/test_session_analyzer.py
class TestSessionAnalyzer (line 10) | class TestSessionAnalyzer:
method test_session_analyzer_init (line 13) | def test_session_analyzer_init(self) -> None:
method test_session_analyzer_init_custom_duration (line 21) | def test_session_analyzer_init_custom_duration(self) -> None:
method test_transform_to_blocks_empty_list (line 28) | def test_transform_to_blocks_empty_list(self) -> None:
method test_transform_to_blocks_single_entry (line 35) | def test_transform_to_blocks_single_entry(self) -> None:
method test_transform_to_blocks_multiple_entries_same_block (line 53) | def test_transform_to_blocks_multiple_entries_same_block(self) -> None:
method test_transform_to_blocks_multiple_blocks (line 80) | def test_transform_to_blocks_multiple_blocks(self) -> None:
method test_should_create_new_block_time_gap (line 108) | def test_should_create_new_block_time_gap(self) -> None:
method test_round_to_hour (line 140) | def test_round_to_hour(self) -> None:
method test_create_new_block (line 164) | def test_create_new_block(self) -> None:
method test_add_entry_to_block (line 182) | def test_add_entry_to_block(self) -> None:
method test_finalize_block (line 216) | def test_finalize_block(self) -> None:
method test_detect_limits_empty_list (line 242) | def test_detect_limits_empty_list(self) -> None:
method test_detect_limits_no_limits (line 249) | def test_detect_limits_no_limits(self) -> None:
method test_detect_single_limit_rate_limit (line 265) | def test_detect_single_limit_rate_limit(self) -> None:
method test_detect_single_limit_opus_limit (line 287) | def test_detect_single_limit_opus_limit(self) -> None:
method test_is_opus_limit (line 309) | def test_is_opus_limit(self) -> None:
method test_extract_wait_time (line 333) | def test_extract_wait_time(self) -> None:
method test_parse_reset_timestamp (line 352) | def test_parse_reset_timestamp(self) -> None:
method test_mark_active_blocks (line 368) | def test_mark_active_blocks(self) -> None:
class TestSessionAnalyzerIntegration (line 396) | class TestSessionAnalyzerIntegration:
method test_full_analysis_workflow (line 399) | def test_full_analysis_workflow(self) -> None:
method test_limit_detection_workflow (line 447) | def test_limit_detection_workflow(self) -> None:
class TestSessionAnalyzerEdgeCases (line 484) | class TestSessionAnalyzerEdgeCases:
method test_malformed_entry_handling (line 487) | def test_malformed_entry_handling(self) -> None:
method test_negative_token_counts (line 504) | def test_negative_token_counts(self) -> None:
method test_very_large_time_gaps (line 522) | def test_very_large_time_gaps(self) -> None:
FILE: src/tests/test_settings.py
class TestLastUsedParams (line 15) | class TestLastUsedParams:
method setup_method (line 18) | def setup_method(self) -> None:
method teardown_method (line 23) | def teardown_method(self) -> None:
method test_init_default_config_dir (line 29) | def test_init_default_config_dir(self) -> None:
method test_init_custom_config_dir (line 36) | def test_init_custom_config_dir(self) -> None:
method test_save_success (line 43) | def test_save_success(self) -> None:
method test_save_without_custom_limit (line 81) | def test_save_without_custom_limit(self) -> None:
method test_save_creates_directory (line 106) | def test_save_creates_directory(self) -> None:
method test_save_error_handling (line 133) | def test_save_error_handling(self, mock_logger: Mock) -> None:
method test_load_success (line 153) | def test_load_success(self) -> None:
method test_load_file_not_exists (line 182) | def test_load_file_not_exists(self) -> None:
method test_load_error_handling (line 188) | def test_load_error_handling(self, mock_logger: Mock) -> None:
method test_clear_success (line 199) | def test_clear_success(self) -> None:
method test_clear_file_not_exists (line 213) | def test_clear_file_not_exists(self) -> None:
method test_clear_error_handling (line 219) | def test_clear_error_handling(self, mock_logger: Mock) -> None:
method test_exists_true (line 229) | def test_exists_true(self) -> None:
method test_exists_false (line 236) | def test_exists_false(self) -> None:
class TestSettings (line 241) | class TestSettings:
method test_default_values (line 244) | def test_default_values(self) -> None:
method test_plan_validator_valid_values (line 262) | def test_plan_validator_valid_values(self) -> None:
method test_plan_validator_case_insensitive (line 270) | def test_plan_validator_case_insensitive(self) -> None:
method test_plan_validator_invalid_value (line 278) | def test_plan_validator_invalid_value(self) -> None:
method test_theme_validator_valid_values (line 283) | def test_theme_validator_valid_values(self) -> None:
method test_theme_validator_case_insensitive (line 291) | def test_theme_validator_case_insensitive(self) -> None:
method test_theme_validator_invalid_value (line 299) | def test_theme_validator_invalid_value(self) -> None:
method test_timezone_validator_valid_values (line 304) | def test_timezone_validator_valid_values(self) -> None:
method test_timezone_validator_invalid_value (line 320) | def test_timezone_validator_invalid_value(self) -> None:
method test_time_format_validator_valid_values (line 325) | def test_time_format_validator_valid_values(self) -> None:
method test_time_format_validator_invalid_value (line 333) | def test_time_format_validator_invalid_value(self) -> None:
method test_log_level_validator_valid_values (line 338) | def test_log_level_validator_valid_values(self) -> None:
method test_log_level_validator_invalid_value (line 350) | def test_log_level_validator_invalid_value(self) -> None:
method test_field_constraints (line 355) | def test_field_constraints(self) -> None:
method test_load_with_last_used_version_flag (line 385) | def test_load_with_last_used_version_flag(
method test_load_with_last_used_clear_flag (line 398) | def test_load_with_last_used_clear_flag(
method test_load_with_last_used_merge_params (line 426) | def test_load_with_last_used_merge_params(
method test_load_with_last_used_cli_priority (line 460) | def test_load_with_last_used_cli_priority(
method test_load_with_last_used_auto_timezone (line 491) | def test_load_with_last_used_auto_timezone(
method test_load_with_last_used_debug_flag (line 510) | def test_load_with_last_used_debug_flag(
method test_load_with_last_used_theme_detection (line 530) | def test_load_with_last_used_theme_detection(
method test_load_with_last_used_custom_plan_reset (line 556) | def test_load_with_last_used_custom_plan_reset(
method test_to_namespace (line 576) | def test_to_namespace(self) -> None:
method test_to_namespace_none_values (line 608) | def test_to_namespace_none_values(self) -> None:
class TestSettingsIntegration (line 618) | class TestSettingsIntegration:
method test_complete_workflow (line 621) | def test_complete_workflow(self) -> None:
method test_settings_customise_sources (line 657) | def test_settings_customise_sources(self) -> None:
FILE: src/tests/test_table_views.py
class TestTableViewsController (line 12) | class TestTableViewsController:
method controller (line 16) | def controller(self) -> TableViewsController:
method sample_daily_data (line 21) | def sample_daily_data(self) -> List[Dict[str, Any]]:
method sample_monthly_data (line 75) | def sample_monthly_data(self) -> List[Dict[str, Any]]:
method sample_totals (line 137) | def sample_totals(self) -> Dict[str, Any]:
method test_init_styles (line 149) | def test_init_styles(self, controller: TableViewsController) -> None:
method test_create_daily_table_structure (line 160) | def test_create_daily_table_structure(
method test_create_daily_table_data (line 189) | def test_create_daily_table_data(
method test_create_monthly_table_structure (line 205) | def test_create_monthly_table_structure(
method test_create_monthly_table_data (line 236) | def test_create_monthly_table_data(
method test_create_summary_panel (line 254) | def test_create_summary_panel(
method test_format_models_single (line 267) | def test_format_models_single(self, controller: TableViewsController) ...
method test_format_models_multiple (line 272) | def test_format_models_multiple(self, controller: TableViewsController...
method test_format_models_empty (line 280) | def test_format_models_empty(self, controller: TableViewsController) -...
method test_create_no_data_display (line 285) | def test_create_no_data_display(self, controller: TableViewsController...
method test_create_aggregate_table_daily (line 296) | def test_create_aggregate_table_daily(
method test_create_aggregate_table_monthly (line 310) | def test_create_aggregate_table_monthly(
method test_create_aggregate_table_invalid_view_type (line 324) | def test_create_aggregate_table_invalid_view_type(
method test_daily_table_timezone_display (line 336) | def test_daily_table_timezone_display(
method test_monthly_table_timezone_display (line 350) | def test_monthly_table_timezone_display(
method test_table_with_zero_tokens (line 362) | def test_table_with_zero_tokens(self, controller: TableViewsController...
method test_summary_panel_different_periods (line 396) | def test_summary_panel_different_periods(
method test_no_data_display_different_view_types (line 413) | def test_no_data_display_different_view_types(
method test_number_formatting_integration (line 422) | def test_number_formatting_integration(
method test_currency_formatting_integration (line 436) | def test_currency_formatting_integration(
method test_table_column_alignment (line 450) | def test_table_column_alignment(
method test_empty_data_lists (line 463) | def test_empty_data_lists(self, controller: TableViewsController) -> N...
FILE: src/tests/test_time_utils.py
class TestTimeFormatDetector (line 25) | class TestTimeFormatDetector:
method test_detect_from_cli_12h (line 28) | def test_detect_from_cli_12h(self) -> None:
method test_detect_from_cli_24h (line 36) | def test_detect_from_cli_24h(self) -> None:
method test_detect_from_cli_none (line 44) | def test_detect_from_cli_none(self) -> None:
method test_detect_from_cli_no_args (line 52) | def test_detect_from_cli_no_args(self) -> None:
method test_detect_from_cli_no_attribute (line 57) | def test_detect_from_cli_no_attribute(self) -> None:
method test_detect_from_timezone_with_babel_12h (line 67) | def test_detect_from_timezone_with_babel_12h(self, mock_get_location: ...
method test_detect_from_timezone_with_babel_24h (line 76) | def test_detect_from_timezone_with_babel_24h(self, mock_get_location: ...
method test_detect_from_timezone_with_babel_exception (line 85) | def test_detect_from_timezone_with_babel_exception(
method test_detect_from_timezone_no_babel (line 95) | def test_detect_from_timezone_no_babel(self) -> None:
method test_detect_from_locale_12h_ampm (line 102) | def test_detect_from_locale_12h_ampm(
method test_detect_from_locale_12h_dt_fmt (line 115) | def test_detect_from_locale_12h_dt_fmt(
method test_detect_from_locale_24h (line 128) | def test_detect_from_locale_24h(
method test_detect_from_locale_exception (line 138) | def test_detect_from_locale_exception(self, mock_setlocale: Mock) -> N...
method test_detect_from_system_macos_12h (line 147) | def test_detect_from_system_macos_12h(
method test_detect_from_system_macos_24h (line 170) | def test_detect_from_system_macos_24h(
method test_detect_from_system_linux_12h (line 193) | def test_detect_from_system_linux_12h(
method test_detect_from_system_linux_24h (line 209) | def test_detect_from_system_linux_24h(
method test_detect_from_system_windows_12h (line 225) | def test_detect_from_system_windows_12h(self, mock_system: Mock) -> None:
method test_detect_from_system_windows_24h (line 243) | def test_detect_from_system_windows_24h(self, mock_system: Mock) -> None:
method test_detect_from_system_windows_exception (line 261) | def test_detect_from_system_windows_exception(self, mock_system: Mock)...
method test_detect_from_system_unknown_platform (line 278) | def test_detect_from_system_unknown_platform(self, mock_system: Mock) ...
method test_get_preference_cli_priority (line 286) | def test_get_preference_cli_priority(self) -> None:
method test_get_preference_timezone_fallback (line 297) | def test_get_preference_timezone_fallback(self) -> None:
method test_get_preference_system_fallback (line 308) | def test_get_preference_system_fallback(self) -> None:
class TestSystemTimeDetector (line 318) | class TestSystemTimeDetector:
method test_get_timezone_linux_timezone_file (line 325) | def test_get_timezone_linux_timezone_file(
method test_get_timezone_linux_timedatectl (line 345) | def test_get_timezone_linux_timedatectl(
method test_get_timezone_windows (line 364) | def test_get_timezone_windows(self, mock_run: Mock, mock_system: Mock)...
method test_get_timezone_unknown_system (line 377) | def test_get_timezone_unknown_system(self, mock_system: Mock) -> None:
method test_get_time_format (line 384) | def test_get_time_format(self) -> None:
class TestTimezoneHandler (line 391) | class TestTimezoneHandler:
method test_init_default (line 394) | def test_init_default(self) -> None:
method test_init_custom_valid (line 399) | def test_init_custom_valid(self) -> None:
method test_init_custom_invalid (line 404) | def test_init_custom_invalid(self) -> None:
method test_validate_and_get_tz_valid (line 411) | def test_validate_and_get_tz_valid(self) -> None:
method test_validate_and_get_tz_invalid (line 417) | def test_validate_and_get_tz_invalid(self) -> None:
method test_parse_timestamp_iso_with_z (line 425) | def test_parse_timestamp_iso_with_z(self) -> None:
method test_parse_timestamp_iso_with_offset (line 433) | def test_parse_timestamp_iso_with_offset(self) -> None:
method test_parse_timestamp_iso_with_microseconds (line 441) | def test_parse_timestamp_iso_with_microseconds(self) -> None:
method test_parse_timestamp_iso_no_timezone (line 449) | def test_parse_timestamp_iso_no_timezone(self) -> None:
method test_parse_timestamp_invalid_iso (line 457) | def test_parse_timestamp_invalid_iso(self) -> None:
method test_parse_timestamp_alternative_formats (line 465) | def test_parse_timestamp_alternative_formats(self) -> None:
method test_parse_timestamp_empty (line 481) | def test_parse_timestamp_empty(self) -> None:
method test_parse_timestamp_none (line 487) | def test_parse_timestamp_none(self) -> None:
method test_parse_timestamp_invalid_format (line 493) | def test_parse_timestamp_invalid_format(self) -> None:
method test_ensure_utc_naive (line 499) | def test_ensure_utc_naive(self) -> None:
method test_ensure_utc_aware (line 507) | def test_ensure_utc_aware(self) -> None:
method test_ensure_timezone_naive (line 515) | def test_ensure_timezone_naive(self) -> None:
method test_ensure_timezone_aware (line 523) | def test_ensure_timezone_aware(self) -> None:
method test_validate_timezone_valid (line 531) | def test_validate_timezone_valid(self) -> None:
method test_validate_timezone_invalid (line 537) | def test_validate_timezone_invalid(self) -> None:
method test_convert_to_timezone_naive (line 542) | def test_convert_to_timezone_naive(self) -> None:
method test_convert_to_timezone_aware (line 550) | def test_convert_to_timezone_aware(self) -> None:
method test_set_timezone (line 558) | def test_set_timezone(self) -> None:
method test_to_utc (line 564) | def test_to_utc(self) -> None:
method test_to_timezone_default (line 572) | def test_to_timezone_default(self) -> None:
method test_to_timezone_specific (line 580) | def test_to_timezone_specific(self) -> None:
method test_format_datetime_default (line 588) | def test_format_datetime_default(self) -> None:
method test_format_datetime_24h (line 597) | def test_format_datetime_24h(self) -> None:
method test_format_datetime_12h (line 605) | def test_format_datetime_12h(self) -> None:
class TestPublicAPI (line 614) | class TestPublicAPI:
method test_get_time_format_preference (line 617) | def test_get_time_format_preference(self) -> None:
method test_get_system_timezone (line 629) | def test_get_system_timezone(self) -> None:
method test_get_system_time_format (line 638) | def test_get_system_time_format(self) -> None:
class TestFormattingUtilities (line 648) | class TestFormattingUtilities:
method test_format_time_minutes_only (line 651) | def test_format_time_minutes_only(self) -> None:
method test_format_time_hours_only (line 656) | def test_format_time_hours_only(self) -> None:
method test_format_time_hours_and_minutes (line 662) | def test_format_time_hours_and_minutes(self) -> None:
method test_percentage_normal (line 668) | def test_percentage_normal(self) -> None:
method test_percentage_zero_whole (line 674) | def test_percentage_zero_whole(self) -> None:
method test_percentage_decimal_places (line 678) | def test_percentage_decimal_places(self) -> None:
method test_format_display_time_12h_with_seconds (line 684) | def test_format_display_time_12h_with_seconds(self) -> None:
method test_format_display_time_12h_without_seconds (line 706) | def test_format_display_time_12h_without_seconds(self) -> None:
method test_format_display_time_24h_with_seconds (line 719) | def test_format_display_time_24h_with_seconds(self) -> None:
method test_format_display_time_24h_without_seconds (line 726) | def test_format_display_time_24h_without_seconds(self) -> None:
method test_format_display_time_auto_detect (line 733) | def test_format_display_time_auto_detect(self) -> None:
method test_format_display_time_windows_fallback (line 744) | def test_format_display_time_windows_fallback(self) -> None:
FILE: src/tests/test_timezone.py
class TestTimezoneHandler (line 16) | class TestTimezoneHandler:
method handler (line 20) | def handler(self) -> TimezoneHandler:
method custom_handler (line 25) | def custom_handler(self) -> TimezoneHandler:
method test_init_default_timezone (line 29) | def test_init_default_timezone(self, handler: TimezoneHandler) -> None:
method test_init_custom_timezone (line 34) | def test_init_custom_timezone(self, custom_handler: TimezoneHandler) -...
method test_init_invalid_timezone_fallback (line 38) | def test_init_invalid_timezone_fallback(self) -> None:
method test_validate_timezone_valid_timezones (line 46) | def test_validate_timezone_valid_timezones(self, handler: TimezoneHand...
method test_validate_timezone_invalid_timezones (line 59) | def test_validate_timezone_invalid_timezones(
method test_parse_timestamp_iso_format_with_z (line 83) | def test_parse_timestamp_iso_format_with_z(self, handler: TimezoneHand...
method test_parse_timestamp_iso_format_with_offset (line 91) | def test_parse_timestamp_iso_format_with_offset(
method test_parse_timestamp_iso_format_without_timezone (line 102) | def test_parse_timestamp_iso_format_without_timezone(
method test_parse_timestamp_with_microseconds (line 113) | def test_parse_timestamp_with_microseconds(self, handler: TimezoneHand...
method test_parse_timestamp_unix_timestamp_string (line 121) | def test_parse_timestamp_unix_timestamp_string(
method test_parse_timestamp_unix_timestamp_with_milliseconds (line 132) | def test_parse_timestamp_unix_timestamp_with_milliseconds(
method test_parse_timestamp_invalid_format (line 143) | def test_parse_timestamp_invalid_format(self, handler: TimezoneHandler...
method test_parse_timestamp_empty_string (line 148) | def test_parse_timestamp_empty_string(self, handler: TimezoneHandler) ...
method test_ensure_utc_with_utc_datetime (line 153) | def test_ensure_utc_with_utc_datetime(self, handler: TimezoneHandler) ...
method test_ensure_utc_with_naive_datetime (line 161) | def test_ensure_utc_with_naive_datetime(self, handler: TimezoneHandler...
method test_ensure_utc_with_different_timezone (line 169) | def test_ensure_utc_with_different_timezone(self, handler: TimezoneHan...
method test_ensure_timezone_utc_to_est (line 181) | def test_ensure_timezone_utc_to_est(self, handler: TimezoneHandler) ->...
method test_ensure_timezone_with_custom_timezone (line 189) | def test_ensure_timezone_with_custom_timezone(
method test_ensure_timezone_with_naive_datetime (line 199) | def test_ensure_timezone_with_naive_datetime(
method test_to_utc_from_different_timezone (line 210) | def test_to_utc_from_different_timezone(self, handler: TimezoneHandler...
method test_to_utc_with_naive_datetime (line 222) | def test_to_utc_with_naive_datetime(self, handler: TimezoneHandler) ->...
method test_to_utc_with_custom_default_timezone (line 231) | def test_to_utc_with_custom_default_timezone(
method test_to_timezone_conversion (line 243) | def test_to_timezone_conversion(self, handler: TimezoneHandler) -> None:
method test_to_timezone_with_default (line 251) | def test_to_timezone_with_default(self, custom_handler: TimezoneHandle...
method test_error_handling_integration (line 260) | def test_error_handling_integration(self, handler: TimezoneHandler) ->...
method test_format_datetime_with_timezone_preference (line 266) | def test_format_datetime_with_timezone_preference(
method test_detect_timezone_preference_integration (line 280) | def test_detect_timezone_preference_integration(
method test_comprehensive_timestamp_parsing (line 293) | def test_comprehensive_timestamp_parsing(self, handler: TimezoneHandle...
class TestTimezonePreferenceDetection (line 313) | class TestTimezonePreferenceDetection:
method test_detect_timezone_time_preference_delegation (line 316) | def test_detect_timezone_time_preference_delegation(self) -> None:
method test_detect_timezone_time_preference_with_args (line 326) | def test_detect_timezone_time_preference_with_args(self) -> None:
FILE: src/tests/test_version.py
function test_get_version_from_metadata (line 11) | def test_get_version_from_metadata() -> None:
function test_get_version_fallback_to_pyproject (line 20) | def test_get_version_fallback_to_pyproject() -> None:
function test_get_version_fallback_unknown (line 52) | def test_get_version_fallback_unknown() -> None:
function test_version_import_from_main_module (line 62) | def test_version_import_from_main_module() -> None:
function test_version_format (line 70) | def test_version_format() -> None:
function test_version_consistency (line 86) | def test_version_consistency() -> None:
function test_version_matches_pyproject (line 95) | def test_version_matches_pyproject() -> None:
Condensed preview — 77 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (825K chars).
[
{
"path": ".gitattributes",
"chars": 66,
"preview": "# Auto detect text files and perform LF normalization\n* text=auto\n"
},
{
"path": ".github/FUNDING.yml",
"chars": 91,
"preview": "github: [Maciek-roboblog]\nbuy_me_a_coffee: maciekroboblog\nthanks_dev: u/gh/maciek-roboblog\n"
},
{
"path": ".github/workflows/lint.yml",
"chars": 1663,
"preview": " name: Lint\n\n on:\n push:\n branches: [main]\n pull_request:\n branches: [main]\n\n jobs:\n ruff:\n runs-on: u"
},
{
"path": ".github/workflows/release.yml",
"chars": 3632,
"preview": " name: Release\n\n on:\n push:\n branches: [main]\n workflow_dispatch:\n\n jobs:\n check-version:\n runs-on: ubuntu"
},
{
"path": ".github/workflows/test.yml",
"chars": 3077,
"preview": "name: Test Suite\n\non:\n push:\n branches: [main, develop]\n pull_request:\n branches: [main, develop]\n\njobs:\n test:"
},
{
"path": ".github/workflows/version-bump.yml",
"chars": 4737,
"preview": " name: Version Bump Helper\n\n on:\n workflow_dispatch:\n inputs:\n bump_type:\n description: 'Version bum"
},
{
"path": ".gitignore",
"chars": 3809,
"preview": "# Byte-compiled / optimized / DLL files\n__pycache__/\n*.py[cod]\n*$py.class\n\n# C extensions\n*.so\nMAIN_INSTRUCTION.md\n.TASK"
},
{
"path": ".pre-commit-config.yaml",
"chars": 548,
"preview": "# .pre-commit-config.yaml\nrepos:\n - repo: https://github.com/astral-sh/ruff-pre-commit\n rev: v0.12.3\n hooks:\n "
},
{
"path": "CHANGELOG.md",
"chars": 12183,
"preview": "# Changelog\n\n## [3.1.0] - 2025-07-23\n\n### 🆕 New Features\n- **📊 Usage Analysis Views**: Added `--view` parameter for diff"
},
{
"path": "CONTRIBUTING.md",
"chars": 11392,
"preview": "# 🤝 Contributing Guide\n\nWelcome to the Claude Code Usage Monitor project! We're excited to have you contribute to making"
},
{
"path": "DEVELOPMENT.md",
"chars": 12690,
"preview": "# 🚧 Development Status & Roadmap\n\nCurrent implementation status and planned features for Claude Code Usage Monitor v3.0."
},
{
"path": "LICENSE",
"chars": 1063,
"preview": "MIT License\n\nCopyright (c) 2025 Maciej\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof "
},
{
"path": "README.md",
"chars": 35178,
"preview": "# 🎯 Claude Code Usage Monitor\n[](https://pypi.org/proje"
},
{
"path": "RELEASE.md",
"chars": 4338,
"preview": "# Release Process\n\nThis document describes the release process for Claude Code Usage Monitor.\n\n## Automated Release (Git"
},
{
"path": "TROUBLESHOOTING.md",
"chars": 12716,
"preview": "# 🐛 Troubleshooting Guide - Claude Monitor v3.0.0\n\n**⚠️ This guide is specifically for Claude Monitor v3.0.0** - If you'"
},
{
"path": "VERSION_MANAGEMENT.md",
"chars": 2686,
"preview": "# Version Management System\n\n## Overview\n\nThe Claude Code Usage Monitor uses a centralized version management system tha"
},
{
"path": "pyproject.toml",
"chars": 5682,
"preview": "# Automatically refactored pyproject.toml with best practices\n\n[build-system]\nrequires = [\"setuptools>=61.0.0\", \"wheel\"]"
},
{
"path": "src/claude_monitor/__init__.py",
"chars": 146,
"preview": "\"\"\"Claude Monitor - Real-time token usage monitoring for Claude AI\"\"\"\n\nfrom claude_monitor._version import __version__\n\n"
},
{
"path": "src/claude_monitor/__main__.py",
"chars": 402,
"preview": "#!/usr/bin/env python3\n\"\"\"Module execution entry point for Claude Monitor.\n\nAllows running the package as a module: pyth"
},
{
"path": "src/claude_monitor/_version.py",
"chars": 4417,
"preview": "\"\"\"Version management utilities.\n\nThis module provides centralized version management that reads from pyproject.toml\nas "
},
{
"path": "src/claude_monitor/cli/__init__.py",
"chars": 78,
"preview": "\"\"\"Claude Monitor CLI package.\"\"\"\n\nfrom .main import main\n\n__all__ = [\"main\"]\n"
},
{
"path": "src/claude_monitor/cli/bootstrap.py",
"chars": 2362,
"preview": "\"\"\"Bootstrap utilities for CLI initialization.\"\"\"\n\nimport logging\nimport os\nimport sys\nfrom logging import Handler\nfrom "
},
{
"path": "src/claude_monitor/cli/main.py",
"chars": 14848,
"preview": "\"\"\"Simplified CLI entry point using pydantic-settings.\"\"\"\n\nimport argparse\nimport contextlib\nimport logging\nimport signa"
},
{
"path": "src/claude_monitor/core/__init__.py",
"chars": 204,
"preview": "\"\"\"Core package for Claude Monitor.\n\nThis module provides the core functionality for Claude usage monitoring,\nincluding "
},
{
"path": "src/claude_monitor/core/calculations.py",
"chars": 6869,
"preview": "\"\"\"Burn rate and cost calculations for Claude Monitor.\"\"\"\n\nimport logging\nfrom datetime import datetime, timedelta, time"
},
{
"path": "src/claude_monitor/core/data_processors.py",
"chars": 7863,
"preview": "\"\"\"Centralized data processing utilities for Claude Monitor.\n\nThis module provides unified data processing functionality"
},
{
"path": "src/claude_monitor/core/models.py",
"chars": 4331,
"preview": "\"\"\"Data models for Claude Monitor.\nCore data structures for usage tracking, session management, and token calculations.\n"
},
{
"path": "src/claude_monitor/core/p90_calculator.py",
"chars": 3096,
"preview": "import time\nfrom collections.abc import Sequence\nfrom dataclasses import dataclass\nfrom functools import lru_cache\nfrom "
},
{
"path": "src/claude_monitor/core/plans.py",
"chars": 6174,
"preview": "\"\"\"Centralized plan configuration for Claude Monitor.\n\nAll plan limits (token, message, cost) live in one place (PLAN_LI"
},
{
"path": "src/claude_monitor/core/pricing.py",
"chars": 8151,
"preview": "\"\"\"Pricing calculations for Claude models.\n\nThis module provides the PricingCalculator class for calculating costs\nbased"
},
{
"path": "src/claude_monitor/core/settings.py",
"chars": 11683,
"preview": "\"\"\"Simplified settings management with CLI and last used params only.\"\"\"\n\nimport argparse\nimport json\nimport logging\nfro"
},
{
"path": "src/claude_monitor/data/__init__.py",
"chars": 110,
"preview": "\"\"\"Data package for Claude Monitor.\"\"\"\n\n# Import directly from modules without facade\n__all__: list[str] = []\n"
},
{
"path": "src/claude_monitor/data/aggregator.py",
"chars": 10022,
"preview": "\"\"\"Data aggregator for daily and monthly statistics.\n\nThis module provides functionality to aggregate Claude usage data\n"
},
{
"path": "src/claude_monitor/data/analysis.py",
"chars": 8445,
"preview": "\"\"\"\nUsage analysis functionality for Claude Monitor.\nContains the main analyze_usage function and related analysis compo"
},
{
"path": "src/claude_monitor/data/analyzer.py",
"chars": 13584,
"preview": "\"\"\"Session analyzer for Claude Monitor.\n\nCombines session block creation and limit detection functionality.\n\"\"\"\n\nimport "
},
{
"path": "src/claude_monitor/data/reader.py",
"chars": 11304,
"preview": "\"\"\"Simplified data reader for Claude Monitor.\n\nCombines functionality from file_reader, filter, mapper, and processor\nin"
},
{
"path": "src/claude_monitor/error_handling.py",
"chars": 4397,
"preview": "\"\"\"Centralized error handling utilities for Claude Monitor.\n\nThis module provides a unified interface for error reportin"
},
{
"path": "src/claude_monitor/monitoring/__init__.py",
"chars": 193,
"preview": "\"\"\"Monitoring package for Claude Monitor.\n\nProvides monitoring functionality with proper separation of concerns.\n\"\"\"\n\n# "
},
{
"path": "src/claude_monitor/monitoring/data_manager.py",
"chars": 5111,
"preview": "\"\"\"Unified data management for monitoring - combines caching and fetching.\"\"\"\n\nimport logging\nimport time\nfrom typing im"
},
{
"path": "src/claude_monitor/monitoring/orchestrator.py",
"chars": 7596,
"preview": "\"\"\"Orchestrator for monitoring components.\"\"\"\n\nimport logging\nimport threading\nimport time\nfrom typing import Any, Calla"
},
{
"path": "src/claude_monitor/monitoring/session_monitor.py",
"chars": 6534,
"preview": "\"\"\"Unified session monitoring - combines tracking and validation.\"\"\"\n\nimport logging\nfrom typing import Any, Callable, D"
},
{
"path": "src/claude_monitor/terminal/__init__.py",
"chars": 125,
"preview": "\"\"\"Terminal package for Claude Monitor.\"\"\"\n\n# Import directly from manager and themes without facade\n__all__: list[str] "
},
{
"path": "src/claude_monitor/terminal/manager.py",
"chars": 3322,
"preview": "\"\"\"Terminal management for Claude Monitor.\nRaw mode setup, input handling, and terminal control.\n\"\"\"\n\nimport logging\nimp"
},
{
"path": "src/claude_monitor/terminal/themes.py",
"chars": 27076,
"preview": "\"\"\"Unified theme management for terminal display.\"\"\"\n\nimport logging\nimport os\nimport re\nimport sys\nimport threading\nfro"
},
{
"path": "src/claude_monitor/ui/__init__.py",
"chars": 94,
"preview": "\"\"\"UI package for Claude Monitor.\"\"\"\n\n# Direct imports without facade\n__all__: list[str] = []\n"
},
{
"path": "src/claude_monitor/ui/components.py",
"chars": 9675,
"preview": "\"\"\"UI components for Claude Monitor.\n\nConsolidates display indicators, error/loading screens, and advanced custom displa"
},
{
"path": "src/claude_monitor/ui/display_controller.py",
"chars": 24945,
"preview": "\"\"\"Main display controller for Claude Monitor.\n\nOrchestrates UI components and coordinates display updates.\n\"\"\"\n\nimport "
},
{
"path": "src/claude_monitor/ui/layouts.py",
"chars": 3721,
"preview": "\"\"\"UI layout managers for Claude Monitor.\n\nThis module consolidates layout management functionality including:\n- Header "
},
{
"path": "src/claude_monitor/ui/progress_bars.py",
"chars": 10559,
"preview": "\"\"\"Progress bar components for Claude Monitor.\n\nProvides token usage, time progress, and model usage progress bars.\n\"\"\"\n"
},
{
"path": "src/claude_monitor/ui/session_display.py",
"chars": 16268,
"preview": "\"\"\"Session display components for Claude Monitor.\n\nHandles formatting of active session screens and session data display"
},
{
"path": "src/claude_monitor/ui/table_views.py",
"chars": 12374,
"preview": "\"\"\"Table views for daily and monthly statistics display.\n\nThis module provides UI components for displaying aggregated u"
},
{
"path": "src/claude_monitor/utils/__init__.py",
"chars": 69,
"preview": "\"\"\"Utilities package for Claude Monitor.\"\"\"\n\n__all__: list[str] = []\n"
},
{
"path": "src/claude_monitor/utils/formatting.py",
"chars": 2538,
"preview": "\"\"\"Formatting utilities for Claude Monitor.\n\nThis module provides formatting functions for currency, time, and display o"
},
{
"path": "src/claude_monitor/utils/model_utils.py",
"chars": 2540,
"preview": "\"\"\"Model utilities for Claude Monitor.\n\nThis module provides model-related utility functions, re-exporting from core.mod"
},
{
"path": "src/claude_monitor/utils/notifications.py",
"chars": 4908,
"preview": "\"\"\"Notification management utilities.\"\"\"\n\nimport json\nfrom datetime import datetime, timedelta\nfrom pathlib import Path\n"
},
{
"path": "src/claude_monitor/utils/time_utils.py",
"chars": 17370,
"preview": "\"\"\"Unified time utilities module combining timezone and system time functionality.\"\"\"\n\nimport contextlib\nimport locale\ni"
},
{
"path": "src/claude_monitor/utils/timezone.py",
"chars": 2347,
"preview": "\"\"\"Timezone utilities for Claude Monitor.\n\nThis module provides timezone handling functionality, re-exporting from time_"
},
{
"path": "src/tests/__init__.py",
"chars": 39,
"preview": "\"\"\"Test package for Claude Monitor.\"\"\"\n"
},
{
"path": "src/tests/conftest.py",
"chars": 9602,
"preview": "\"\"\"Shared pytest fixtures for Claude Monitor tests.\"\"\"\n\nfrom datetime import datetime, timezone\nfrom typing import Any, "
},
{
"path": "src/tests/examples/api_examples.py",
"chars": 12264,
"preview": "\"\"\"Usage examples for the Claude Monitor API wrapper.\n\nThis module demonstrates how to use the backward compatibility AP"
},
{
"path": "src/tests/run_tests.py",
"chars": 1106,
"preview": "#!/usr/bin/env python3\n\"\"\"Test runner for Claude Monitor tests.\"\"\"\n\nimport subprocess\nimport sys\nfrom pathlib import Pat"
},
{
"path": "src/tests/test_aggregator.py",
"chars": 22286,
"preview": "\"\"\"Tests for data aggregator module.\"\"\"\n\nfrom datetime import datetime, timezone\nfrom typing import List\n\nimport pytest\n"
},
{
"path": "src/tests/test_analysis.py",
"chars": 21159,
"preview": "\"\"\"Tests for data/analysis.py module.\"\"\"\n\nfrom datetime import datetime, timezone\nfrom unittest.mock import Mock, patch\n"
},
{
"path": "src/tests/test_calculations.py",
"chars": 22836,
"preview": "\"\"\"Tests for calculations module.\"\"\"\n\nfrom datetime import datetime, timedelta, timezone\nfrom typing import Any, Dict, L"
},
{
"path": "src/tests/test_cli_main.py",
"chars": 5258,
"preview": "\"\"\"Simplified tests for CLI main module.\"\"\"\n\nfrom pathlib import Path\nfrom unittest.mock import Mock, patch\n\nfrom claude"
},
{
"path": "src/tests/test_data_reader.py",
"chars": 65414,
"preview": "\"\"\"\nComprehensive tests for data/reader.py module.\n\nTests the data loading and processing functions to achieve 80%+ cove"
},
{
"path": "src/tests/test_display_controller.py",
"chars": 38745,
"preview": "\"\"\"Tests for DisplayController class.\"\"\"\n\nfrom datetime import datetime, timedelta, timezone\nfrom typing import Any, Dic"
},
{
"path": "src/tests/test_error_handling.py",
"chars": 11548,
"preview": "\"\"\"Tests for error handling module.\"\"\"\n\nfrom typing import Dict\nfrom unittest.mock import Mock, patch\n\nimport pytest\n\nfr"
},
{
"path": "src/tests/test_formatting.py",
"chars": 19757,
"preview": "\"\"\"Tests for formatting utilities.\"\"\"\n\nfrom datetime import datetime, timezone\nfrom unittest.mock import Mock, patch\n\nfr"
},
{
"path": "src/tests/test_monitoring_orchestrator.py",
"chars": 34748,
"preview": "\"\"\"Comprehensive tests for monitoring orchestrator module.\"\"\"\n\nimport threading\nimport time\nfrom typing import Any, Dict"
},
{
"path": "src/tests/test_pricing.py",
"chars": 14685,
"preview": "\"\"\"Comprehensive tests for PricingCalculator class.\"\"\"\n\nfrom typing import Dict, List, Union\n\nimport pytest\n\nfrom claude"
},
{
"path": "src/tests/test_session_analyzer.py",
"chars": 18716,
"preview": "\"\"\"Tests for session analyzer module.\"\"\"\n\nfrom datetime import datetime, timedelta, timezone\nfrom typing import Dict, Li"
},
{
"path": "src/tests/test_settings.py",
"chars": 25486,
"preview": "\"\"\"Comprehensive tests for core/settings.py module.\"\"\"\n\nimport argparse\nimport json\nimport tempfile\nfrom pathlib import "
},
{
"path": "src/tests/test_table_views.py",
"chars": 18193,
"preview": "\"\"\"Tests for table views module.\"\"\"\n\nfrom typing import Any, Dict, List\n\nimport pytest\nfrom rich.panel import Panel\nfrom"
},
{
"path": "src/tests/test_time_utils.py",
"chars": 29055,
"preview": "\"\"\"Comprehensive tests for time_utils module.\"\"\"\n\nimport locale\nimport platform\nfrom datetime import datetime\nfrom typin"
},
{
"path": "src/tests/test_timezone.py",
"chars": 13519,
"preview": "\"\"\"Comprehensive tests for TimezoneHandler class.\"\"\"\n\nfrom datetime import datetime, timezone\nfrom typing import List, U"
},
{
"path": "src/tests/test_version.py",
"chars": 4395,
"preview": "\"\"\"Tests for version management.\"\"\"\n\nfrom typing import Dict\nfrom unittest.mock import mock_open, patch\n\nimport pytest\n\n"
}
]
About this extraction
This page contains the full source code of the Maciek-roboblog/Claude-Code-Usage-Monitor GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 77 files (761.9 KB), approximately 175.0k tokens, and a symbol index with 1032 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.