burndown 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,27 @@
1
+ name: CI
2
+
3
+ on:
4
+ push:
5
+ branches: [ main ]
6
+ pull_request:
7
+ workflow_dispatch:
8
+
9
+ jobs:
10
+ test:
11
+ name: ${{ matrix.os }} · py${{ matrix.python }}
12
+ runs-on: ${{ matrix.os }}
13
+ strategy:
14
+ fail-fast: false
15
+ matrix:
16
+ os: [ubuntu-latest, macos-latest, windows-latest]
17
+ python: ["3.11", "3.12", "3.13"]
18
+ steps:
19
+ - uses: actions/checkout@v4
20
+ - uses: actions/setup-python@v5
21
+ with:
22
+ python-version: ${{ matrix.python }}
23
+ # Zero runtime deps — only pytest is needed to run the suite.
24
+ - run: python -m pip install --upgrade pip pytest
25
+ - run: python -m pytest tests/ -q
26
+ # Smoke: the CLI must import + run on every OS (no logs in CI is fine).
27
+ - run: python -m burndown config
@@ -0,0 +1,21 @@
1
+ # python
2
+ __pycache__/
3
+ *.py[cod]
4
+ *.egg-info/
5
+ .eggs/
6
+ build/
7
+ dist/
8
+ .pytest_cache/
9
+ .ruff_cache/
10
+ .venv/
11
+ venv/
12
+
13
+ # burndown's own generated artifacts (never commit a usage report)
14
+ burndown-report.html
15
+ *-report.html
16
+
17
+ # internal strategy / session-context doc (never ship)
18
+ BURNDOWN_STATUS.md
19
+
20
+ # os
21
+ .DS_Store
@@ -0,0 +1,25 @@
1
+ # Changelog
2
+
3
+ ## 0.1.0 — unreleased
4
+ Initial vertical slice.
5
+ - Read Claude Code usage logs (`~/.claude/projects/**/*.jsonl`), privacy-safe and
6
+ read-only; de-dup by message uuid; skip `<synthetic>`.
7
+ - Configurable per-model pricing (estimated defaults) + token-budget mode.
8
+ - Period aggregation (monthly/weekly/daily) with by-project / by-model / by-day
9
+ breakdowns and a 24h/7d recent-pace.
10
+ - Forecast: burn rate, runway (days-to-zero at recent pace), projected period
11
+ total, over-budget detection.
12
+ - CLI: `status` · `watch` · `budget` · `check` · `report` (HTML) · `config` · `currency`.
13
+ - Dual-currency display (USD + a configurable secondary currency, e.g. INR) via a
14
+ static FX rate — no live fetch, zero-network preserved (ADR-007).
15
+ - **Credit-pool guardian:** programmatic vs interactive split via the `entrypoint`
16
+ field; `scope` config + `burndown scope programmatic` make the headline + runway
17
+ meter just the June-2026 credit pool (ADR-009). Split always shown for context.
18
+ - **Local web dashboard:** `burndown serve` → auto-refreshing page on 127.0.0.1
19
+ (loopback only, self-contained, no external resources; ADR-010).
20
+ - **Cross-platform:** macOS/Linux/Windows log-dir auto-discovery, `%APPDATA%`
21
+ config on Windows, Windows ANSI enable, `\`-safe project names; CI matrix across
22
+ all three OSes × Python 3.11–3.13 (ADR-011).
23
+ - Test suite: 23 tests across parser/pricing/aggregate/forecast/currency/scope/
24
+ dashboard, incl. a structural content-blindness assertion. Zero runtime deps.
25
+ - Full decision log (8 ADRs) + threat model.
burndown-0.1.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Aryaman Gupta
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,130 @@
1
+ Metadata-Version: 2.4
2
+ Name: burndown
3
+ Version: 0.1.0
4
+ Summary: A local, real-time cockpit for your Claude credit burn — burn rate, runway forecast, and budget alerts. Zero dependencies, nothing leaves your machine.
5
+ Project-URL: Homepage, https://github.com/aryasgit/burndown
6
+ Project-URL: Source, https://github.com/aryasgit/burndown
7
+ Project-URL: Issues, https://github.com/aryasgit/burndown/issues
8
+ Project-URL: Changelog, https://github.com/aryasgit/burndown/blob/main/CHANGELOG.md
9
+ Author-email: Aryaman Gupta <rayman3304@gmail.com>
10
+ License-Expression: MIT
11
+ License-File: LICENSE
12
+ Keywords: anthropic,budget,claude,claude-code,cli,cost,credits,local-first,tokens
13
+ Classifier: Development Status :: 4 - Beta
14
+ Classifier: Environment :: Console
15
+ Classifier: Intended Audience :: Developers
16
+ Classifier: License :: OSI Approved :: MIT License
17
+ Classifier: Operating System :: OS Independent
18
+ Classifier: Programming Language :: Python :: 3 :: Only
19
+ Classifier: Programming Language :: Python :: 3.11
20
+ Classifier: Programming Language :: Python :: 3.12
21
+ Classifier: Programming Language :: Python :: 3.13
22
+ Classifier: Topic :: Utilities
23
+ Requires-Python: >=3.11
24
+ Provides-Extra: dev
25
+ Requires-Dist: pytest>=8; extra == 'dev'
26
+ Description-Content-Type: text/markdown
27
+
28
+ <div align="center">
29
+
30
+ # Burndown
31
+
32
+ **A local, real-time cockpit for your Claude credit burn.**
33
+ Burn rate · runway forecast · budget alerts — so you see "I'll run dry in 3 days" *before* it happens, not after.
34
+
35
+ [![License: MIT](https://img.shields.io/badge/License-MIT-000.svg)](LICENSE)
36
+ [![Python 3.11+](https://img.shields.io/badge/Python-3.11+-000.svg)](https://python.org)
37
+ [![Zero dependencies](https://img.shields.io/badge/dependencies-0-000.svg)]()
38
+ [![100%25 local](https://img.shields.io/badge/runs-100%25%20local-000.svg)]()
39
+
40
+ </div>
41
+
42
+ ---
43
+
44
+ Anthropic split programmatic Claude usage into a **separate monthly credit pool**
45
+ that can run out mid-month. The existing tools show you what you *already* spent —
46
+ a bank statement, after the fact. Burndown is the **fuel gauge**: how fast you're
47
+ burning right now, when you'll hit zero, and a check that fires before you overspend.
48
+
49
+ It reads the usage logs Claude Code already writes on your machine. **Nothing
50
+ leaves your computer** — zero dependencies, no network, read-only on your logs,
51
+ and it never touches your prompts or code (see [SECURITY.md](docs/SECURITY.md)).
52
+
53
+ ## Quickstart
54
+
55
+ ```bash
56
+ pipx install git+https://github.com/aryasgit/burndown.git # works today (PyPI: `pipx install burndown` — soon)
57
+ burndown # snapshot of this period
58
+ burndown budget 100 # set your monthly credit-pool budget → get a runway
59
+ burndown scope programmatic # guardian mode: meter just the credit pool
60
+ burndown currency INR # show INR next to USD (static rate, no live fetch)
61
+ burndown watch # live terminal dashboard …
62
+ burndown serve # … or a web dashboard at http://127.0.0.1:8787
63
+ ```
64
+
65
+ No install? From a clone: `python -m burndown` (needs Python 3.11+, nothing else).
66
+
67
+ ```
68
+ BURNDOWN monthly period · resets Jul 01
69
+
70
+ $41.80 / $100.00 ███████████░░░░░░░░░░░░░░░░░ 42%
71
+
72
+ burn rate $6.10/day (last 24h) avg $3.20/day
73
+ runway 9.6 days (Jun 22) ✓ lasts the period
74
+ projected $89.40 by reset
75
+
76
+ top projects
77
+ memcon $22.10
78
+ burndown $11.40
79
+ barq-firmware $8.30
80
+
81
+ last 14d ▂▁▃▅▂▇█▄▃▂▅▆▃▄
82
+
83
+ 1,204 billable msgs · 38,902,114 tokens · 100% local, nothing sent anywhere
84
+ ```
85
+
86
+ ## Commands
87
+
88
+ | Command | What it does |
89
+ |---|---|
90
+ | `burndown` / `burndown status` | one-shot snapshot |
91
+ | `burndown watch [--interval 5]` | live dashboard, re-reads logs every few seconds |
92
+ | `burndown budget <amount> [--tokens] [--reset-day N]` | set the budget runway is measured against (dollars, or `--tokens` to skip pricing) |
93
+ | `burndown scope programmatic` | **guardian mode** — meter just the June-2026 credit pool (programmatic usage); `all` / `interactive` also available |
94
+ | `burndown currency INR [--rate R]` | show a second currency next to USD (static rate, no live fetch) |
95
+ | `burndown check` | exit `0` ok · `1` projected-over · `2` over — wire it into your own pre-run hook / CI |
96
+ | `burndown report [--html out.html]` | self-contained local HTML report (opens from `file://`, no server) |
97
+ | `burndown serve [--port 8787]` | live **web dashboard** on `127.0.0.1` — local only, auto-refreshing (nice if you don't live in a terminal) |
98
+ | `burndown config` | show config + verify which logs are being read |
99
+
100
+ ## How it works
101
+
102
+ Claude Code logs every assistant message to `~/.claude/projects/**/*.jsonl` with
103
+ a `usage` block (input / output / cache-write / cache-read tokens). Burndown
104
+ reads those numbers (only those — never the message text), prices them with a
105
+ configurable per-model table, rolls them into your current billing period,
106
+ computes burn rate from the **last 24 hours**, and projects when you'll hit your
107
+ budget.
108
+
109
+ ## Honest limitations
110
+
111
+ - **Pricing is estimated and configurable.** Default per-model rates are
112
+ best-effort; correct them in `~/.config/burndown/config.toml` (`burndown
113
+ config` shows the active table). The burn-rate/runway math is exact regardless
114
+ — prices only scale the dollar figure. Prefer not to trust a dollar estimate?
115
+ `burndown budget <N> --tokens` forecasts in raw tokens.
116
+ - It reads **local Claude Code logs**. If your usage doesn't write those logs
117
+ (e.g. a flow that doesn't log locally), Burndown can't see it yet.
118
+ - The "budget stop" is a **check you act on**, not an auto-kill (on purpose — see
119
+ [ADR-005](docs/DECISIONS.md)).
120
+
121
+ ## Trust
122
+
123
+ Zero dependencies · no outbound network (the optional dashboard is loopback-only) · read-only · content-blind · cross-platform (macOS/Linux/Windows) · MIT.
124
+ Verify it yourself in one line:
125
+ ```bash
126
+ grep -REn "urllib|httpx|requests|telemetry|analytics|0\.0\.0\.0|socket\.socket|create_connection|\.connect\(" burndown/ # → nothing (the only socket binds 127.0.0.1)
127
+ ```
128
+
129
+ Decisions are logged in [docs/DECISIONS.md](docs/DECISIONS.md); the threat model
130
+ is in [docs/SECURITY.md](docs/SECURITY.md).
@@ -0,0 +1,103 @@
1
+ <div align="center">
2
+
3
+ # Burndown
4
+
5
+ **A local, real-time cockpit for your Claude credit burn.**
6
+ Burn rate · runway forecast · budget alerts — so you see "I'll run dry in 3 days" *before* it happens, not after.
7
+
8
+ [![License: MIT](https://img.shields.io/badge/License-MIT-000.svg)](LICENSE)
9
+ [![Python 3.11+](https://img.shields.io/badge/Python-3.11+-000.svg)](https://python.org)
10
+ [![Zero dependencies](https://img.shields.io/badge/dependencies-0-000.svg)]()
11
+ [![100%25 local](https://img.shields.io/badge/runs-100%25%20local-000.svg)]()
12
+
13
+ </div>
14
+
15
+ ---
16
+
17
+ Anthropic split programmatic Claude usage into a **separate monthly credit pool**
18
+ that can run out mid-month. The existing tools show you what you *already* spent —
19
+ a bank statement, after the fact. Burndown is the **fuel gauge**: how fast you're
20
+ burning right now, when you'll hit zero, and a check that fires before you overspend.
21
+
22
+ It reads the usage logs Claude Code already writes on your machine. **Nothing
23
+ leaves your computer** — zero dependencies, no network, read-only on your logs,
24
+ and it never touches your prompts or code (see [SECURITY.md](docs/SECURITY.md)).
25
+
26
+ ## Quickstart
27
+
28
+ ```bash
29
+ pipx install git+https://github.com/aryasgit/burndown.git # works today (PyPI: `pipx install burndown` — soon)
30
+ burndown # snapshot of this period
31
+ burndown budget 100 # set your monthly credit-pool budget → get a runway
32
+ burndown scope programmatic # guardian mode: meter just the credit pool
33
+ burndown currency INR # show INR next to USD (static rate, no live fetch)
34
+ burndown watch # live terminal dashboard …
35
+ burndown serve # … or a web dashboard at http://127.0.0.1:8787
36
+ ```
37
+
38
+ No install? From a clone: `python -m burndown` (needs Python 3.11+, nothing else).
39
+
40
+ ```
41
+ BURNDOWN monthly period · resets Jul 01
42
+
43
+ $41.80 / $100.00 ███████████░░░░░░░░░░░░░░░░░ 42%
44
+
45
+ burn rate $6.10/day (last 24h) avg $3.20/day
46
+ runway 9.6 days (Jun 22) ✓ lasts the period
47
+ projected $89.40 by reset
48
+
49
+ top projects
50
+ memcon $22.10
51
+ burndown $11.40
52
+ barq-firmware $8.30
53
+
54
+ last 14d ▂▁▃▅▂▇█▄▃▂▅▆▃▄
55
+
56
+ 1,204 billable msgs · 38,902,114 tokens · 100% local, nothing sent anywhere
57
+ ```
58
+
59
+ ## Commands
60
+
61
+ | Command | What it does |
62
+ |---|---|
63
+ | `burndown` / `burndown status` | one-shot snapshot |
64
+ | `burndown watch [--interval 5]` | live dashboard, re-reads logs every few seconds |
65
+ | `burndown budget <amount> [--tokens] [--reset-day N]` | set the budget runway is measured against (dollars, or `--tokens` to skip pricing) |
66
+ | `burndown scope programmatic` | **guardian mode** — meter just the June-2026 credit pool (programmatic usage); `all` / `interactive` also available |
67
+ | `burndown currency INR [--rate R]` | show a second currency next to USD (static rate, no live fetch) |
68
+ | `burndown check` | exit `0` ok · `1` projected-over · `2` over — wire it into your own pre-run hook / CI |
69
+ | `burndown report [--html out.html]` | self-contained local HTML report (opens from `file://`, no server) |
70
+ | `burndown serve [--port 8787]` | live **web dashboard** on `127.0.0.1` — local only, auto-refreshing (nice if you don't live in a terminal) |
71
+ | `burndown config` | show config + verify which logs are being read |
72
+
73
+ ## How it works
74
+
75
+ Claude Code logs every assistant message to `~/.claude/projects/**/*.jsonl` with
76
+ a `usage` block (input / output / cache-write / cache-read tokens). Burndown
77
+ reads those numbers (only those — never the message text), prices them with a
78
+ configurable per-model table, rolls them into your current billing period,
79
+ computes burn rate from the **last 24 hours**, and projects when you'll hit your
80
+ budget.
81
+
82
+ ## Honest limitations
83
+
84
+ - **Pricing is estimated and configurable.** Default per-model rates are
85
+ best-effort; correct them in `~/.config/burndown/config.toml` (`burndown
86
+ config` shows the active table). The burn-rate/runway math is exact regardless
87
+ — prices only scale the dollar figure. Prefer not to trust a dollar estimate?
88
+ `burndown budget <N> --tokens` forecasts in raw tokens.
89
+ - It reads **local Claude Code logs**. If your usage doesn't write those logs
90
+ (e.g. a flow that doesn't log locally), Burndown can't see it yet.
91
+ - The "budget stop" is a **check you act on**, not an auto-kill (on purpose — see
92
+ [ADR-005](docs/DECISIONS.md)).
93
+
94
+ ## Trust
95
+
96
+ Zero dependencies · no outbound network (the optional dashboard is loopback-only) · read-only · content-blind · cross-platform (macOS/Linux/Windows) · MIT.
97
+ Verify it yourself in one line:
98
+ ```bash
99
+ grep -REn "urllib|httpx|requests|telemetry|analytics|0\.0\.0\.0|socket\.socket|create_connection|\.connect\(" burndown/ # → nothing (the only socket binds 127.0.0.1)
100
+ ```
101
+
102
+ Decisions are logged in [docs/DECISIONS.md](docs/DECISIONS.md); the threat model
103
+ is in [docs/SECURITY.md](docs/SECURITY.md).
@@ -0,0 +1,6 @@
1
+ """Burndown — a local, real-time cockpit for your Claude credit burn.
2
+
3
+ Zero runtime dependencies. 100% local. Read-only on your logs. Content-blind.
4
+ Nothing this package does opens a network connection.
5
+ """
6
+ __version__ = "0.1.0"
@@ -0,0 +1,4 @@
1
+ from .cli import main
2
+
3
+ if __name__ == "__main__":
4
+ main()
@@ -0,0 +1,92 @@
1
+ """
2
+ burndown/aggregate.py — roll a stream of Events into a period Snapshot.
3
+ Pure functions, no I/O — unit-tested offline.
4
+ """
5
+ from __future__ import annotations
6
+
7
+ from dataclasses import dataclass, field
8
+ from datetime import datetime, timedelta, timezone
9
+
10
+ from .pricing import cost_usd
11
+
12
+
13
+ def period_window(period: str, reset_day: int, now: datetime) -> tuple[datetime, datetime]:
14
+ """[start, end) of the billing window `now` falls in."""
15
+ now = now.astimezone(timezone.utc)
16
+ if period == "daily":
17
+ start = now.replace(hour=0, minute=0, second=0, microsecond=0)
18
+ return start, start + timedelta(days=1)
19
+ if period == "weekly":
20
+ start = (now - timedelta(days=now.weekday())).replace(
21
+ hour=0, minute=0, second=0, microsecond=0)
22
+ return start, start + timedelta(weeks=1)
23
+ # monthly, anchored on reset_day
24
+ rd = max(1, min(28, reset_day))
25
+ midnight = now.replace(hour=0, minute=0, second=0, microsecond=0)
26
+ if now.day >= rd:
27
+ start = midnight.replace(day=rd)
28
+ else:
29
+ prev_last = midnight.replace(day=1) - timedelta(days=1)
30
+ start = prev_last.replace(day=rd)
31
+ end = start.replace(year=start.year + 1, month=1) if start.month == 12 \
32
+ else start.replace(month=start.month + 1)
33
+ return start, end
34
+
35
+
36
+ @dataclass
37
+ class Snapshot:
38
+ now: datetime
39
+ period: str
40
+ period_start: datetime
41
+ period_end: datetime
42
+ spent_usd: float = 0.0
43
+ tokens: int = 0
44
+ events: int = 0
45
+ by_project: dict = field(default_factory=dict) # name -> usd
46
+ by_model: dict = field(default_factory=dict) # model -> usd
47
+ by_day: dict = field(default_factory=dict) # 'YYYY-MM-DD' -> usd
48
+ cost_last_24h: float = 0.0
49
+ cost_last_7d: float = 0.0
50
+ spent_programmatic: float = 0.0 # period $ split — always tracked, for the breakdown line
51
+ spent_interactive: float = 0.0
52
+
53
+
54
+ def build_snapshot(events, cfg, now: datetime | None = None, scope: str = "all") -> Snapshot:
55
+ """Roll events into a period snapshot.
56
+
57
+ `scope` filters what the headline (spent / burn / runway) is about:
58
+ 'all' — every event; 'programmatic' — credit-pool usage only;
59
+ 'interactive' — app usage only. The programmatic/interactive period split is
60
+ ALWAYS tracked (for the breakdown line) regardless of scope.
61
+ """
62
+ now = (now or datetime.now(timezone.utc)).astimezone(timezone.utc)
63
+ start, end = period_window(cfg.period, cfg.reset_day, now)
64
+ snap = Snapshot(now=now, period=cfg.period, period_start=start, period_end=end)
65
+ t24, t7 = now - timedelta(hours=24), now - timedelta(days=7)
66
+
67
+ def in_scope(e) -> bool:
68
+ return scope == "all" or e.programmatic == (scope == "programmatic")
69
+
70
+ for e in events:
71
+ c = cost_usd(e.model, e.input, e.output, e.cache_write, e.cache_read, cfg.pricing)
72
+ if in_scope(e):
73
+ if t24 <= e.ts <= now:
74
+ snap.cost_last_24h += c
75
+ if t7 <= e.ts <= now:
76
+ snap.cost_last_7d += c
77
+ if not (start <= e.ts < end):
78
+ continue
79
+ if e.programmatic: # period split — always, for the breakdown
80
+ snap.spent_programmatic += c
81
+ else:
82
+ snap.spent_interactive += c
83
+ if not in_scope(e): # headline rollup respects the scope
84
+ continue
85
+ snap.spent_usd += c
86
+ snap.tokens += e.total_tokens
87
+ snap.events += 1
88
+ snap.by_project[e.project] = snap.by_project.get(e.project, 0.0) + c
89
+ snap.by_model[e.model] = snap.by_model.get(e.model, 0.0) + c
90
+ d = e.ts.date().isoformat()
91
+ snap.by_day[d] = snap.by_day.get(d, 0.0) + c
92
+ return snap
@@ -0,0 +1,184 @@
1
+ """
2
+ burndown/cli.py — command-line entry. Subcommands:
3
+
4
+ status one-shot snapshot (default)
5
+ watch live terminal dashboard (re-reads logs every few seconds)
6
+ budget set/show the budget the forecast is measured against
7
+ report write a self-contained local HTML report
8
+ check exit non-zero if over / projected-over budget (for your own hooks)
9
+ config show config + verify which logs are being read
10
+ """
11
+ from __future__ import annotations
12
+
13
+ import argparse
14
+ import os
15
+ import sys
16
+ import time
17
+
18
+
19
+ def _enable_windows_ansi() -> None:
20
+ """Turn on ANSI/VT processing on Windows 10+ so colors render in the terminal."""
21
+ if os.name != "nt":
22
+ return
23
+ try:
24
+ import ctypes
25
+ k = ctypes.windll.kernel32
26
+ k.SetConsoleMode(k.GetStdHandle(-11), 7) # ENABLE_VIRTUAL_TERMINAL_PROCESSING
27
+ except Exception:
28
+ pass
29
+
30
+ from . import config as cfgmod
31
+ from . import report
32
+ from .aggregate import build_snapshot
33
+ from .forecast import build_forecast
34
+ from .logs import find_log_files, iter_events
35
+ from .money import KNOWN_FX, money
36
+
37
+
38
+ def _snapshot(cfg):
39
+ snap = build_snapshot(iter_events(cfg.log_dirs), cfg, scope=cfg.scope)
40
+ return snap, build_forecast(snap, cfg)
41
+
42
+
43
+ def cmd_status(cfg, args):
44
+ snap, fc = _snapshot(cfg)
45
+ print(report.render_status(snap, fc, cfg))
46
+
47
+
48
+ def cmd_watch(cfg, args):
49
+ interval = max(2, getattr(args, "interval", 5))
50
+ try:
51
+ while True:
52
+ cfg = cfgmod.load()
53
+ snap, fc = _snapshot(cfg)
54
+ sys.stdout.write("\033[2J\033[H") # clear + home
55
+ print(report.render_status(snap, fc, cfg))
56
+ print(report.c(f"\n refreshing every {interval}s · ctrl-c to quit", "gray"))
57
+ time.sleep(interval)
58
+ except KeyboardInterrupt:
59
+ print()
60
+
61
+
62
+ def cmd_budget(cfg, args):
63
+ if args.amount is None:
64
+ val = "not set" if cfg.budget is None else money(cfg.budget, cfg)
65
+ print(f"budget: {val} per {cfg.period} (resets day {cfg.reset_day})")
66
+ return
67
+ cfg.budget = float(args.amount)
68
+ cfg.budget_unit = "tokens" if args.tokens else "usd"
69
+ if args.period:
70
+ cfg.period = args.period
71
+ if args.reset_day:
72
+ cfg.reset_day = max(1, min(28, args.reset_day))
73
+ path = cfgmod.save(cfg)
74
+ print(f"saved → {path}\n")
75
+ cmd_status(cfgmod.load(), args)
76
+
77
+
78
+ def cmd_currency(cfg, args):
79
+ if not args.code:
80
+ sec = f"{cfg.currency2} @ {cfg.fx_rate}" if cfg.currency2 else "USD only"
81
+ print(f"secondary currency: {sec}")
82
+ print(f"known codes: {', '.join(KNOWN_FX)}")
83
+ return
84
+ code = args.code.upper()
85
+ sym, default_rate = KNOWN_FX.get(code, (code + " ", 0.0))
86
+ cfg.currency2 = code
87
+ cfg.currency2_symbol = args.symbol or sym
88
+ cfg.fx_rate = args.rate if args.rate else default_rate
89
+ if not cfg.fx_rate:
90
+ print(f"unknown code {code} — pass a rate: `burndown currency {code} --rate <USD->{code}>`")
91
+ return
92
+ path = cfgmod.save(cfg)
93
+ print(f"saved → {path} (showing USD + {code} @ {cfg.fx_rate}; static rate, no live fetch)\n")
94
+ cmd_status(cfgmod.load(), args)
95
+
96
+
97
+ def cmd_scope(cfg, args):
98
+ if not args.value:
99
+ print(f"scope: {cfg.scope} (all | programmatic = credit pool | interactive)")
100
+ return
101
+ cfg.scope = args.value
102
+ path = cfgmod.save(cfg)
103
+ note = " — metering your credit-pool usage" if args.value == "programmatic" else ""
104
+ print(f"saved → {path}{note}\n")
105
+ cmd_status(cfgmod.load(), args)
106
+
107
+
108
+ def cmd_config(cfg, args):
109
+ files = find_log_files(cfg.log_dirs)
110
+ pricing = "custom (config)" if cfg.pricing else "defaults — estimated, override in config"
111
+ print(f"config file : {cfgmod.config_path()}")
112
+ print(f"log dirs : {', '.join(cfg.log_dirs)}")
113
+ print(f"log files : {len(files)} *.jsonl found")
114
+ print(f"budget : {cfg.budget if cfg.budget is not None else 'not set'} {cfg.budget_unit}")
115
+ print(f"period : {cfg.period} (reset day {cfg.reset_day})")
116
+ print(f"pricing : {pricing}")
117
+ print("network : none — burndown never opens a connection")
118
+
119
+
120
+ def cmd_report(cfg, args):
121
+ snap, fc = _snapshot(cfg)
122
+ out = args.html or "burndown-report.html"
123
+ with open(out, "w") as f:
124
+ f.write(report.render_html(snap, fc, cfg))
125
+ print(f"wrote {out}")
126
+
127
+
128
+ def cmd_serve(cfg, args):
129
+ from . import serve as serve_mod
130
+ serve_mod.serve(port=args.port, open_browser=not args.no_open)
131
+
132
+
133
+ def cmd_check(cfg, args):
134
+ snap, fc = _snapshot(cfg)
135
+ if fc.budget is None:
136
+ print("no budget set — nothing to check")
137
+ sys.exit(0)
138
+ if fc.spent >= fc.budget:
139
+ print(f"OVER budget: {money(fc.spent, cfg)} of {money(fc.budget, cfg)}")
140
+ sys.exit(2)
141
+ if fc.will_exceed:
142
+ print(f"projected to exceed before reset ({money(fc.projected_period_total, cfg)})")
143
+ sys.exit(1)
144
+ print(f"within budget ({fc.pct_used:.0f}% used)")
145
+ sys.exit(0)
146
+
147
+
148
+ def main(argv=None):
149
+ _enable_windows_ansi()
150
+ p = argparse.ArgumentParser(
151
+ prog="burndown",
152
+ description="A local, real-time cockpit for your Claude credit burn. "
153
+ "Zero dependencies, 100%% local, nothing leaves your machine.")
154
+ sub = p.add_subparsers(dest="cmd")
155
+ sub.add_parser("status", help="one-shot snapshot (default)")
156
+ w = sub.add_parser("watch", help="live dashboard")
157
+ w.add_argument("--interval", type=int, default=5)
158
+ b = sub.add_parser("budget", help="set/show your budget")
159
+ b.add_argument("amount", nargs="?", type=float)
160
+ b.add_argument("--tokens", action="store_true", help="budget in tokens, not dollars")
161
+ b.add_argument("--period", choices=["monthly", "weekly", "daily"])
162
+ b.add_argument("--reset-day", dest="reset_day", type=int, help="day-of-month the pool resets")
163
+ sub.add_parser("config", help="show config + which logs are read")
164
+ sc = sub.add_parser("scope", help="meter all usage, or just the credit pool")
165
+ sc.add_argument("value", nargs="?", choices=["all", "programmatic", "interactive"])
166
+ cu = sub.add_parser("currency", help="show USD + a second currency (e.g. INR)")
167
+ cu.add_argument("code", nargs="?", help="currency code, e.g. INR")
168
+ cu.add_argument("--rate", type=float, help="USD -> code conversion (static, no live fetch)")
169
+ cu.add_argument("--symbol", help="currency symbol, e.g. ₹")
170
+ r = sub.add_parser("report", help="write a self-contained HTML report")
171
+ r.add_argument("--html", help="output path (default burndown-report.html)")
172
+ sv = sub.add_parser("serve", help="open a local web dashboard (127.0.0.1 only)")
173
+ sv.add_argument("--port", type=int, default=8787)
174
+ sv.add_argument("--no-open", action="store_true", help="don't auto-open the browser")
175
+ sub.add_parser("check", help="exit non-zero if over/projected-over budget")
176
+
177
+ args = p.parse_args(argv)
178
+ cfg = cfgmod.load()
179
+ dispatch = {
180
+ "status": cmd_status, "watch": cmd_watch, "budget": cmd_budget,
181
+ "config": cmd_config, "scope": cmd_scope, "currency": cmd_currency,
182
+ "report": cmd_report, "serve": cmd_serve, "check": cmd_check,
183
+ }
184
+ dispatch[args.cmd or "status"](cfg, args)