Metadata-Version: 2.4
Name: burnclock
Version: 0.1.2
Summary: Burnclock: see your AI budget runway at a glance — balances, burn, days-left. Estimates, not billing truth.
Author: Jeremy Silbernagel
License: MIT
Requires-Python: >=3.12
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: typer>=0.12
Requires-Dist: rich>=13
Requires-Dist: httpx>=0.27
Requires-Dist: tomli-w>=1.0
Provides-Extra: dev
Requires-Dist: pytest>=8; extra == "dev"
Provides-Extra: tui
Requires-Dist: textual>=0.80; extra == "tui"
Dynamic: license-file

# Burnclock

**How much AI budget you have left, and how long it lasts — in one glance.**

Burnclock reads each AI provider's own balance/usage endpoint (or your local Hermes usage records),
then computes what most consoles don't show you: **burn per day, days of paid runway left, and the date
your monthly budget runs out** — per provider, in one table or one JSON envelope.

> **Estimates, not billing truth.** Every dollar is computed locally from provider responses and usage
> records. It is not a bill, not a guarantee, and not affiliated with any provider.

Example output (synthesized, not a live account):

```text
┌────────────┬───────┬───────────┬──────────┬───────────────┐
│ Provider   │ State │ Balance   │ Burn/day │ Runway (paid) │
├────────────┼───────┼───────────┼──────────┼───────────────┤
│ DeepSeek   │ OK    │ 20.23 USD │ 2.69     │ 6.0 d         │
│ OpenRouter │ EMPTY │ -0.09 USD │ 8.08     │ —             │
└────────────┴───────┴───────────┴──────────┴───────────────┘
```

## What it answers

- **Do I need to top up today?** Balance + days-left at your current burn, per provider.
- **Where is my money going?** Burn/day from your actual usage (Hermes `state.db`, deduped logs), not a guess.
- **When does the month's budget run out?** Budget bars and a projected crossing date.
- **Is this key dead or just empty?** `KEY_REJECTED` vs `EMPTY` vs `PERMISSION_DENIED` are distinct states.

## Providers

| Provider | Balance source | Verification |
|---|---|---|
| DeepSeek | `GET /user/balance` | live-verified |
| OpenRouter | `GET /api/v1/key` + `/credits` (remaining = credits − usage) | live-verified |
| Moonshot | `GET /v1/users/me/balance` | doc-unverified, disabled by default |
| xAI | management API prepaid balance (needs `team_id`) | doc-unverified, disabled by default |
| MiniMax | `account/query_balance` or `token_plan/remains` (region auto) | doc-unverified, disabled by default |
| fal | `account/billing` (prepaid/postpaid discriminated) | doc-unverified, disabled by default |

Providers without a balance API still get an honest row: `NO_API` (spend-only, e.g. priced usage records)
or `NOT_QUERYABLE` (console-only). No state ever fabricates a number.

## Install

```sh
pipx install burnclock     # isolated install, recommended for a CLI
# or
uv tool install burnclock
# or
python3 -m pip install burnclock
```

Requires Python ≥ 3.12. The `watch` TUI needs the optional extra:
`pipx install 'burnclock[tui]'` (or `pip install 'burnclock[tui]'`).

From source:

```sh
git clone https://github.com/jersilb/burnclock && cd burnclock
python3 -m venv .venv && .venv/bin/pip install -e '.[dev]'
.venv/bin/burnclock --version
```

## Use

```sh
burnclock                          # human status table
burnclock status --json            # envelope v1 + script-safe exit codes
burnclock forecast                 # paid runway, burn/day, budget crossing date
burnclock dashboard                # every known provider (disabled rows included)
burnclock budget show              # resolved monthly budgets
burnclock budget set deepseek 50 --dry-run   # offline preview, never a surprise write
burnclock watch                    # live TUI on a TTY; plain table when piped
burnclock badge                    # prompt-safe one-line balance (cached; never blocks)
burnclock completion bash|zsh|fish # print a static shell completion script
burnclock config show | path       # effective config / config file location
```

Config lives at `~/.config/burnclock/config.toml` on macOS/Linux and `%APPDATA%\burnclock\config.toml` on
Windows (the pre-rename location is still read, with a one-time warning). Keys are **never** stored in config: a provider entry names an environment
variable (e.g. `key_env = "DEEPSEEK_API_KEY"`); a secret-shaped value in config refuses to load.

Exit codes on machine paths: `0` ok · `1` internal · `2` usage · `3` config · `4` auth
(`KEY_REJECTED`/`PERMISSION_DENIED`) · `5` threshold crossed · `6` unreachable — worst wins, in the order
`1 > 2 > 3 > 4 > 5 > 6 > 0`. Human views exit 0 whenever output was produced.

