tokenmaxxr 1.0.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.
- tokenmaxxr-1.0.0/.github/workflows/ci.yml +26 -0
- tokenmaxxr-1.0.0/.github/workflows/publish.yml +36 -0
- tokenmaxxr-1.0.0/.gitignore +29 -0
- tokenmaxxr-1.0.0/CHANGELOG.md +30 -0
- tokenmaxxr-1.0.0/LICENSE +21 -0
- tokenmaxxr-1.0.0/PKG-INFO +148 -0
- tokenmaxxr-1.0.0/README.md +118 -0
- tokenmaxxr-1.0.0/openspec/changes/archive/2026-07-22-add-quota-aggregator/.openspec.yaml +2 -0
- tokenmaxxr-1.0.0/openspec/changes/archive/2026-07-22-add-quota-aggregator/design.md +107 -0
- tokenmaxxr-1.0.0/openspec/changes/archive/2026-07-22-add-quota-aggregator/proposal.md +35 -0
- tokenmaxxr-1.0.0/openspec/changes/archive/2026-07-22-add-quota-aggregator/specs/cli-output/spec.md +49 -0
- tokenmaxxr-1.0.0/openspec/changes/archive/2026-07-22-add-quota-aggregator/specs/usage-caching/spec.md +46 -0
- tokenmaxxr-1.0.0/openspec/changes/archive/2026-07-22-add-quota-aggregator/specs/usage-collection/spec.md +95 -0
- tokenmaxxr-1.0.0/openspec/changes/archive/2026-07-22-add-quota-aggregator/specs/web-dashboard/spec.md +47 -0
- tokenmaxxr-1.0.0/openspec/changes/archive/2026-07-22-add-quota-aggregator/tasks.md +52 -0
- tokenmaxxr-1.0.0/openspec/config.yaml +20 -0
- tokenmaxxr-1.0.0/openspec/specs/cli-output/spec.md +53 -0
- tokenmaxxr-1.0.0/openspec/specs/usage-caching/spec.md +50 -0
- tokenmaxxr-1.0.0/openspec/specs/usage-collection/spec.md +99 -0
- tokenmaxxr-1.0.0/openspec/specs/web-dashboard/spec.md +37 -0
- tokenmaxxr-1.0.0/pyproject.toml +46 -0
- tokenmaxxr-1.0.0/scripts/start_server.sh +5 -0
- tokenmaxxr-1.0.0/scripts/stop_server.sh +3 -0
- tokenmaxxr-1.0.0/tests/conftest.py +8 -0
- tokenmaxxr-1.0.0/tests/fixtures/clinepass.html +1850 -0
- tokenmaxxr-1.0.0/tests/fixtures/ollama_cloud.html +12 -0
- tokenmaxxr-1.0.0/tests/fixtures/ollama_cloud_empty.html +12 -0
- tokenmaxxr-1.0.0/tests/fixtures/opencode_go.html +19 -0
- tokenmaxxr-1.0.0/tests/test_cache_history.py +74 -0
- tokenmaxxr-1.0.0/tests/test_cli.py +84 -0
- tokenmaxxr-1.0.0/tests/test_cookies.py +91 -0
- tokenmaxxr-1.0.0/tests/test_providers.py +107 -0
- tokenmaxxr-1.0.0/tests/test_reset.py +33 -0
- tokenmaxxr-1.0.0/tests/test_schema.py +29 -0
- tokenmaxxr-1.0.0/tests/test_serve.py +36 -0
- tokenmaxxr-1.0.0/tokenmaxxr/__init__.py +20 -0
- tokenmaxxr-1.0.0/tokenmaxxr/_webui/dist/app.js +160 -0
- tokenmaxxr-1.0.0/tokenmaxxr/_webui/dist/index.html +33 -0
- tokenmaxxr-1.0.0/tokenmaxxr/_webui/dist/styles.css +36 -0
- tokenmaxxr-1.0.0/tokenmaxxr/cache.py +125 -0
- tokenmaxxr-1.0.0/tokenmaxxr/cli.py +163 -0
- tokenmaxxr-1.0.0/tokenmaxxr/cookies.py +278 -0
- tokenmaxxr-1.0.0/tokenmaxxr/fetch.py +82 -0
- tokenmaxxr-1.0.0/tokenmaxxr/history.py +62 -0
- tokenmaxxr-1.0.0/tokenmaxxr/providers/__init__.py +21 -0
- tokenmaxxr-1.0.0/tokenmaxxr/providers/base.py +65 -0
- tokenmaxxr-1.0.0/tokenmaxxr/providers/clinepass.py +119 -0
- tokenmaxxr-1.0.0/tokenmaxxr/providers/ollama_cloud.py +138 -0
- tokenmaxxr-1.0.0/tokenmaxxr/providers/opencode_go.py +101 -0
- tokenmaxxr-1.0.0/tokenmaxxr/py.typed +1 -0
- tokenmaxxr-1.0.0/tokenmaxxr/reset.py +58 -0
- tokenmaxxr-1.0.0/tokenmaxxr/schema.py +71 -0
- tokenmaxxr-1.0.0/tokenmaxxr/serve.py +72 -0
- tokenmaxxr-1.0.0/uv.lock +780 -0
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
name: ci
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [main]
|
|
6
|
+
pull_request:
|
|
7
|
+
branches: [main]
|
|
8
|
+
|
|
9
|
+
jobs:
|
|
10
|
+
test:
|
|
11
|
+
runs-on: ubuntu-latest
|
|
12
|
+
strategy:
|
|
13
|
+
matrix:
|
|
14
|
+
python-version: ["3.10", "3.11", "3.12", "3.13"]
|
|
15
|
+
steps:
|
|
16
|
+
- uses: actions/checkout@v4
|
|
17
|
+
- name: Install uv
|
|
18
|
+
uses: astral-sh/setup-uv@v3
|
|
19
|
+
with:
|
|
20
|
+
enable-cache: true
|
|
21
|
+
- name: Set up Python ${{ matrix.python-version }}
|
|
22
|
+
run: uv python install ${{ matrix.python-version }}
|
|
23
|
+
- name: Install package
|
|
24
|
+
run: uv sync --all-extras --dev
|
|
25
|
+
- name: Run tests
|
|
26
|
+
run: uv run pytest -v
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
name: publish
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
release:
|
|
5
|
+
types: [published]
|
|
6
|
+
workflow_dispatch:
|
|
7
|
+
|
|
8
|
+
jobs:
|
|
9
|
+
build:
|
|
10
|
+
runs-on: ubuntu-latest
|
|
11
|
+
steps:
|
|
12
|
+
- uses: actions/checkout@v4
|
|
13
|
+
- uses: astral-sh/setup-uv@v3
|
|
14
|
+
- name: Build sdist and wheel
|
|
15
|
+
run: uv build
|
|
16
|
+
- name: Upload artifacts
|
|
17
|
+
uses: actions/upload-artifact@v4
|
|
18
|
+
with:
|
|
19
|
+
name: dist
|
|
20
|
+
path: dist/
|
|
21
|
+
|
|
22
|
+
publish:
|
|
23
|
+
needs: build
|
|
24
|
+
runs-on: ubuntu-latest
|
|
25
|
+
environment: pypi
|
|
26
|
+
if: github.event_name == 'release'
|
|
27
|
+
permissions:
|
|
28
|
+
id-token: write # for PyPI trusted publishing
|
|
29
|
+
steps:
|
|
30
|
+
- uses: actions/checkout@v4
|
|
31
|
+
- uses: actions/download-artifact@v4
|
|
32
|
+
with:
|
|
33
|
+
name: dist
|
|
34
|
+
path: dist/
|
|
35
|
+
- name: Publish to PyPI
|
|
36
|
+
uses: pypa/gh-action-pypi-publish@release/v1
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# Python
|
|
2
|
+
__pycache__/
|
|
3
|
+
*.py[cod]
|
|
4
|
+
*.egg-info/
|
|
5
|
+
build/
|
|
6
|
+
/dist/
|
|
7
|
+
.eggs/
|
|
8
|
+
*.egg
|
|
9
|
+
.venv/
|
|
10
|
+
.pytest_cache/
|
|
11
|
+
.ruff_cache/
|
|
12
|
+
.mypy_cache/
|
|
13
|
+
|
|
14
|
+
# Editor
|
|
15
|
+
.idea/
|
|
16
|
+
.vscode/
|
|
17
|
+
*.swp
|
|
18
|
+
|
|
19
|
+
# OS
|
|
20
|
+
.DS_Store
|
|
21
|
+
Thumbs.db
|
|
22
|
+
|
|
23
|
+
# State
|
|
24
|
+
.cookies/
|
|
25
|
+
local_state/
|
|
26
|
+
|
|
27
|
+
# Test artifacts
|
|
28
|
+
*.db
|
|
29
|
+
*.sqlite
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project are documented here.
|
|
4
|
+
This project follows [Keep a Changelog](https://keepachangelog.com/) and
|
|
5
|
+
[Semantic Versioning](https://semver.org/).
|
|
6
|
+
|
|
7
|
+
## 1.0.0 — 2026-07-23
|
|
8
|
+
|
|
9
|
+
First stable release. Aggregates AI subscription quota usage from three
|
|
10
|
+
providers into one versioned JSON document.
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
- Three providers: OpenCode Go, ClinePass, Ollama Cloud.
|
|
14
|
+
- `tokenmaxxr` CLI — human table (default), `--json` schema:1 document,
|
|
15
|
+
`--refresh` (bypass 300s cache), `--provider` filter (repeatable).
|
|
16
|
+
- `tokenmaxxr --version` and `tokenmaxxr.__version__`.
|
|
17
|
+
- `tokenmaxxr doctor` — diagnose cookie discovery per provider.
|
|
18
|
+
- `tokenmaxxr serve` — FastAPI WebUI + HTTP API on `127.0.0.1:8765`
|
|
19
|
+
(`/api/usage`, `/api/refresh`, `/api/history`, vanilla-JS dashboard).
|
|
20
|
+
- Cookie discovery chain: `--cookie-file`/`TOKENMAXXR_COOKIE_*` env →
|
|
21
|
+
Firefox-family stores (Mozilla, Zen, Flatpak Zen, Floorp, LibreWolf,
|
|
22
|
+
Waterfox) → Chromium-family via `browser-cookie3` → `no-auth`.
|
|
23
|
+
- WAL-safe cookie reads (temp-file copy), 300s cache, and history
|
|
24
|
+
append under `~/.local/state/tokenmaxxr/`.
|
|
25
|
+
- Versioned `schema:1` JSON contract with `ok|stale|no-auth|error` status.
|
|
26
|
+
- CI matrix (Python 3.10–3.13), OIDC trusted publishing workflow.
|
|
27
|
+
|
|
28
|
+
### Fixed
|
|
29
|
+
- WebUI dashboard now ships in the wheel (`.gitignore` `dist/` rule was
|
|
30
|
+
excluding `tokenmaxxr/_webui/dist/`; scoped to `/dist/`).
|
tokenmaxxr-1.0.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Izzur
|
|
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,148 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: tokenmaxxr
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: Aggregate AI subscription quota usage (OpenCode Go, ClinePass, Ollama Cloud) into one view.
|
|
5
|
+
Project-URL: Homepage, https://github.com/Izzur/tokenmaxxr
|
|
6
|
+
Project-URL: Bug Tracker, https://github.com/Izzur/tokenmaxxr/issues
|
|
7
|
+
Author-email: Izzur <13365626+Izzur@users.noreply.github.com>
|
|
8
|
+
Maintainer-email: Izzur <13365626+Izzur@users.noreply.github.com>
|
|
9
|
+
License: MIT
|
|
10
|
+
License-File: LICENSE
|
|
11
|
+
Keywords: ai,cline,fastfetch,ollama,opencode,quota,usage,waybar
|
|
12
|
+
Classifier: Environment :: Console
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
15
|
+
Classifier: Operating System :: OS Independent
|
|
16
|
+
Classifier: Programming Language :: Python :: 3
|
|
17
|
+
Classifier: Topic :: Utilities
|
|
18
|
+
Requires-Python: >=3.10
|
|
19
|
+
Requires-Dist: beautifulsoup4>=4.12
|
|
20
|
+
Requires-Dist: browser-cookie3>=0.20
|
|
21
|
+
Requires-Dist: requests>=2.31
|
|
22
|
+
Provides-Extra: dev
|
|
23
|
+
Requires-Dist: httpx>=0.27; extra == 'dev'
|
|
24
|
+
Requires-Dist: pytest-asyncio>=0.23; extra == 'dev'
|
|
25
|
+
Requires-Dist: pytest>=8; extra == 'dev'
|
|
26
|
+
Provides-Extra: serve
|
|
27
|
+
Requires-Dist: fastapi>=0.110; extra == 'serve'
|
|
28
|
+
Requires-Dist: uvicorn>=0.27; extra == 'serve'
|
|
29
|
+
Description-Content-Type: text/markdown
|
|
30
|
+
|
|
31
|
+
# tokenmaxxr
|
|
32
|
+
|
|
33
|
+
Aggregate AI subscription quota usage (OpenCode Go, ClinePass, Ollama Cloud) into one view.
|
|
34
|
+
|
|
35
|
+
Three providers, one engine, one versioned JSON contract. CLI for bars/dashboards, optional WebUI, optional HTTP API.
|
|
36
|
+
|
|
37
|
+
## Install
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
# from PyPI (recommended)
|
|
41
|
+
uvx tokenmaxxr --json
|
|
42
|
+
|
|
43
|
+
# from source
|
|
44
|
+
git clone https://github.com/Izzur/tokenmaxxr
|
|
45
|
+
cd tokenmaxxr
|
|
46
|
+
uv pip install -e ".[serve]" # add [serve] for `tokenmaxxr serve`
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
## Quickstart
|
|
50
|
+
|
|
51
|
+
```bash
|
|
52
|
+
tokenmaxxr # human table
|
|
53
|
+
tokenmaxxr --json # machine-readable schema:1
|
|
54
|
+
tokenmaxxr --refresh # bypass the 300s cache
|
|
55
|
+
tokenmaxxr doctor # diagnose cookie discovery
|
|
56
|
+
tokenmaxxr serve # WebUI + HTTP API
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
## Web UI
|
|
60
|
+
|
|
61
|
+
`tokenmaxxr serve` (requires `pip install 'tokenmaxxr[serve]'`) starts a small FastAPI app on `127.0.0.1:8765`:
|
|
62
|
+
|
|
63
|
+
- `GET /api/usage` — schema:1 document (same as `tokenmaxxr --json`)
|
|
64
|
+
- `POST /api/refresh` — force a live re-fetch
|
|
65
|
+
- `GET /api/history?provider=&window=` — appended history rows
|
|
66
|
+
- `GET /` — vanilla-JS dashboard (cards + history chart, opt-in 60s auto-refresh)
|
|
67
|
+
|
|
68
|
+
The dashboard degrades gracefully: with no cookies, every card shows `no-auth` and a hint.
|
|
69
|
+
|
|
70
|
+
## Output contract
|
|
71
|
+
|
|
72
|
+
`tokenmaxxr --json` returns a stable, versioned document:
|
|
73
|
+
|
|
74
|
+
```json
|
|
75
|
+
{
|
|
76
|
+
"schema": 1,
|
|
77
|
+
"fetched_at": "2026-07-22T12:00:00Z",
|
|
78
|
+
"providers": [
|
|
79
|
+
{
|
|
80
|
+
"id": "ollama-cloud",
|
|
81
|
+
"name": "Ollama Cloud",
|
|
82
|
+
"status": "ok",
|
|
83
|
+
"windows": [
|
|
84
|
+
{"label": "5h", "pct_used": 47, "reset_at": "2026-07-22T20:00:00Z"}
|
|
85
|
+
]
|
|
86
|
+
}
|
|
87
|
+
]
|
|
88
|
+
}
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
`status` is one of `ok` | `stale` | `no-auth` | `error`.
|
|
92
|
+
|
|
93
|
+
## Cookie sources
|
|
94
|
+
|
|
95
|
+
1. `--cookie-file SLUG=/path/to/cookies.sqlite` or `TOKENMAXXR_COOKIE_<DOMAIN>` env
|
|
96
|
+
2. Firefox-family stores (globbed: `~/.mozilla/firefox/*`, `~/.zen/*`, Zen/Firefox Flatpak paths, Floorp/LibreWolf/Waterfox)
|
|
97
|
+
3. Chromium-family via `browser_cookie3` (best-effort, locked keyring → skip)
|
|
98
|
+
4. No match → `no-auth` status + remediation in `doctor`
|
|
99
|
+
|
|
100
|
+
Cookies are copied to a temp file before reading (WAL lock-safe). Cache and history live under `~/.local/state/tokenmaxxr/`.
|
|
101
|
+
|
|
102
|
+
## Waybar
|
|
103
|
+
|
|
104
|
+
```json
|
|
105
|
+
"custom/tokenmaxxr": {
|
|
106
|
+
"exec": "tokenmaxxr --json --provider ollama-cloud",
|
|
107
|
+
"return-type": "json",
|
|
108
|
+
"format": "Ollama 5h: {}%",
|
|
109
|
+
"exec-on-event": true,
|
|
110
|
+
"interval": 600
|
|
111
|
+
}
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
`--json` is the stable schema:1 contract — `jq '.providers[0].windows[0].pct_used'` always returns an integer.
|
|
115
|
+
|
|
116
|
+
## fastfetch
|
|
117
|
+
|
|
118
|
+
```text
|
|
119
|
+
"tokenmaxxr": {
|
|
120
|
+
"type": "command",
|
|
121
|
+
"key": "TKN",
|
|
122
|
+
"command": "tokenmaxxr --json | jq -r '.providers[] | select(.status==\"ok\") | \"\\(.id): \\(.windows[0].pct_used)%\"' | paste -sd, -"
|
|
123
|
+
}
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
## Headless / `--cookie-file`
|
|
127
|
+
|
|
128
|
+
```bash
|
|
129
|
+
tokenmaxxr --cookie-file ollama-cloud=/srv/cookies/ollama.sqlite --json
|
|
130
|
+
# or
|
|
131
|
+
TOKENMAXXR_COOKIE_OLLAMA_COM=/srv/cookies/ollama.sqlite tokenmaxxr --json
|
|
132
|
+
|
|
133
|
+
## Development
|
|
134
|
+
|
|
135
|
+
```bash
|
|
136
|
+
uv sync --extra dev # set up dev deps (pytest, fastapi, etc.)
|
|
137
|
+
uv run python3 -m pytest # 32 tests, <1s
|
|
138
|
+
uv run python3 -m pytest tests/test_providers.py -v # one file
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
Provider parser tests live in `tests/test_providers.py` against HTML/JSON
|
|
142
|
+
fixtures under `tests/fixtures/`. To add a fixture, drop a real captured
|
|
143
|
+
payload into `tests/fixtures/<slug>.<ext>` and write a `test_<slug>` that
|
|
144
|
+
feeds it through `RawHtml` and asserts on the parsed `UsageWindow`s.
|
|
145
|
+
|
|
146
|
+
## License
|
|
147
|
+
|
|
148
|
+
MIT
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
# tokenmaxxr
|
|
2
|
+
|
|
3
|
+
Aggregate AI subscription quota usage (OpenCode Go, ClinePass, Ollama Cloud) into one view.
|
|
4
|
+
|
|
5
|
+
Three providers, one engine, one versioned JSON contract. CLI for bars/dashboards, optional WebUI, optional HTTP API.
|
|
6
|
+
|
|
7
|
+
## Install
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
# from PyPI (recommended)
|
|
11
|
+
uvx tokenmaxxr --json
|
|
12
|
+
|
|
13
|
+
# from source
|
|
14
|
+
git clone https://github.com/Izzur/tokenmaxxr
|
|
15
|
+
cd tokenmaxxr
|
|
16
|
+
uv pip install -e ".[serve]" # add [serve] for `tokenmaxxr serve`
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## Quickstart
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
tokenmaxxr # human table
|
|
23
|
+
tokenmaxxr --json # machine-readable schema:1
|
|
24
|
+
tokenmaxxr --refresh # bypass the 300s cache
|
|
25
|
+
tokenmaxxr doctor # diagnose cookie discovery
|
|
26
|
+
tokenmaxxr serve # WebUI + HTTP API
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## Web UI
|
|
30
|
+
|
|
31
|
+
`tokenmaxxr serve` (requires `pip install 'tokenmaxxr[serve]'`) starts a small FastAPI app on `127.0.0.1:8765`:
|
|
32
|
+
|
|
33
|
+
- `GET /api/usage` — schema:1 document (same as `tokenmaxxr --json`)
|
|
34
|
+
- `POST /api/refresh` — force a live re-fetch
|
|
35
|
+
- `GET /api/history?provider=&window=` — appended history rows
|
|
36
|
+
- `GET /` — vanilla-JS dashboard (cards + history chart, opt-in 60s auto-refresh)
|
|
37
|
+
|
|
38
|
+
The dashboard degrades gracefully: with no cookies, every card shows `no-auth` and a hint.
|
|
39
|
+
|
|
40
|
+
## Output contract
|
|
41
|
+
|
|
42
|
+
`tokenmaxxr --json` returns a stable, versioned document:
|
|
43
|
+
|
|
44
|
+
```json
|
|
45
|
+
{
|
|
46
|
+
"schema": 1,
|
|
47
|
+
"fetched_at": "2026-07-22T12:00:00Z",
|
|
48
|
+
"providers": [
|
|
49
|
+
{
|
|
50
|
+
"id": "ollama-cloud",
|
|
51
|
+
"name": "Ollama Cloud",
|
|
52
|
+
"status": "ok",
|
|
53
|
+
"windows": [
|
|
54
|
+
{"label": "5h", "pct_used": 47, "reset_at": "2026-07-22T20:00:00Z"}
|
|
55
|
+
]
|
|
56
|
+
}
|
|
57
|
+
]
|
|
58
|
+
}
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
`status` is one of `ok` | `stale` | `no-auth` | `error`.
|
|
62
|
+
|
|
63
|
+
## Cookie sources
|
|
64
|
+
|
|
65
|
+
1. `--cookie-file SLUG=/path/to/cookies.sqlite` or `TOKENMAXXR_COOKIE_<DOMAIN>` env
|
|
66
|
+
2. Firefox-family stores (globbed: `~/.mozilla/firefox/*`, `~/.zen/*`, Zen/Firefox Flatpak paths, Floorp/LibreWolf/Waterfox)
|
|
67
|
+
3. Chromium-family via `browser_cookie3` (best-effort, locked keyring → skip)
|
|
68
|
+
4. No match → `no-auth` status + remediation in `doctor`
|
|
69
|
+
|
|
70
|
+
Cookies are copied to a temp file before reading (WAL lock-safe). Cache and history live under `~/.local/state/tokenmaxxr/`.
|
|
71
|
+
|
|
72
|
+
## Waybar
|
|
73
|
+
|
|
74
|
+
```json
|
|
75
|
+
"custom/tokenmaxxr": {
|
|
76
|
+
"exec": "tokenmaxxr --json --provider ollama-cloud",
|
|
77
|
+
"return-type": "json",
|
|
78
|
+
"format": "Ollama 5h: {}%",
|
|
79
|
+
"exec-on-event": true,
|
|
80
|
+
"interval": 600
|
|
81
|
+
}
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
`--json` is the stable schema:1 contract — `jq '.providers[0].windows[0].pct_used'` always returns an integer.
|
|
85
|
+
|
|
86
|
+
## fastfetch
|
|
87
|
+
|
|
88
|
+
```text
|
|
89
|
+
"tokenmaxxr": {
|
|
90
|
+
"type": "command",
|
|
91
|
+
"key": "TKN",
|
|
92
|
+
"command": "tokenmaxxr --json | jq -r '.providers[] | select(.status==\"ok\") | \"\\(.id): \\(.windows[0].pct_used)%\"' | paste -sd, -"
|
|
93
|
+
}
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
## Headless / `--cookie-file`
|
|
97
|
+
|
|
98
|
+
```bash
|
|
99
|
+
tokenmaxxr --cookie-file ollama-cloud=/srv/cookies/ollama.sqlite --json
|
|
100
|
+
# or
|
|
101
|
+
TOKENMAXXR_COOKIE_OLLAMA_COM=/srv/cookies/ollama.sqlite tokenmaxxr --json
|
|
102
|
+
|
|
103
|
+
## Development
|
|
104
|
+
|
|
105
|
+
```bash
|
|
106
|
+
uv sync --extra dev # set up dev deps (pytest, fastapi, etc.)
|
|
107
|
+
uv run python3 -m pytest # 32 tests, <1s
|
|
108
|
+
uv run python3 -m pytest tests/test_providers.py -v # one file
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
Provider parser tests live in `tests/test_providers.py` against HTML/JSON
|
|
112
|
+
fixtures under `tests/fixtures/`. To add a fixture, drop a real captured
|
|
113
|
+
payload into `tests/fixtures/<slug>.<ext>` and write a `test_<slug>` that
|
|
114
|
+
feeds it through `RawHtml` and asserts on the parsed `UsageWindow`s.
|
|
115
|
+
|
|
116
|
+
## License
|
|
117
|
+
|
|
118
|
+
MIT
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
## Context
|
|
2
|
+
|
|
3
|
+
Greenfield repo (`~/z/tokenmaxxr`, currently only `openspec/`). The user subscribes to three AI coding services — OpenCode Go ($10/mo), ClinePass ($9.99/mo), Ollama Cloud — and wants one place to see quota usage. Investigation on 2026-07-22 established:
|
|
4
|
+
|
|
5
|
+
- **No official usage APIs exist.** OpenCode (anomalyco/opencode#18648) and Ollama (ollama/ollama#15663) both closed feature requests without shipping an endpoint; ClinePass documents dashboard-only usage. Web-scraping with a session cookie is the only path.
|
|
6
|
+
- **Inference credentials don't work for usage.** The API keys/OAuth tokens in `~/.local/share/opencode/auth.json` and `~/.config/pi/agent/auth.json` authenticate chat-completions calls only. Usage data lives behind the web dashboard session cookie.
|
|
7
|
+
- **Prior art exists for 2 of 3 providers.** `dzackgarza/usage-limits` (Python) has working OpenCode Go and Ollama Cloud scrapers; `~hrbrmstr/ollama-usage` (Go) has another Ollama one. ClinePass has no published scraper, but its selectors were verified from a real dashboard dump.
|
|
8
|
+
- **The user's browser is Zen (Firefox fork) via Flatpak.** Verified live cookies for all three target domains in `~/.var/app/app.zen_browser.zen/.zen/skn3ylbm.Default (release)/cookies.sqlite` (plaintext SQLite — no keychain dance).
|
|
9
|
+
|
|
10
|
+
Downstream consumers already queued: Waybar (exec + JSON), fastfetch custom module (exec one-liner), and "other people's Java/Python/Go/TS projects" — which all collapse into one requirement: a stable, versioned JSON contract plus exec/HTTP access.
|
|
11
|
+
|
|
12
|
+
## Goals / Non-Goals
|
|
13
|
+
|
|
14
|
+
**Goals:**
|
|
15
|
+
- One Python engine that scrapes all three dashboards and normalizes to a versioned JSON schema (`schema: 1`).
|
|
16
|
+
- Browser-agnostic cookie resolution: works on Zen/Firefox/Chromium/Flatpak variants without per-browser code forks; headless override via `--cookie-file`/env.
|
|
17
|
+
- Cache-by-default (TTL 300s) with explicit `--refresh`; stale-fallback when a fetch fails; failure-tolerant per provider (one provider's breakage never blocks the others).
|
|
18
|
+
- Append-only `history.jsonl` so the WebUI can graph trends without a database.
|
|
19
|
+
- CLI with human table + `--json`; `doctor` command for auth diagnostics.
|
|
20
|
+
- Optional `tokenmaxxr serve` (FastAPI) + vanilla-JS WebUI (single `index.html` + `styles.css` + `app.js` served from `tokenmaxxr/_webui/dist/`, no build step) with opt-in auto-refresh and canvas2D history graphs. See `specs/web-dashboard/spec.md` for the rejection note on the Vite + React + shadcn/ui + TanStack Query + Recharts stack.
|
|
21
|
+
- Ship as a PyPI package runnable via `uvx tokenmaxxr`.
|
|
22
|
+
|
|
23
|
+
**Non-Goals:**
|
|
24
|
+
- Per-language SDKs (Java/Go/TS). The JSON contract + CLI exec + HTTP API covers them.
|
|
25
|
+
- A Go static CLI binary (deferred until a real consumer needs it).
|
|
26
|
+
- Historical backfill, a real database, pruning/compaction of `history.jsonl` (revisit when it's actually big).
|
|
27
|
+
- Auto-refresh as default behavior in the WebUI (opt-in only).
|
|
28
|
+
- Notifications/alerting (ntfy, email) — usage-limits does this; we don't, yet.
|
|
29
|
+
- Multi-account per provider (one cookie set per domain).
|
|
30
|
+
|
|
31
|
+
## Decisions
|
|
32
|
+
|
|
33
|
+
### D1: One Python engine + JSON contract, not N SDKs
|
|
34
|
+
|
|
35
|
+
The "reusable for other people" requirement is satisfied by a **stable versioned output schema** consumed via `--json` (exec) or HTTP — not by writing the scraper in four languages. Scraper markup churns; one codebase to fix beats four.
|
|
36
|
+
|
|
37
|
+
- Alternatives considered: 4 SDKs (rejected — 4× maintenance on the fragile part); Go CLI reimplementation now (rejected — parallel experiment on an unstabilized schema; revisit post-1.0 if static-binary demand appears).
|
|
38
|
+
|
|
39
|
+
### D2: CLI is Python, not Go/Rust
|
|
40
|
+
|
|
41
|
+
`tokenmaxxr` is a console-script entry point of the core package. The CLI is a thin formatter over `fetch()` output; putting it in the same language as the engine removes an entire sync surface. Static-binary distribution is a "later" port of a stabilized schema.
|
|
42
|
+
|
|
43
|
+
### D3: Cookie resolution chain (custom thin resolver over browser_cookie3)
|
|
44
|
+
|
|
45
|
+
Per provider domain, resolve cookies in order:
|
|
46
|
+
1. `--cookie-file <path>` / `TOKENMAXXR_COOKIE_<PROVIDER>` env (headless/servers).
|
|
47
|
+
2. Firefox-family stores, globbed: `~/.mozilla/firefox/*`, `~/.zen/*`, `~/.var/app/app.zen_browser.zen/.zen/*`, `~/.var/app/org.mozilla.firefox/...`, floorp/librewolf/waterfox equivalents → `cookies.sqlite` (plaintext).
|
|
48
|
+
3. Chromium-family via `browser_cookie3` (needs unlocked libsecret keyring — best-effort).
|
|
49
|
+
4. No match → provider status `no-auth`; `doctor` prints remediation.
|
|
50
|
+
|
|
51
|
+
Always **copy `cookies.sqlite` to a temp file before reading** (WAL lock conflicts with a running browser). browser_cookie3 doesn't know Zen's Flatpak path, so we glob ourselves and hand it paths.
|
|
52
|
+
|
|
53
|
+
- Alternatives considered: pure browser_cookie3 (rejected — misses Zen Flatpak, the user's actual setup); Playwright/selenium automation (rejected — heavyweight, brittle, and unnecessary when the cookie store is readable).
|
|
54
|
+
|
|
55
|
+
### D4: Per-provider scrapers behind a common interface
|
|
56
|
+
|
|
57
|
+
```python
|
|
58
|
+
class Provider(Protocol):
|
|
59
|
+
slug: str
|
|
60
|
+
def fetch_raw(self, cookies: CookieJar) -> RawHtml: ...
|
|
61
|
+
def to_rows(self, raw: RawHtml) -> list[UsageWindow]: ...
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
Anchor on **semantic HTML** (ARIA roles/labels, `data-slot`, `data-time`) rather than utility classes:
|
|
65
|
+
|
|
66
|
+
| Provider | URL | Anchor | pct | reset |
|
|
67
|
+
|---|---|---|---|---|
|
|
68
|
+
| opencode-go | `opencode.ai/workspace/{id}/go` (id via `/auth` redirect) | `[data-slot=usage-item]` | `[data-slot=usage-value]` | `[data-slot=reset-time]` rel. text |
|
|
69
|
+
| clinepass | `app.cline.bot/dashboard/subscription?personal=true` | `[role=progressbar][aria-label$=" usage"]` | `@aria-valuenow` | sibling "Resets in…" rel. text |
|
|
70
|
+
| ollama-cloud | `ollama.com/settings` | `[aria-label^="Session usage "]/["Weekly usage "]` | aria-label text | sibling `[data-time]` ISO |
|
|
71
|
+
|
|
72
|
+
Relative reset strings parse to absolute UTC (`now + delta`) at fetch time.
|
|
73
|
+
|
|
74
|
+
### D5: Cache = latest-value JSON; history = separate JSONL
|
|
75
|
+
|
|
76
|
+
```
|
|
77
|
+
~/.local/state/tokenmaxxr/
|
|
78
|
+
├── cache/<provider>.json # {fetched_at, payload}; TTL 300s; --refresh deletes first
|
|
79
|
+
└── history.jsonl # {"ts","provider","window","pct_used"} per window per fetch
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
Cache answers "what's the number now" fast; history answers "how did it trend". On fetch failure within TTL: serve stale cache and mark provider `stale`. On failure with no cache: status `error` (or `no-auth` for cookie problems). Never let one provider's failure poison the others' output.
|
|
83
|
+
|
|
84
|
+
### D6: WebUI = `tokenmaxxr serve` hosting a vanilla-JS single page
|
|
85
|
+
|
|
86
|
+
FastAPI serves `GET /api/usage` (schema:1 JSON), `GET /api/history?provider=&window=`, `POST /api/refresh`, and statics from `tokenmaxxr/_webui/dist/`. The frontend is a single `index.html` + `styles.css` + `app.js` — no build step, no `node_modules`. The history chart is a hand-rolled canvas2D line plot. This keeps one process owning cache+history — no dual-write consistency problem. Opt-in 60s auto-refresh via a manual toggle button.
|
|
87
|
+
|
|
88
|
+
- Alternatives considered: Vite + React + shadcn/ui + TanStack Query + Recharts (rejected 2026-07-22 — dashboard is one route with three data sources, the JSON contract is the only real API surface, and pulling in a build pipeline + node_modules would add a JS toolchain the project otherwise does not need). See `openspec/changes/add-quota-aggregator/specs/web-dashboard/spec.md` for the spec-side rejection note and the threshold for revisiting.
|
|
89
|
+
|
|
90
|
+
- Revisit if any of: a second route appears, the chart requirements exceed what canvas2D can draw, or vanilla-JS component complexity crosses ~500 lines. At that point Recharts + TanStack Query is the preferred upgrade path.
|
|
91
|
+
|
|
92
|
+
## Risks / Trade-offs
|
|
93
|
+
|
|
94
|
+
- Vendor redesigns dashboard markup → scraper breaks. → Semantic anchors (D4) reduce frequency; per-provider isolation means one breakage ≠ total outage; `error` status is explicit in output; prior-art repos act as an early-warning canary.
|
|
95
|
+
- Session cookie expires/revoked → provider can't fetch. → Status becomes `no-auth`; `doctor` names the exact missing domain; re-login in the browser fixes it with zero config changes.
|
|
96
|
+
- Cookie extraction touches sensitive data → misuse risk. → Read-only, copy-before-read, cookies only ever sent to their own domain over HTTPS; cache/history contain only percentages/timestamps, never credentials.
|
|
97
|
+
- Chromium keyring locked → Chromium path fails silently. → Expected; chain order prefers Firefox-family (plaintext), and `doctor` reports which stores were readable.
|
|
98
|
+
- Scraping may violate ToS. → Accepted: read-only, low-frequency (TTL-gated), same data the user sees in their own dashboard; both prior-art projects operate the same way.
|
|
99
|
+
- Relative reset-time parsing is locale/fragile → wrong reset_at. → Ollama gives ISO via `data-time` (parse that preferentially); others get a regex over "Resets in Nd Nh Nm" with unit tests against the captured dump.
|
|
100
|
+
|
|
101
|
+
## Migration Plan
|
|
102
|
+
|
|
103
|
+
Greenfield — no migration. Rollout: implement core → CLI → serve/WebUI in the order given in `tasks.md`. Rollback is `pip uninstall`; all state lives under `~/.local/state/tokenmaxxr/` and is safe to delete.
|
|
104
|
+
|
|
105
|
+
## Open Questions
|
|
106
|
+
|
|
107
|
+
- Exact Zen non-Flatpak Linux path (`~/.zen` vs XDG data dir) — glob both; only Flatpak verified so far.
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
## Why
|
|
2
|
+
|
|
3
|
+
Checking quota usage for three AI subscriptions (OpenCode Go, ClinePass, Ollama Cloud) means opening three different web dashboards by hand. None of the providers exposes an official usage/quota API (confirmed: anomalyco/opencode#18648 closed without an endpoint, ollama/ollama#15663 closed without an endpoint, ClinePass documents dashboard-only usage), so the data has to be scraped from the dashboards with a browser session cookie. tokenmaxxr centralizes that scrape behind one engine, one versioned JSON contract, and a CLI/WebUI on top.
|
|
4
|
+
|
|
5
|
+
## What Changes
|
|
6
|
+
|
|
7
|
+
- New Python core package (`core/tokenmaxxr/`): provider plugins that fetch and parse usage HTML, normalize results into a versioned JSON schema (`schema: 1`), and persist results.
|
|
8
|
+
- Three providers: `opencode-go`, `clinepass`, `ollama-cloud` (ClinePass selectors verified against a real dashboard dump; the other two crib from published prior art).
|
|
9
|
+
- Browser-agnostic cookie resolution chain: explicit `--cookie-file`/env override → glob of Firefox-family stores (incl. Zen Flatpak paths) → Chromium-family best-effort → `no-auth` status with doctor hint. Cookies are copied to a temp file before reading (WAL lock on running browsers).
|
|
10
|
+
- Per-provider TTL cache (default 300s) as JSON files under `~/.local/state/tokenmaxxr/cache/`; `--refresh` bypasses the cache. Failures fall back to stale cache when available.
|
|
11
|
+
- Append-only history log `~/.local/state/tokenmaxxr/history.jsonl` written on every successful fetch (drives WebUI graphs).
|
|
12
|
+
- `tokenmaxxr` CLI: human table output (default) and `--json` (the stable contract for Waybar/fastfetch/any consumer), plus `tokenmaxxr doctor` (profile + per-domain cookie presence report).
|
|
13
|
+
- Optional `tokenmaxxr serve`: FastAPI server exposing the JSON contract + history, serving the WebUI build.
|
|
14
|
+
- Vanilla-JS WebUI (`tokenmaxxr/_webui/dist/`): single `index.html` + `styles.css` + `app.js` (no build step, no `node_modules`); opt-in auto-refresh via a manual toggle, history graphs hand-rolled on canvas2D. See `specs/web-dashboard/spec.md` for the rejection note on Vite + React + shadcn/ui + TanStack Query + Recharts and the threshold for revisiting that stack.
|
|
15
|
+
- Distribution via PyPI/`uvx tokenmaxxr`. No per-language SDKs — other languages consume the JSON contract or exec the CLI.
|
|
16
|
+
|
|
17
|
+
## Capabilities
|
|
18
|
+
|
|
19
|
+
### New Capabilities
|
|
20
|
+
- `usage-collection`: Provider plugins that authenticate via resolved browser cookies, fetch usage dashboards, parse them into normalized usage windows (label, pct_used, reset_at), and surface per-provider status (`ok`/`stale`/`error`/`no-auth`).
|
|
21
|
+
- `usage-caching`: TTL cache of the latest fetch per provider, force-refresh bypass, stale-fallback on fetch failure, and append-only history logging of successful fetches.
|
|
22
|
+
- `cli-output`: The `tokenmaxxr` command — table and `--json` output of the versioned schema, `--refresh`, per-provider selection, and the `doctor` diagnostics command.
|
|
23
|
+
- `web-dashboard`: Optional `tokenmaxxr serve` HTTP API (current usage + history) and the vanilla-JS WebUI with opt-in auto-refresh and canvas2D history graphs.
|
|
24
|
+
|
|
25
|
+
### Modified Capabilities
|
|
26
|
+
<!-- None — greenfield repo, no existing specs. -->
|
|
27
|
+
|
|
28
|
+
## Impact
|
|
29
|
+
|
|
30
|
+
- **Code**: greenfield repo; adds `core/`, `cli` entry point, `web/`, `openspec/` artifacts.
|
|
31
|
+
- **Dependencies (core)**: `requests`, `beautifulsoup4`, `browser_cookie3`, `fastapi`+`uvicorn` (serve only). Web: zero — single static page served from `tokenmaxxr/_webui/dist/`, no build toolchain.
|
|
32
|
+
- **Data**: writes only under `~/.local/state/tokenmaxxr/` (cache + history). Reads browser cookie stores (copy-before-read, never mutates).
|
|
33
|
+
- **Security**: reads sensitive session cookies locally; never transmits them anywhere except the owning provider's domain over HTTPS. `history.jsonl` and cache contain only percentages/timestamps — no credentials.
|
|
34
|
+
- **Consumers**: Waybar (exec + JSON), fastfetch custom module (exec one-liner), any language via `--json` or the HTTP API.
|
|
35
|
+
- **Fragility (accepted)**: scrapers depend on vendor dashboard markup; a redesign degrades one provider to `error` status without affecting others.
|
tokenmaxxr-1.0.0/openspec/changes/archive/2026-07-22-add-quota-aggregator/specs/cli-output/spec.md
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
## ADDED Requirements
|
|
2
|
+
|
|
3
|
+
### Requirement: Versioned JSON output contract
|
|
4
|
+
|
|
5
|
+
The CLI SHALL support `--json` producing a stable, versioned JSON document. The document MUST include a top-level `schema` field (integer, currently `1`), `fetched_at` (UTC ISO 8601), and a `providers` array. Each provider entry MUST include `id`, `name`, `status` (`ok`, `stale`, `error`, or `no-auth`), and a `windows` array of `{label, pct_used, reset_at}` objects. The schema version MUST NOT change without a corresponding spec change.
|
|
6
|
+
|
|
7
|
+
#### Scenario: Machine consumers get stable shape
|
|
8
|
+
- **WHEN** any consumer runs `tokenmaxxr --json`
|
|
9
|
+
- **THEN** the output parses against the schema:1 contract regardless of which providers succeeded or failed
|
|
10
|
+
|
|
11
|
+
#### Scenario: Waybar-style consumption
|
|
12
|
+
- **WHEN** a consumer extracts `providers[i].windows[0].pct_used` from `--json` output
|
|
13
|
+
- **THEN** the value is an integer percentage suitable for direct display
|
|
14
|
+
|
|
15
|
+
### Requirement: Human table output
|
|
16
|
+
|
|
17
|
+
By default (no `--json`), the CLI SHALL print a human-readable table listing each provider, its windows, percentage used, and reset time. Providers with non-`ok` status MUST be visibly marked.
|
|
18
|
+
|
|
19
|
+
#### Scenario: Default invocation prints table
|
|
20
|
+
- **WHEN** the user runs `tokenmaxxr` with no flags
|
|
21
|
+
- **THEN** a table of all providers' windows is printed, with stale/error/no-auth providers marked
|
|
22
|
+
|
|
23
|
+
### Requirement: Refresh and provider selection flags
|
|
24
|
+
|
|
25
|
+
The CLI SHALL support `--refresh` (bypass cache) and a provider-selection option (e.g. `--provider opencode-go`, repeatable) restricting collection to the named providers.
|
|
26
|
+
|
|
27
|
+
#### Scenario: Single provider query
|
|
28
|
+
- **WHEN** the user runs `tokenmaxxr --provider ollama-cloud`
|
|
29
|
+
- **THEN** only the ollama-cloud provider is collected and rendered
|
|
30
|
+
|
|
31
|
+
### Requirement: Doctor diagnostics
|
|
32
|
+
|
|
33
|
+
The CLI SHALL provide `tokenmaxxr doctor`, which MUST report, for each provider: which cookie stores were discovered, whether cookies for the provider's domain were found, and the remediation step when missing. Doctor MUST NOT print cookie values.
|
|
34
|
+
|
|
35
|
+
#### Scenario: Missing cookie remediation
|
|
36
|
+
- **WHEN** no cookie exists for a provider's domain
|
|
37
|
+
- **THEN** doctor names the missing domain and instructs the user to log in via their browser (or use `--cookie-file`)
|
|
38
|
+
|
|
39
|
+
#### Scenario: Cookie values never shown
|
|
40
|
+
- **WHEN** doctor finds valid cookies for all providers
|
|
41
|
+
- **THEN** it reports presence only, never cookie contents
|
|
42
|
+
|
|
43
|
+
### Requirement: Distribution as installable package
|
|
44
|
+
|
|
45
|
+
The core engine and CLI SHALL be distributed as a single Python package installable from PyPI and runnable via `uvx tokenmaxxr`.
|
|
46
|
+
|
|
47
|
+
#### Scenario: Run without install
|
|
48
|
+
- **WHEN** a user runs `uvx tokenmaxxr --json`
|
|
49
|
+
- **THEN** the command works without a prior explicit install step
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
## ADDED Requirements
|
|
2
|
+
|
|
3
|
+
### Requirement: Per-provider TTL cache
|
|
4
|
+
|
|
5
|
+
The system SHALL cache the latest successful fetch per provider as a JSON file under `~/.local/state/tokenmaxxr/cache/<provider>.json`, recording the fetch timestamp and the normalized payload. The default TTL MUST be 300 seconds. When a cached entry is younger than the TTL, the system MUST return the cached payload without contacting the provider.
|
|
6
|
+
|
|
7
|
+
#### Scenario: Cache hit within TTL
|
|
8
|
+
- **WHEN** a fetch is requested and a cache entry younger than the TTL exists
|
|
9
|
+
- **THEN** the cached payload is returned and no network request is made
|
|
10
|
+
|
|
11
|
+
#### Scenario: Cache miss after TTL
|
|
12
|
+
- **WHEN** a fetch is requested and the cache entry is older than the TTL (or absent)
|
|
13
|
+
- **THEN** the provider is fetched live and the cache file is overwritten on success
|
|
14
|
+
|
|
15
|
+
### Requirement: Force refresh
|
|
16
|
+
|
|
17
|
+
The system SHALL support a `--refresh` flag (and equivalent API trigger) that bypasses the cache and fetches all selected providers live.
|
|
18
|
+
|
|
19
|
+
#### Scenario: Refresh bypasses fresh cache
|
|
20
|
+
- **WHEN** the user runs with `--refresh` and a fresh cache entry exists
|
|
21
|
+
- **THEN** the system fetches live anyway and overwrites the cache on success
|
|
22
|
+
|
|
23
|
+
### Requirement: Stale fallback on failure
|
|
24
|
+
|
|
25
|
+
When a live fetch fails and a cache entry exists (of any age), the system SHALL serve the stale cached payload and mark that provider's status as `stale`. When a live fetch fails and no cache exists, the provider's status MUST be `error`.
|
|
26
|
+
|
|
27
|
+
#### Scenario: Stale data beats no data
|
|
28
|
+
- **WHEN** a live fetch raises and a cache entry exists
|
|
29
|
+
- **THEN** output shows the cached values with status `stale`
|
|
30
|
+
|
|
31
|
+
#### Scenario: Failure with no cache
|
|
32
|
+
- **WHEN** a live fetch raises and no cache entry exists
|
|
33
|
+
- **THEN** the provider reports status `error` and other providers are unaffected
|
|
34
|
+
|
|
35
|
+
### Requirement: Append-only history log
|
|
36
|
+
|
|
37
|
+
On every successful live fetch, the system SHALL append one JSON line per usage window to `~/.local/state/tokenmaxxr/history.jsonl`. Each line MUST contain `ts` (fetch time, UTC ISO 8601), `provider`, `window` (label), and `pct_used`. Cache hits MUST NOT write history. The system MUST NOT require a database for history storage.
|
|
38
|
+
|
|
39
|
+
#### Scenario: History written on live fetch only
|
|
40
|
+
- **WHEN** a live fetch of a provider with three windows succeeds
|
|
41
|
+
- **THEN** three lines are appended to history.jsonl
|
|
42
|
+
- **AND** a subsequent cache-hit fetch appends nothing
|
|
43
|
+
|
|
44
|
+
#### Scenario: Failed fetch writes no history
|
|
45
|
+
- **WHEN** a live fetch fails (serving stale or error)
|
|
46
|
+
- **THEN** no lines are appended to history.jsonl for that provider
|