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.
Files changed (54) hide show
  1. tokenmaxxr-1.0.0/.github/workflows/ci.yml +26 -0
  2. tokenmaxxr-1.0.0/.github/workflows/publish.yml +36 -0
  3. tokenmaxxr-1.0.0/.gitignore +29 -0
  4. tokenmaxxr-1.0.0/CHANGELOG.md +30 -0
  5. tokenmaxxr-1.0.0/LICENSE +21 -0
  6. tokenmaxxr-1.0.0/PKG-INFO +148 -0
  7. tokenmaxxr-1.0.0/README.md +118 -0
  8. tokenmaxxr-1.0.0/openspec/changes/archive/2026-07-22-add-quota-aggregator/.openspec.yaml +2 -0
  9. tokenmaxxr-1.0.0/openspec/changes/archive/2026-07-22-add-quota-aggregator/design.md +107 -0
  10. tokenmaxxr-1.0.0/openspec/changes/archive/2026-07-22-add-quota-aggregator/proposal.md +35 -0
  11. tokenmaxxr-1.0.0/openspec/changes/archive/2026-07-22-add-quota-aggregator/specs/cli-output/spec.md +49 -0
  12. tokenmaxxr-1.0.0/openspec/changes/archive/2026-07-22-add-quota-aggregator/specs/usage-caching/spec.md +46 -0
  13. tokenmaxxr-1.0.0/openspec/changes/archive/2026-07-22-add-quota-aggregator/specs/usage-collection/spec.md +95 -0
  14. tokenmaxxr-1.0.0/openspec/changes/archive/2026-07-22-add-quota-aggregator/specs/web-dashboard/spec.md +47 -0
  15. tokenmaxxr-1.0.0/openspec/changes/archive/2026-07-22-add-quota-aggregator/tasks.md +52 -0
  16. tokenmaxxr-1.0.0/openspec/config.yaml +20 -0
  17. tokenmaxxr-1.0.0/openspec/specs/cli-output/spec.md +53 -0
  18. tokenmaxxr-1.0.0/openspec/specs/usage-caching/spec.md +50 -0
  19. tokenmaxxr-1.0.0/openspec/specs/usage-collection/spec.md +99 -0
  20. tokenmaxxr-1.0.0/openspec/specs/web-dashboard/spec.md +37 -0
  21. tokenmaxxr-1.0.0/pyproject.toml +46 -0
  22. tokenmaxxr-1.0.0/scripts/start_server.sh +5 -0
  23. tokenmaxxr-1.0.0/scripts/stop_server.sh +3 -0
  24. tokenmaxxr-1.0.0/tests/conftest.py +8 -0
  25. tokenmaxxr-1.0.0/tests/fixtures/clinepass.html +1850 -0
  26. tokenmaxxr-1.0.0/tests/fixtures/ollama_cloud.html +12 -0
  27. tokenmaxxr-1.0.0/tests/fixtures/ollama_cloud_empty.html +12 -0
  28. tokenmaxxr-1.0.0/tests/fixtures/opencode_go.html +19 -0
  29. tokenmaxxr-1.0.0/tests/test_cache_history.py +74 -0
  30. tokenmaxxr-1.0.0/tests/test_cli.py +84 -0
  31. tokenmaxxr-1.0.0/tests/test_cookies.py +91 -0
  32. tokenmaxxr-1.0.0/tests/test_providers.py +107 -0
  33. tokenmaxxr-1.0.0/tests/test_reset.py +33 -0
  34. tokenmaxxr-1.0.0/tests/test_schema.py +29 -0
  35. tokenmaxxr-1.0.0/tests/test_serve.py +36 -0
  36. tokenmaxxr-1.0.0/tokenmaxxr/__init__.py +20 -0
  37. tokenmaxxr-1.0.0/tokenmaxxr/_webui/dist/app.js +160 -0
  38. tokenmaxxr-1.0.0/tokenmaxxr/_webui/dist/index.html +33 -0
  39. tokenmaxxr-1.0.0/tokenmaxxr/_webui/dist/styles.css +36 -0
  40. tokenmaxxr-1.0.0/tokenmaxxr/cache.py +125 -0
  41. tokenmaxxr-1.0.0/tokenmaxxr/cli.py +163 -0
  42. tokenmaxxr-1.0.0/tokenmaxxr/cookies.py +278 -0
  43. tokenmaxxr-1.0.0/tokenmaxxr/fetch.py +82 -0
  44. tokenmaxxr-1.0.0/tokenmaxxr/history.py +62 -0
  45. tokenmaxxr-1.0.0/tokenmaxxr/providers/__init__.py +21 -0
  46. tokenmaxxr-1.0.0/tokenmaxxr/providers/base.py +65 -0
  47. tokenmaxxr-1.0.0/tokenmaxxr/providers/clinepass.py +119 -0
  48. tokenmaxxr-1.0.0/tokenmaxxr/providers/ollama_cloud.py +138 -0
  49. tokenmaxxr-1.0.0/tokenmaxxr/providers/opencode_go.py +101 -0
  50. tokenmaxxr-1.0.0/tokenmaxxr/py.typed +1 -0
  51. tokenmaxxr-1.0.0/tokenmaxxr/reset.py +58 -0
  52. tokenmaxxr-1.0.0/tokenmaxxr/schema.py +71 -0
  53. tokenmaxxr-1.0.0/tokenmaxxr/serve.py +72 -0
  54. 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/`).
@@ -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,2 @@
1
+ schema: spec-driven
2
+ created: 2026-07-22
@@ -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.
@@ -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