Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions .editorconfig
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
root = true

[*]
end_of_line = lf
insert_final_newline = true
charset = utf-8
indent_style = space
indent_size = 4

[*.{ts,tsx,js,jsx,json,yaml,yml,css}]
indent_size = 2

[*.swift]
indent_size = 4

[Makefile]
indent_style = tab
11 changes: 11 additions & 0 deletions .github/dependabot.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
version: 2
updates:
- package-ecosystem: "pip"
directory: "/"
schedule:
interval: "weekly"
open-pull-requests-limit: 10
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: "weekly"
17 changes: 17 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
name: CI
on:
pull_request:
branches: [main]

jobs:
check:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: astral-sh/setup-uv@v4
- run: uv sync
- run: uv run ruff check .
- run: uv run ruff format --check .
- run: uv run pyright
- run: uv run bandit -r src/
- run: uv run pytest --cov --cov-fail-under=80 -v
9 changes: 9 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
.venv/
__pycache__/
*.pyc
.env
dist/
*.egg-info/
.coverage
htmlcov/
.pyright/
1 change: 1 addition & 0 deletions .python-version
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
3.12
78 changes: 78 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
.PHONY: check fix lint typecheck security test test-unit test-e2e

# Colors and symbols
GREEN := \033[0;32m
RED := \033[0;31m
CYAN := \033[0;36m
BOLD := \033[1m
RESET := \033[0m

# Verbose flag: make check V=1 for full output
V ?= 0
ifeq ($(V),1)
QUIET :=
TEST_FLAGS := --cov --cov-fail-under=80 -v
else
QUIET := > /dev/null 2>&1
TEST_FLAGS := --cov --cov-fail-under=80 -q --no-header
endif

# All checks (mirrors CI — run before pushing a PR)
check:
@printf "\n$(BOLD)$(CYAN)🔍 Running all checks...$(RESET)\n"
@$(MAKE) --no-print-directory lint
@$(MAKE) --no-print-directory typecheck
@$(MAKE) --no-print-directory security
@$(MAKE) --no-print-directory test
@printf "\n$(BOLD)$(CYAN)🎉 All checks passed!$(RESET)\n\n"

# Lint (includes format check)
lint:
@printf "\n$(BOLD)$(CYAN)🧹 Linting...$(RESET)\n"
@uv run ruff check . $(QUIET) && printf " $(GREEN)✅ ruff lint$(RESET)\n" || { printf " $(RED)❌ ruff lint$(RESET)\n"; exit 1; }
@uv run ruff format --check . $(QUIET) && printf " $(GREEN)✅ ruff format$(RESET)\n" || { printf " $(RED)❌ ruff format$(RESET)\n"; exit 1; }

# Type checking
typecheck:
@printf "\n$(BOLD)$(CYAN)🔬 Type checking...$(RESET)\n"
@uv run pyright $(QUIET) && printf " $(GREEN)✅ pyright$(RESET)\n" || { printf " $(RED)❌ pyright$(RESET)\n"; exit 1; }

# Security scan
security:
@printf "\n$(BOLD)$(CYAN)🛡️ Security scan...$(RESET)\n"
@uv run bandit -r src/ -q $(QUIET) && printf " $(GREEN)✅ bandit$(RESET)\n" || { printf " $(RED)❌ bandit$(RESET)\n"; exit 1; }

# Tests with coverage
test:
@printf "\n$(BOLD)$(CYAN)🧪 Testing...$(RESET)\n"
@COV=$$(uv run pytest tests/copilot_usage $(TEST_FLAGS) 2>&1); \
if [ $$? -eq 0 ]; then \
COV_PCT=$$(echo "$$COV" | grep "^TOTAL" | awk '{print $$NF}' | tr -d '%'); \
printf " $(GREEN)✅ unit tests ($${COV_PCT}%% coverage)$(RESET)\n"; \
else \
printf " $(RED)❌ unit tests$(RESET)\n"; exit 1; \
fi
@E2E=$$(uv run pytest tests/e2e -q --no-header 2>&1); \
if [ $$? -eq 0 ]; then \
PASSED=$$(echo "$$E2E" | tail -1 | grep -oE '[0-9]+ passed' | awk '{print $$1}'); \
printf " $(GREEN)✅ e2e tests ($${PASSED} passed)$(RESET)\n"; \
else \
printf " $(RED)❌ e2e tests$(RESET)\n"; exit 1; \
fi

# Run only unit tests
test-unit:
@printf "\n$(BOLD)$(CYAN)🧪 Unit tests...$(RESET)\n"
@uv run pytest tests/copilot_usage -v --cov --cov-fail-under=80

# Run only e2e tests
test-e2e:
@printf "\n$(BOLD)$(CYAN)🧪 E2E tests...$(RESET)\n"
@uv run pytest tests/e2e -v

# Auto-fix
fix:
@printf "\n$(BOLD)$(CYAN)🔧 Auto-fixing...$(RESET)\n"
@uv run ruff check --fix . 2>/dev/null; uv run ruff format . $(QUIET)
@printf " $(GREEN)✅ ruff fix + format$(RESET)\n"
@printf "\n$(BOLD)$(CYAN)✨ All fixed!$(RESET)\n\n"
240 changes: 240 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,240 @@
# CLI Tools

Monorepo for personal Python CLI utilities. Each tool is a separate package under `src/` with its own entry point.

## Tools

### copilot-usage

Parses local Copilot CLI session data to show token usage, premium requests, model breakdown, and raw event counts — the data GitHub's usage dashboard doesn't show you (or shows with multi-day delays).

**Why?** GitHub's usage page has significant delays in reporting CLI premium request consumption. Your local `~/.copilot/session-state/` files have the real data — this tool surfaces it instantly.

#### Installation

```bash
# From the repo (dev mode — no install needed)
cd ~/projects/cli-tools
uv run copilot-usage summary

# Global install (available everywhere)
uv tool install ~/projects/cli-tools
copilot-usage summary
```

#### Interactive Mode

Run `copilot-usage` with no subcommand to launch the interactive session:

```
$ copilot-usage
```

This shows the full summary dashboard with a numbered session list. From there:
- Enter a **session number** to drill into that session's detail view
- Press **c** to see the cost breakdown
- Press **r** to refresh data
- Press **q** to quit

Each sub-view has a "Press Enter to go back" prompt to return home.

If `watchdog` is installed, the display auto-refreshes when session files change (2-second debounce). Install it with `uv add watchdog` for live-updating views.

#### Commands

##### `copilot-usage summary`

Show usage totals across all sessions with per-model and per-session breakdowns. Sessions table includes Model Calls (assistant.turn_start count) and User Msgs columns for raw event counts.

```
copilot-usage summary [--since DATE] [--until DATE] [--path PATH]
```

Options:
- `--since` — show sessions starting after this date (`YYYY-MM-DD` or `YYYY-MM-DDTHH:MM:SS`)
- `--until` — show sessions starting before this date
- `--path` — custom session-state directory (default: `~/.copilot/session-state/`)

Example:
```
$ copilot-usage summary

Copilot Usage Summary (2026-03-07 → 2026-03-08)

╭──────────────────────────────────────────── Totals ─────────────────────────────────────────────╮
│ 0 premium requests 2337 model calls 647 user messages 2.2M output tokens │
│ 6h 47m 42s API duration 3 sessions │
╰────────────────────────────────────────────────────────────────────────────────────────────────╯

Per-Model Breakdown
┌────────────────────┬──────────┬──────────────┬──────────────┬───────────────┬────────────┐
│ Model │ Requests │ Premium Cost │ Input Tokens │ Output Tokens │ Cache Read │
├────────────────────┼──────────┼──────────────┼──────────────┼───────────────┼────────────┤
│ claude-haiku-4.5 │ 99 │ 0 │ 2.9M │ 93.7K │ 2.4M │
│ claude-opus-4.6 │ 622 │ 0 │ 31.2M │ 1.1M │ 30.0M │
│ claude-opus-4.6-1m │ 1420 │ 1986 │ 294.7M │ 1.0M │ 278.6M │
└────────────────────┴──────────┴──────────────┴──────────────┴───────────────┴────────────┘

Sessions
┌─────────────────────┬──────────────────┬─────────┬─────────────┬───────────┬───────────────┬───────────┐
│ Name │ Model │ Premium │ Model Calls │ User Msgs │ Output Tokens │ Status │
├─────────────────────┼──────────────────┼─────────┼─────────────┼───────────┼───────────────┼───────────┤
│ Copilot CLI Usage │ claude-opus-4.6… │ 288 │ 539 │ 200 │ 468.0K │ Active 🟢 │
│ Tracker — Plan │ │ │ │ │ │ │
│ Stock Market │ claude-opus-4.6… │ 504 │ 967 │ 169 │ 483.8K │ Active 🟢 │
│ Tracker │ │ │ │ │ │ │
│ ShapeShifter │ claude-opus-4.6 │ 1194 │ 831 │ 278 │ 1.2M │ Active 🟢 │
└─────────────────────┴──────────────────┴─────────┴─────────────┴───────────┴───────────────┴───────────┘
```

##### `copilot-usage cost`

Show premium request costs from shutdown data (raw counts, no estimation).

```
copilot-usage cost [--since DATE] [--until DATE] [--path PATH]
```

Uses `render_cost_view` to show a per-session, per-model breakdown with 6 columns: Session, Model, Requests, Premium Cost, Model Calls, and Output Tokens. Resumed sessions include a "↳ Since last shutdown" row with active-period stats.

Example:
```
$ copilot-usage cost

💰 Cost Breakdown
┌──────────────────────────┬────────────────────┬──────────┬──────────────┬─────────────┬───────────────┐
│ Session │ Model │ Requests │ Premium Cost │ Model Calls │ Output Tokens │
├──────────────────────────┼────────────────────┼──────────┼──────────────┼─────────────┼───────────────┤
│ Session Alpha │ claude-opus-4.6-1m │ 235 │ 288 │ 3 │ 93.6K │
│ Session Beta │ claude-opus-4.6-1m │ 592 │ 504 │ 3 │ 207.3K │
│ Session Gamma │ claude-opus-4.6 │ 8 │ 10 │ 2 │ 400 │
│ ↳ Since last shutdown │ claude-opus-4.6 │ N/A │ N/A │ 1 │ 150 │
├──────────────────────────┼────────────────────┼──────────┼──────────────┼─────────────┼───────────────┤
│ Grand Total │ │ 835 │ 802 │ 9 │ 301.5K │
└──────────────────────────┴────────────────────┴──────────┴──────────────┴─────────────┴───────────────┘
```

##### `copilot-usage live`

Show currently active (running) Copilot sessions with real-time stats.

```
copilot-usage live [--path PATH]
```

Example:
```
$ copilot-usage live

🟢 Active Copilot Sessions
┌─────────────┬───────────────────────────────┬────────────────────┬──────────┬──────────┬───────────────┬─────────────┐
│ Session ID │ Name │ Model │ Running │ Messages │ Output Tokens │ CWD │
├─────────────┼───────────────────────────────┼────────────────────┼──────────┼──────────┼───────────────┼─────────────┤
│ 🟢 b5df8a34 │ Copilot CLI Usage Tracker — │ claude-opus-4.6-1m │ 126h 21m │ 200 │ 468,625 │ /Users/you │
│ │ Plan │ │ │ │ │ │
│ 🟢 0faecbdf │ Stock Market Tracker │ claude-opus-4.6-1m │ 136h 17m │ 169 │ 483,762 │ /Users/you │
│ 🟢 4a547040 │ ShapeShifter │ claude-opus-4.6 │ 150h 33m │ 278 │ 1,225,990 │ /Users/you │
└─────────────┴───────────────────────────────┴────────────────────┴──────────┴──────────┴───────────────┴─────────────┘
```

##### `copilot-usage session`

Show detailed per-turn breakdown for a specific session. Accepts a session ID prefix (first 8 chars is enough).

```
copilot-usage session SESSION_ID [--path PATH]
```

Example:
```
$ copilot-usage session b5df8a34
```

#### How It Works

Copilot CLI stores session data locally in `~/.copilot/session-state/{session-id}/`. Each session directory contains an `events.jsonl` file with structured events:

- **`session.start`** — session ID, start time, working directory
- **`assistant.message`** — per-message output tokens and model info
- **`session.shutdown`** — the goldmine: `totalPremiumRequests`, `modelMetrics` (per-model input/output/cache tokens, request counts), `codeChanges`
- **`tool.execution_complete`** — model used per tool call

For active sessions (no shutdown event yet), the tool sums individual message tokens to build a running total. For resumed sessions (activity after a shutdown), it merges the shutdown baseline with post-resume tokens.

#### Model Pricing (reference)

GitHub Copilot charges different premium-request multipliers per model. This tool reports raw counts (model calls, user messages, exact premium requests from shutdown data) — not estimated billing. The multiplier table below is provided for reference only:

| Model | Multiplier | Tier |
|---|---|---|
| `claude-opus-4.6` / `claude-opus-4.5` | 3× | Premium |
| `claude-opus-4.6-1m` | 6× | Premium |
| `claude-sonnet-4.6` / `claude-sonnet-4.5` / `claude-sonnet-4` | 1× | Standard |
| `gpt-5.4` / `gpt-5.2` / `gpt-5.1` / `gpt-5.x-codex` / `gpt-5.1-codex-max` | 1× | Standard |
| `gemini-3-pro-preview` | 1× | Standard |
| `claude-haiku-4.5` / `gpt-5.1-codex-mini` | 0.33× | Light |
| `gpt-5-mini` / `gpt-4.1` | 0× | Free/Included |

## Development

**Prerequisites:** [uv](https://docs.astral.sh/uv/) (v0.10+)

```bash
git clone git@github.com:microsasa/cli-tools.git
cd cli-tools
uv sync
```

### Make Targets

| Command | Description |
|---|---|
| `make check` | Run all checks (lint + typecheck + security + tests) |
| `make test` | Unit tests (80% coverage enforced) + e2e tests |
| `make test-unit` | Unit tests only with verbose output |
| `make test-e2e` | E2E tests only |
| `make lint` | Ruff lint + format check |
| `make fix` | Auto-fix lint/formatting issues |

Add `V=1` for verbose output: `make check V=1`

### Project Structure

```
cli-tools/
├── src/
│ └── copilot_usage/
│ ├── cli.py # Click commands + interactive loop + watchdog auto-refresh
│ ├── models.py # Pydantic data models
│ ├── parser.py # events.jsonl parsing
│ ├── pricing.py # Model cost multipliers
│ ├── logging_config.py # Loguru configuration
│ └── report.py # Rich terminal output
├── tests/
│ ├── copilot_usage/ # Unit tests
│ └── e2e/ # End-to-end tests with fixtures
├── docs/
│ ├── plan.md
│ ├── architecture.md
│ ├── changelog.md
│ └── implementation.md
├── Makefile
└── pyproject.toml
```

### Stack

- **Python 3.12+** with pyright strict mode
- **Click** — CLI framework
- **Rich** — terminal tables and formatting
- **Pydantic v2** — runtime-validated data models
- **Ruff** — linting + formatting
- **Bandit** — security scanning
- **pytest + pytest-cov** — testing (80% coverage minimum)

## Future Tools

- **repo-health** — audit any repo against project-standards (missing Makefile targets, wrong ruff rules, etc.)
- **session-manager** — list/search/clean up Copilot CLI sessions in `~/.copilot/session-state/`
- **docker-status** — pretty overview of all OrbStack containers, ports, health, resource usage
- **env-check** — verify dev environment matches standards (uv version, Python, pnpm, Docker, VS Code extensions)
Loading
Loading