`status --min <usd>` and `status --min-days <n>` turn it into an alert (exit 5) for cron and agents.

On an interactive terminal only (never for `--json` or piped output), burnclock checks PyPI at most once a
day and prints a one-line upgrade hint to stderr; set `BURNCLOCK_NO_UPGRADE_CHECK=1` to opt out.

## Badge

`burnclock badge` prints one line for a shell prompt, tmux bar, or agent surface:
`DeepSeek $12.68 · 4.3d`. It reads a small cache and never blocks on the network — a stale or missing
cache prints the last known line (else `burnclock —`) and refreshes itself in a detached background
process.

```sh
burnclock badge                      # cache hit: instant, no network, no config
burnclock badge --refresh            # collect now (same exit codes as status), write the cache
burnclock badge --print-path         # print the cat-able badge.txt path and exit
burnclock badge --provider xai --format plain
```

`--refresh` writes `badge.json` plus a plain-text sibling `badge.txt` (exactly one line, no ANSI) for
prompts that just `cat` it. Both live in `~/.cache/burnclock/` on macOS/Linux (`%LOCALAPPDATA%\burnclock\`
on Windows); `$BURNCLOCK_BADGE_CACHE` moves the cache file. The cache is fresh for 300 s
(`$BURNCLOCK_BADGE_TTL`). `--provider` picks the row; otherwise `[badge].provider` from config, then the
first provider with a numeric balance. Color is TTY-only and disabled by `NO_COLOR` or `--format plain`;
`--json` is always ANSI-free.

### In a prompt or status bar

`badge.txt` is one plain line, so a prompt only has to read the file — no Python startup, no network:

```zsh
# ~/.zshrc: right prompt, refreshed in the background at most every 4 minutes
_burnclock_precmd() {
  [[ -r ~/.cache/burnclock/badge.txt ]] &&
    RPROMPT="%F{244}$(<~/.cache/burnclock/badge.txt)%f"
  (( EPOCHSECONDS - ${_bc_last:-0} < 240 )) && return
  _bc_last=$EPOCHSECONDS
  ~/.hermes/skills/burnclock/scripts/badge-chip.sh --refresh >/dev/null 2>&1 &!
}
autoload -Uz add-zsh-hook && add-zsh-hook precmd _burnclock_precmd
```

```tmux
# ~/.tmux.conf: burnclock paints its own line, tmux refreshes it every 60 s
set -g status-interval 60
set -g status-right '#[fg=colour244]#(~/.hermes/skills/burnclock/scripts/badge-chip.sh)#[default]'
```

`badge-chip.sh` is the integration entry point: it routes through the skill shim, so a gateway- or
launchd-spawned shell (which never inherited your keys) can still refresh the cache.

The repo also ships a **Hermes desktop status-bar chip** (`desktop-plugin/`);
`bash scripts/install-badge.sh` installs it into `~/.hermes/desktop-plugins/burnclock-badge/`, where the
app hot-loads it. It polls the same cache every 60 s and tones the dot green → amber (<5 days runway) →
red (<2 days or a failed provider).

## States

`OK · LOW · CRITICAL · EMPTY · KEY_REJECTED · PERMISSION_DENIED · NO_API · NOT_QUERYABLE · ERROR · UNREACHABLE`

`KEY_REJECTED` distinguishes a dead/rotated key from a `$0` balance; `PERMISSION_DENIED` means the key is
valid but under-scoped. `EMPTY` means unknown or zero — never "free".

## Hermes skill

The repo ships a Hermes skill (`hermes-skill/`, packaged as `dist/burnclock-skill.zip`) so an agent session
can answer "how many tokens do I have left" with real numbers. Its shim reads only the six provider key
variable **names** from `~/.hermes/.env` — it never sources the file and never prints a value. A daily cron
suggestion for a runway alert lives in `hermes-skill/CRON.md`.

## Design rules

- **Read-only.** No top-ups, no spending, no provider mutations. Spending endpoints are never called.
- **Fail-closed.** Missing keys are reported as such; malformed responses are `ERROR`, never `$0`.
- **No secrets in files.** Config holds env-var names only; the packaged skill is secret-scanned.
- **Offline floor.** With zero keys it still reports what your local Hermes usage records know.
- **Estimates, not billing truth.** Runway is a range: bursty usage moves it, and providers change prices.

## Development

```sh
.venv/bin/pytest -q                            # full suite (synthetic fixtures only, no network)
bash hermes-skill/scripts/build-skill-zip.sh   # package + secret-scan the skill
```

Tests never touch a real provider: transports are faked and every fixture is synthetic. Timing protocol and
recorded numbers live in [BENCH.md](BENCH.md); release history in [CHANGELOG.md](CHANGELOG.md).

MIT licensed. Built by Jeremy Silbernagel.
