burnclock 0.1.0__py3-none-any.whl

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,134 @@
1
+ Metadata-Version: 2.4
2
+ Name: burnclock
3
+ Version: 0.1.0
4
+ Summary: Burnclock: see your AI budget runway at a glance — balances, burn, days-left. Estimates, not billing truth.
5
+ Author: Jeremy Silbernagel
6
+ License: MIT
7
+ Requires-Python: >=3.12
8
+ Description-Content-Type: text/markdown
9
+ Requires-Dist: typer>=0.12
10
+ Requires-Dist: rich>=13
11
+ Requires-Dist: httpx>=0.27
12
+ Requires-Dist: tomli-w>=1.0
13
+ Provides-Extra: dev
14
+ Requires-Dist: pytest>=8; extra == "dev"
15
+ Provides-Extra: tui
16
+ Requires-Dist: textual>=0.80; extra == "tui"
17
+
18
+ # Burnclock
19
+
20
+ **How much AI budget you have left, and how long it lasts — in one glance.**
21
+
22
+ Burnclock reads each AI provider's own balance/usage endpoint (or your local Hermes usage records),
23
+ then computes what most consoles don't show you: **burn per day, days of paid runway left, and the date
24
+ your monthly budget runs out** — per provider, in one table or one JSON envelope.
25
+
26
+ > **Estimates, not billing truth.** Every dollar is computed locally from provider responses and usage
27
+ > records. It is not a bill, not a guarantee, and not affiliated with any provider.
28
+
29
+ Example output (synthesized, not a live account):
30
+
31
+ ```text
32
+ ┌────────────┬───────┬───────────┬──────────┬───────────────┐
33
+ │ Provider │ State │ Balance │ Burn/day │ Runway (paid) │
34
+ ├────────────┼───────┼───────────┼──────────┼───────────────┤
35
+ │ DeepSeek │ OK │ 20.23 USD │ 2.69 │ 6.0 d │
36
+ │ OpenRouter │ EMPTY │ -0.09 USD │ 8.08 │ — │
37
+ └────────────┴───────┴───────────┴──────────┴───────────────┘
38
+ ```
39
+
40
+ ## What it answers
41
+
42
+ - **Do I need to top up today?** Balance + days-left at your current burn, per provider.
43
+ - **Where is my money going?** Burn/day from your actual usage (Hermes `state.db`, deduped logs), not a guess.
44
+ - **When does the month's budget run out?** Budget bars and a projected crossing date.
45
+ - **Is this key dead or just empty?** `KEY_REJECTED` vs `EMPTY` vs `PERMISSION_DENIED` are distinct states.
46
+
47
+ ## Providers
48
+
49
+ | Provider | Balance source | Verification |
50
+ |---|---|---|
51
+ | DeepSeek | `GET /user/balance` | live-verified |
52
+ | OpenRouter | `GET /api/v1/key` + `/credits` (remaining = credits − usage) | live-verified |
53
+ | Moonshot | `GET /v1/users/me/balance` | doc-unverified, disabled by default |
54
+ | xAI | management API prepaid balance (needs `team_id`) | doc-unverified, disabled by default |
55
+ | MiniMax | `account/query_balance` or `token_plan/remains` (region auto) | doc-unverified, disabled by default |
56
+ | fal | `account/billing` (prepaid/postpaid discriminated) | doc-unverified, disabled by default |
57
+
58
+ Providers without a balance API still get an honest row: `NO_API` (spend-only, e.g. priced usage records)
59
+ or `NOT_QUERYABLE` (console-only). No state ever fabricates a number.
60
+
61
+ ## Install
62
+
63
+ Not on PyPI yet — the first release is pending. From source:
64
+
65
+ ```sh
66
+ git clone <repo-url> && cd burnclock
67
+ python3 -m venv .venv && .venv/bin/pip install -e '.[dev]'
68
+ .venv/bin/burnclock --version
69
+ ```
70
+
71
+ Once released: `pipx install burnclock` or `uv tool install burnclock`.
72
+
73
+ Requires Python ≥ 3.12. The `watch` TUI needs the optional extra: `pip install 'burnclock[tui]'`.
74
+
75
+ ## Use
76
+
77
+ ```sh
78
+ burnclock # human status table
79
+ burnclock status --json # envelope v1 + script-safe exit codes
80
+ burnclock forecast # paid runway, burn/day, budget crossing date
81
+ burnclock dashboard # every known provider (disabled rows included)
82
+ burnclock budget show # resolved monthly budgets
83
+ burnclock budget set deepseek 50 --dry-run # offline preview, never a surprise write
84
+ burnclock watch # live TUI on a TTY; plain table when piped
85
+ burnclock completion bash|zsh|fish # print a static shell completion script
86
+ burnclock config show | path # effective config / config file location
87
+ ```
88
+
89
+ Config lives at `~/.config/burnclock/config.toml` on macOS/Linux and `%APPDATA%\burnclock\config.toml` on
90
+ 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
91
+ variable (e.g. `key_env = "DEEPSEEK_API_KEY"`); a secret-shaped value in config refuses to load.
92
+
93
+ Exit codes on machine paths: `0` ok · `1` internal · `2` usage · `3` config · `4` auth
94
+ (`KEY_REJECTED`/`PERMISSION_DENIED`) · `5` threshold crossed · `6` unreachable — worst wins, in the order
95
+ `1 > 2 > 3 > 4 > 5 > 6 > 0`. Human views exit 0 whenever output was produced.
96
+
97
+ `status --min <usd>` and `status --min-days <n>` turn it into an alert (exit 5) for cron and agents.
98
+
99
+ On an interactive terminal only (never for `--json` or piped output), burnclock checks PyPI at most once a
100
+ day and prints a one-line upgrade hint to stderr; set `BURNCLOCK_NO_UPGRADE_CHECK=1` to opt out.
101
+
102
+ ## States
103
+
104
+ `OK · LOW · CRITICAL · EMPTY · KEY_REJECTED · PERMISSION_DENIED · NO_API · NOT_QUERYABLE · ERROR · UNREACHABLE`
105
+
106
+ `KEY_REJECTED` distinguishes a dead/rotated key from a `$0` balance; `PERMISSION_DENIED` means the key is
107
+ valid but under-scoped. `EMPTY` means unknown or zero — never "free".
108
+
109
+ ## Hermes skill
110
+
111
+ The repo ships a Hermes skill (`hermes-skill/`, packaged as `dist/burnclock-skill.zip`) so an agent session
112
+ can answer "how many tokens do I have left" with real numbers. Its shim reads only the six provider key
113
+ variable **names** from `~/.hermes/.env` — it never sources the file and never prints a value. A daily cron
114
+ suggestion for a runway alert lives in `hermes-skill/CRON.md`.
115
+
116
+ ## Design rules
117
+
118
+ - **Read-only.** No top-ups, no spending, no provider mutations. Spending endpoints are never called.
119
+ - **Fail-closed.** Missing keys are reported as such; malformed responses are `ERROR`, never `$0`.
120
+ - **No secrets in files.** Config holds env-var names only; the packaged skill is secret-scanned.
121
+ - **Offline floor.** With zero keys it still reports what your local Hermes usage records know.
122
+ - **Estimates, not billing truth.** Runway is a range: bursty usage moves it, and providers change prices.
123
+
124
+ ## Development
125
+
126
+ ```sh
127
+ .venv/bin/pytest -q # full suite (synthetic fixtures only, no network)
128
+ bash hermes-skill/scripts/build-skill-zip.sh # package + secret-scan the skill
129
+ ```
130
+
131
+ Tests never touch a real provider: transports are faked and every fixture is synthetic. Timing protocol and
132
+ recorded numbers live in [BENCH.md](BENCH.md); release history in [CHANGELOG.md](CHANGELOG.md).
133
+
134
+ MIT licensed. Built by Jeremy Silbernagel.
@@ -0,0 +1,24 @@
1
+ burnclock_cli/__init__.py,sha256=AaQXy1QzKy2Bn2Kvf4N17yP6UthNUduRSiIZ0leueqE,83
2
+ burnclock_cli/completions.py,sha256=K-euw8a07QFDHfh-vuGraWuzs6_dha7jSh9t_p7IZo8,6117
3
+ burnclock_cli/main.py,sha256=OGHPlIi4Y7VzkeB1yZQQt-MtOifS-YizdUkoqGWZCVM,57637
4
+ burnclock_core/__init__.py,sha256=13T6e2YdeGK08krQDbJ3ygwUz3gTG4pwhjHJJB5bNoo,192
5
+ burnclock_core/burn.py,sha256=yQVTgdyyUqPkB4Si6qZcvXTWcs0KgXUYSrrg5fcQVR0,13109
6
+ burnclock_core/config.py,sha256=ZGTdF2opa3zTSnZUOlLDr1OfHFMoBfViMzyDmQ_pTxY,24279
7
+ burnclock_core/envelope.py,sha256=yyTW0YqpC9UxreQ3HMJ8r7cJbj27fe8b7oJLkYWIXvI,12207
8
+ burnclock_core/forecast.py,sha256=ttpbkxe7mJzdNwVgK14drPniGPesHdojxPXR8bbTV1A,5437
9
+ burnclock_core/pricing.py,sha256=7ku3rRZdVY7Hcf4bXN04ctd_ImkeRMXflLmFMIB85bs,17936
10
+ burnclock_core/schema.py,sha256=zVuxfQwWoBAFPOeptFnm5y0TnjP5phXhM-heeoKF1-A,4764
11
+ burnclock_core/upgrade.py,sha256=DrMM6I472AJxzLgRQOzrGc5vVOpEp9i5R57Fqr0BSN0,8078
12
+ burnclock_core/usage.py,sha256=pYuTvk6y1qX_Pqi7UVWwDCGzLsu3C3wMceNAhTW7CpM,9484
13
+ burnclock_core/providers/__init__.py,sha256=RYx9-aeu1pX8K1_qza40IELbdD0p_zDSy1aUGJe2LUE,80
14
+ burnclock_core/providers/deepseek.py,sha256=ZRy1PuVzZUJn4DBx1C-428bz-DA25xo7hv1z2k0ORpw,14499
15
+ burnclock_core/providers/fal.py,sha256=rj5yveD5X6DbrzBz7keo-sdg5SRAO_iDpSw4IezJZK8,19580
16
+ burnclock_core/providers/minimax.py,sha256=OUURVNM7sOUNwiqq-zA6V2LnoyTTgi5kQlrQKhAuTpc,19392
17
+ burnclock_core/providers/moonshot.py,sha256=XK4srkVxMh_muqRJU6BaSHOFrseBFydVht8hKckCHSg,12294
18
+ burnclock_core/providers/openrouter.py,sha256=fDqXF7YbI1RWC037D3OW4ld01LV0cmVvqUJVdP_3eF4,9399
19
+ burnclock_core/providers/xai.py,sha256=gbbEeMgOLqdUVQ0869YfawtlqISm1W34zhyKZMJjKLU,10252
20
+ burnclock-0.1.0.dist-info/METADATA,sha256=ENY3R6ZhRpa2QRrjI8aS18YmvByo430mtTKn3XyifBg,6833
21
+ burnclock-0.1.0.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
22
+ burnclock-0.1.0.dist-info/entry_points.txt,sha256=Pu3zaqBJMVN1VbaG0mY7PetAHB1jE_ufGKmB4Rt0G0c,53
23
+ burnclock-0.1.0.dist-info/top_level.txt,sha256=s8KB0J8w7pA1EVYltmg1HZNn8T7Pwxs0tlY-QjGj1bE,29
24
+ burnclock-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (84.0.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ burnclock = burnclock_cli.main:run
@@ -0,0 +1,2 @@
1
+ burnclock_cli
2
+ burnclock_core
@@ -0,0 +1 @@
1
+ """burnclock_cli — thin Typer + Rich CLI over burnclock_core. No TUI imports."""
@@ -0,0 +1,159 @@
1
+ """Static shell-completion scripts for `burnclock completion <shell>` (B5-B1).
2
+
3
+ Pure stdlib strings: importing this module must never pull in typer, rich, or
4
+ httpx — the completion path stays as light as the scripts it prints. Nothing
5
+ here is generated at runtime from the Typer app; the command/option lists are
6
+ deliberate, reviewed text so a completion can never leak ANSI styling or a
7
+ startup side effect.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ SUPPORTED_SHELLS: tuple[str, ...] = ("bash", "zsh", "fish")
13
+
14
+ # The full command surface the scripts complete (B5-B1 brief §1).
15
+ SUBCOMMANDS: tuple[str, ...] = (
16
+ "status",
17
+ "forecast",
18
+ "dashboard",
19
+ "watch",
20
+ "version",
21
+ "config",
22
+ "budget",
23
+ "completion",
24
+ )
25
+
26
+ # Long options shared by the commands (help + machine/human switches + tuning).
27
+ LONG_OPTIONS: tuple[str, ...] = (
28
+ "--json",
29
+ "--min",
30
+ "--min-days",
31
+ "--interval",
32
+ "--config",
33
+ "--help",
34
+ )
35
+
36
+ _BASH_SCRIPT = """\
37
+ # burnclock bash completion (static; generated by `burnclock completion bash`)
38
+ _burnclock_completion() {
39
+ local cur prev
40
+ cur="${COMP_WORDS[COMP_CWORD]}"
41
+ prev="${COMP_WORDS[COMP_CWORD-1]}"
42
+ local commands="status forecast dashboard watch version config budget completion"
43
+ local options="--json --min --min-days --interval --config --help"
44
+
45
+ if [[ "${cur}" == -* ]]; then
46
+ COMPREPLY=( $(compgen -W "${options}" -- "${cur}") )
47
+ return 0
48
+ fi
49
+
50
+ case "${prev}" in
51
+ completion)
52
+ COMPREPLY=( $(compgen -W "bash zsh fish" -- "${cur}") )
53
+ return 0
54
+ ;;
55
+ config)
56
+ COMPREPLY=( $(compgen -W "show path" -- "${cur}") )
57
+ return 0
58
+ ;;
59
+ budget)
60
+ COMPREPLY=( $(compgen -W "show get set unset" -- "${cur}") )
61
+ return 0
62
+ ;;
63
+ esac
64
+
65
+ COMPREPLY=( $(compgen -W "${commands} ${options}" -- "${cur}") )
66
+ return 0
67
+ }
68
+ complete -F _burnclock_completion burnclock
69
+ """
70
+
71
+ _ZSH_SCRIPT = """\
72
+ #compdef burnclock
73
+ # burnclock zsh completion (static; generated by `burnclock completion zsh`)
74
+
75
+ _burnclock() {
76
+ _arguments -C \\
77
+ '(-h --help)'{-h,--help}'[show help and exit]' \\
78
+ '--json[machine-readable envelope output]' \\
79
+ '--min[alert below this USD balance]:usd:' \\
80
+ '--min-days[alert below this many days of paid runway]:days:' \\
81
+ '--interval[watch refresh interval in seconds]:seconds:' \\
82
+ '--config[path to config.toml]:file:_files' \\
83
+ '1:command:->command' \\
84
+ '*::argument:->argument'
85
+
86
+ case "$state" in
87
+ command)
88
+ _values 'burnclock command' \\
89
+ 'status[provider status table or JSON envelope]' \\
90
+ 'forecast[paid runway, burn per day, budget crossing]' \\
91
+ 'dashboard[every known provider, disabled rows included]' \\
92
+ 'watch[live refresh loop (TUI on a terminal)]' \\
93
+ 'version[print the burnclock version]' \\
94
+ 'config[inspect configuration (show, path)]' \\
95
+ 'budget[view or set per-provider monthly budgets]' \\
96
+ 'completion[print a shell completion script]'
97
+ ;;
98
+ argument)
99
+ case "${words[CURRENT-1]}" in
100
+ completion)
101
+ _values 'shell' bash zsh fish
102
+ ;;
103
+ config)
104
+ _values 'config subcommand' show path
105
+ ;;
106
+ budget)
107
+ _values 'budget subcommand' show get set unset
108
+ ;;
109
+ *)
110
+ # compadd: the leading dashes would be parsed as _values
111
+ # options, and after `--` they are plain completions.
112
+ compadd -- --json --min --min-days --interval --config --help
113
+ ;;
114
+ esac
115
+ ;;
116
+ esac
117
+ }
118
+
119
+ _burnclock "$@"
120
+ """
121
+
122
+ _FISH_SCRIPT = """\
123
+ # burnclock fish completion (static; generated by `burnclock completion fish`)
124
+ complete -c burnclock -f
125
+ complete -c burnclock -n '__fish_use_subcommand' -a status -d 'provider status table or JSON envelope'
126
+ complete -c burnclock -n '__fish_use_subcommand' -a forecast -d 'paid runway, burn per day, budget crossing'
127
+ complete -c burnclock -n '__fish_use_subcommand' -a dashboard -d 'every known provider, disabled rows included'
128
+ complete -c burnclock -n '__fish_use_subcommand' -a watch -d 'live refresh loop (TUI on a terminal)'
129
+ complete -c burnclock -n '__fish_use_subcommand' -a version -d 'print the burnclock version'
130
+ complete -c burnclock -n '__fish_use_subcommand' -a config -d 'inspect configuration (show, path)'
131
+ complete -c burnclock -n '__fish_use_subcommand' -a budget -d 'view or set per-provider monthly budgets'
132
+ complete -c burnclock -n '__fish_use_subcommand' -a completion -d 'print a shell completion script'
133
+ complete -c burnclock -n '__fish_seen_subcommand_from completion' -a 'bash zsh fish' -d 'shell'
134
+ complete -c burnclock -n '__fish_seen_subcommand_from config' -a 'show path' -d 'config subcommand'
135
+ complete -c burnclock -n '__fish_seen_subcommand_from budget' -a 'show get set unset' -d 'budget subcommand'
136
+ complete -c burnclock -l json -d 'machine-readable envelope output'
137
+ complete -c burnclock -l min -r -d 'alert below this USD balance'
138
+ complete -c burnclock -l min-days -r -d 'alert below this many days of paid runway'
139
+ complete -c burnclock -l interval -r -d 'watch refresh interval in seconds'
140
+ complete -c burnclock -l config -r -d 'path to config.toml'
141
+ complete -c burnclock -l help -d 'show help and exit'
142
+ """
143
+
144
+ _SCRIPTS: dict[str, str] = {
145
+ "bash": _BASH_SCRIPT,
146
+ "zsh": _ZSH_SCRIPT,
147
+ "fish": _FISH_SCRIPT,
148
+ }
149
+
150
+
151
+ def completion_script(shell: str) -> str | None:
152
+ """Return the static completion script for `shell`, or None if unsupported.
153
+
154
+ Case/whitespace tolerant so `BASH` and ` bash ` both work; an unknown name
155
+ returns None and the CLI turns that into exit 2 (EXIT_USAGE). The returned
156
+ text is pure ASCII/UTF-8 plain text with no ANSI or Rich markup.
157
+ """
158
+ key = (shell or "").strip().lower()
159
+ return _SCRIPTS.get(key)