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.
- burndown-0.1.0/.github/workflows/ci.yml +27 -0
- burndown-0.1.0/.gitignore +21 -0
- burndown-0.1.0/CHANGELOG.md +25 -0
- burndown-0.1.0/LICENSE +21 -0
- burndown-0.1.0/PKG-INFO +130 -0
- burndown-0.1.0/README.md +103 -0
- burndown-0.1.0/burndown/__init__.py +6 -0
- burndown-0.1.0/burndown/__main__.py +4 -0
- burndown-0.1.0/burndown/aggregate.py +92 -0
- burndown-0.1.0/burndown/cli.py +184 -0
- burndown-0.1.0/burndown/config.py +117 -0
- burndown-0.1.0/burndown/forecast.py +68 -0
- burndown-0.1.0/burndown/logs.py +173 -0
- burndown-0.1.0/burndown/money.py +50 -0
- burndown-0.1.0/burndown/pricing.py +54 -0
- burndown-0.1.0/burndown/report.py +127 -0
- burndown-0.1.0/burndown/serve.py +149 -0
- burndown-0.1.0/docs/DECISIONS.md +134 -0
- burndown-0.1.0/docs/SECURITY.md +61 -0
- burndown-0.1.0/docs/index.html +1264 -0
- burndown-0.1.0/docs/vercel.json +7 -0
- burndown-0.1.0/pyproject.toml +43 -0
- burndown-0.1.0/tests/conftest.py +54 -0
- burndown-0.1.0/tests/test_aggregate.py +61 -0
- burndown-0.1.0/tests/test_forecast.py +40 -0
- burndown-0.1.0/tests/test_logs.py +36 -0
- burndown-0.1.0/tests/test_money.py +21 -0
- burndown-0.1.0/tests/test_pricing.py +23 -0
- burndown-0.1.0/tests/test_serve.py +18 -0
|
@@ -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.
|
burndown-0.1.0/PKG-INFO
ADDED
|
@@ -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)
|
|
36
|
+
[](https://python.org)
|
|
37
|
+
[]()
|
|
38
|
+
[]()
|
|
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).
|
burndown-0.1.0/README.md
ADDED
|
@@ -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)
|
|
9
|
+
[](https://python.org)
|
|
10
|
+
[]()
|
|
11
|
+
[]()
|
|
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,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)
|