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.
- burnclock-0.1.0.dist-info/METADATA +134 -0
- burnclock-0.1.0.dist-info/RECORD +24 -0
- burnclock-0.1.0.dist-info/WHEEL +5 -0
- burnclock-0.1.0.dist-info/entry_points.txt +2 -0
- burnclock-0.1.0.dist-info/top_level.txt +2 -0
- burnclock_cli/__init__.py +1 -0
- burnclock_cli/completions.py +159 -0
- burnclock_cli/main.py +1408 -0
- burnclock_core/__init__.py +8 -0
- burnclock_core/burn.py +333 -0
- burnclock_core/config.py +603 -0
- burnclock_core/envelope.py +311 -0
- burnclock_core/forecast.py +144 -0
- burnclock_core/pricing.py +450 -0
- burnclock_core/providers/__init__.py +1 -0
- burnclock_core/providers/deepseek.py +374 -0
- burnclock_core/providers/fal.py +499 -0
- burnclock_core/providers/minimax.py +484 -0
- burnclock_core/providers/moonshot.py +308 -0
- burnclock_core/providers/openrouter.py +241 -0
- burnclock_core/providers/xai.py +256 -0
- burnclock_core/schema.py +150 -0
- burnclock_core/upgrade.py +239 -0
- burnclock_core/usage.py +250 -0
|
@@ -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 @@
|
|
|
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)
|