A personal, all-time token timeline for Codex / Claude Code / Cursor / One API, backed by the existing multi-Mac collector. The headline, cache disclosure, Skyline, and README cards are derived from public/usage.json on every build.
Live site: https://brickerp.github.io/ai-usage-report/
Supports multiple Macs: each machine writes public/machines/<id>.json; publish merges them (SUM for local tools; Cursor from account API).
git clone https://github.com/BrickerP/ai-usage-report.git
cd ai-usage-report
npm install
# Dev UI against public/usage.json
npm run dev
# Refresh data from this machine (needs ccusage + Cursor session)
export AI_USAGE_MACHINE_ID=mac-home # unique per Mac
export AI_USAGE_TIMEZONE=Asia/Shanghai
npm run collect
# Build the static site and both README SVG cards into docs/
npm run build
npm run previewThe build emits fixed light and dark SVG files instead of running a dynamic card service. A GitHub Profile README can link the card to the full Chronicle:
<a href="https://brickerp.github.io/ai-usage-report/">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="https://brickerp.github.io/ai-usage-report/ai-usage-card-dark.svg">
<img alt="BrickerP AI Usage Chronicle with recorded token history and a weekly skyline" src="https://brickerp.github.io/ai-usage-report/ai-usage-card-light.svg" width="560">
</picture>
</a>The cards intentionally show only the dynamically computed recorded-token total, date span, cache disclosure, and a weekly stacked Skyline. Spend, models, machines, rankings, and runtime theme endpoints stay on the full page or out of the public card.
On each Mac, set a different machine id (launchd / shell profile):
export AI_USAGE_MACHINE_ID=mac-home # other Mac: mac-office
export AI_USAGE_TIMEZONE=Asia/ShanghaiThen either:
bash scripts/publish.sh
# or
npm run publish:pagesWhat happens:
- Collect this Mac into the durable ledger
public/machines/<your-id>.json- First run on a machine: seed full local history once
- Later runs: re-read
mutable_fromthrough today, then keep yesterday + today open - If the Mac was off for days, the old boundary remains and every missed day is recovered
- A lower source snapshot is never allowed to overwrite the stored high-water value
git pull --rebase— pick up the other Mac’s fragment and latest account snapshots- After the pull, fetch and atomically cache a complete five-calendar-day One API account snapshot. Retain only residual non-GPT/Codex and non-Claude models; preserve the complete prior model timeline outside that window
- Fetch Cursor Dashboard API (account-level; it has its own
cursor_mutable_fromboundary) - SUM local tools across all fragments and reconcile each account-level source by
its latest complete snapshot → write
public/usage.json - Build + commit + push
- If push races the other Mac: pull → re-merge → rebuild → push again
usage.json.source_status records refresh health separately from the aggregate
generated_at. A failed account-level refresh keeps the last known good values,
but the page shows the affected source as stale or failed instead of implying that
every source was refreshed successfully.
Same launchd schedule on both Macs is fine; a push collision just triggers re-merge.
| Tool | Rule |
|---|---|
| Codex / Claude Code | Per-machine durable fragment; dates before mutable_from are frozen, the open window is re-collected; report = SUM across machines/*.json |
| Cursor | Account-level; dates before cursor_mutable_from are frozen, the open window is refreshed; do not SUM across Macs |
| One API | Account-level residual gateway series; collect after pull, exclude GPT/Codex and Claude model families, replace only a complete five-day fetch window, and keep prior history on missing/expired state or incomplete pagination. Local Comate history is retained here only for dates without gateway coverage |
| Ducc | Claude wrapper → counted under Claude Code |
The non-overlap rule assumes Cursor reports its own cloud usage and
use_openai_key=false. This is the currently verified account configuration.
If Cursor is later pointed at a custom OpenAI-compatible gateway, revalidate the
boundary before combining Cursor and One API totals.
One-time migration: public/machines/legacy-other-mac.json holds pre-multi-mac Codex/Claude history from the old single-Mac usage.json. After the other Mac publishes its own machines/<id>.json, delete the legacy file and re-publish once.
--force-reseed rebuilds a machine fragment from scratch (breaks freeze — only use if you intentionally want to replace that Mac’s ledger).
publish.sh:
--skip-collect— rebuild UI only--skip-push— local build only--backfill-codex-cache— one-time migration for this Mac's frozen Codex cache-read breakdown
Collector (npm run collect / Python):
--machine-id/AI_USAGE_MACHINE_ID--machines-dir(defaultpublic/machines)--no-merge— write this Mac’s fragment only--merge-only— re-merge existing fragments + Cursor API (used after push conflict)--collect-local-only— atomically capture this Mac before any GitHub/build work--today— print today’s table--backfill-codex-cache— derive frozen cache read astotal - input - output--dry-run— validate that migration without writing files
Older fragments stored Codex totals correctly but wrote codex_cache_read=0 because
the collector used the legacy cachedInputTokens field instead of ccusage's current
cacheReadTokens. The migration reconstructs the cache value from the same frozen
snapshot (codex_tokens - codex_input - codex_output). It changes no totals, costs,
dates, or Claude/Cursor fields and does not read mutable historical session logs.
Run it once on each Mac with that Mac's existing unique machine id. Publish Macs strictly one at a time: do not start Mac B until Mac A reports a successful push. The publish command also has a generated-file-only reconciliation path if two Macs race accidentally; it preserves each machine fragment and rebuilds the shared aggregate.
export AI_USAGE_MACHINE_ID=mac-home
export AI_USAGE_TIMEZONE=Asia/Shanghai
# Validate first; does not write or publish.
python3 scripts/usage_pipeline.py \
--json-out public/usage.json \
--machines-dir public/machines \
--machine-id "$AI_USAGE_MACHINE_ID" \
--backfill-codex-cache \
--dry-run
# Pull, migrate this machine fragment, rebuild, commit, and push.
bash scripts/publish.sh --backfill-codex-cacheThe migration fails closed on missing components, duplicate dates, negative derived
cache, or an existing nonzero cache value that conflicts with the frozen snapshot.
Do not use --force-reseed for this migration.
| Path | Role |
|---|---|
src/ |
Astryx + React report UI |
public/usage.json |
Merged daily series for the UI |
public/ai-usage-card-{light,dark}.svg |
Generated static GitHub README cards |
public/machines/<id>.json |
Per-Mac Codex/Claude fragment plus one-time local Comate history |
scripts/generate_readme_cards.mjs |
Dynamic summary + weekly Skyline SVG generator |
scripts/usage_pipeline.py |
Collect + merge |
scripts/ai_usage_comparison_image.py |
Compatibility CLI for the former collector name |
scripts/model_prices.v1.json |
Versioned Codex/Claude estimate ledger |
scripts/cursor_usage_api.py |
Cursor session, API, redaction, and event normalization library |
scripts/cursor_usage_api_probe.py |
Manual Cursor diagnostic export CLI |
scripts/oneapi_usage.py |
One API browser collection, model ownership filter, and aggregation |
scripts/comate_usage.py |
Local Comate session parser |
scripts/machine_fragments.py |
Fragment I/O + SUM merge |
scripts/legacy_report_renderer.py |
Legacy --out / --png report rendering |
scripts/publish.sh |
Pull → collect → build → push (+ re-merge on conflict) |
docs/ |
Built GitHub Pages output |
- Codex / Claude Code:
npx ccusage@latest … --json --offline - Cursor: authenticated Dashboard API via local Cursor session
- Historical Comate:
~/.comate-engine/store/chat_session_*(positivecontextUseddeltas; cost always 0), migrated under One API only on dates without gateway coverage - One API: management log API through a saved
chrome-useUUAP session. GPT/Codex and Claude model families are excluded; other named models such as Grok, DeepSeek, GLM, Kimi, and MiniMax form the residual series. Empty model names are excluded as unclassified. Ownership matching recognizes provider prefixes separated by/,.,:,,, or\\, so names such asanthropic.claude-*,openai.gpt-*, andprovider.o3cannot leak into the residual series. Each snapshot records its account scope, timezone-aware capture time, ownership-rule version, complete calendar window, and a stable SHA-256 content id; model rows retain canonical and raw labels. - Costs: the checked-in, versioned model-price ledger for reproducible
Codex/Claude estimates; unresolved historical models retain any collector
estimate, or remain unpriced, with explicit
legacy/unpricedprovenance rather than being presented as part of the pinned ledger. Cursor uses its account API; One API quota is converted at 250,000 units/CNY and estimated at the pinned 0.14 USD/CNY rate.
Save or refresh the One API browser state after logging in manually:
chrome-use open https://oneapi-comate.baidu-int.com/log
install -d -m 700 "$HOME/Library/Application Support/ai-usage-report"
chrome-use state save "$HOME/Library/Application Support/ai-usage-report/oneapi-chrome-state.json"
chmod 600 "$HOME/Library/Application Support/ai-usage-report/oneapi-chrome-state.json"The default state location is persistent across reboots. Use
ONEAPI_STATE_PATH to choose another state file. Collection is complete-or-
nothing: rate-limit or authentication failures retain the prior published One
API series instead of writing a partial or zero snapshot. The publisher collects
this account snapshot after git pull and installs its cache with an atomic
rename, preventing an older pre-pull or half-written cache from replacing newer
remote data. Set CHROME_USE_BIN
when the executable is outside the LaunchAgent PATH; otherwise the collector
also checks ~/.local/bin/chrome-use.
Each published daily row also carries compact model breakdowns. The Explore section aggregates those rows for the selected date range; selecting a tool reveals its model series, and focusing a model updates the chart, URL, keyboard state, and accessible status together. No separate model dashboard is introduced.
Point your LaunchAgent at the bounded retry wrapper, not directly at
publish.sh:
export AI_USAGE_MACHINE_ID=mac-home
export AI_USAGE_TIMEZONE=Asia/Shanghai
bash /path/to/ai-usage-report/scripts/launchd-run.shRecommended LaunchAgent settings:
RunAtLoad = truefor login/reboot catch-up.KeepAliveabsent/false; this is a finite batch job.- Two
StartCalendarIntervalentries: shortly after midnight to close yesterday, and one daytime snapshot (for example00:05and15:03). Stagger the second Mac by several minutes to reduce avoidable push races.
publish.sh captures the machine-local fragment before Git fetch/pull, then
pulls, collects the complete One API account window, refreshes Cursor, merges,
builds, and pushes. If GitHub or the build is down, launchd-run.sh retries the
complete pipeline without losing the local
snapshot. Every run refreshes at least today + yesterday, while an older persisted
boundary expands the recovery window across any missed days. Local source logs
must still be retained until at least one later successful reconciliation; no
collector can reconstruct records that were deleted before they were ever read.
Run the LaunchAgent from a dedicated publisher clone. The publisher refuses to
switch branches or abort an in-progress Git operation. After the local snapshot
is durable but before pull/build/commit, it non-interactively reads the configured
Git credential for origin and verifies repository write access with a
no-mutation dry run to a unique probe ref. It does not infer authentication
identity from the credential username. The real push uses the ordinary Git
credential helper; the publisher does not invoke gh directly or print the
stored password.
Only generated report artifacts are staged. Source-code releases should be
committed separately from scheduled data refreshes.
Required env:
AI_USAGE_MACHINE_ID(required; use one stable, unique value per Mac)
Optional env:
AI_USAGE_TIMEZONE(defaultAsia/Shanghai)GH_PUBLISH_ACCOUNT(defaultBrickerP; commit author/config label only, not authentication identity)PUBLISH_BRANCH(defaultmain)ONEAPI_STATE_PATH(default~/Library/Application Support/ai-usage-report/oneapi-chrome-state.json)CHROME_USE_BIN(optional explicit path to thechrome-useexecutable)AI_USAGE_RETRY_ATTEMPTS/AI_USAGE_RETRY_DELAY_SECONDSAI_USAGE_JOB_RETRY_ATTEMPTS/AI_USAGE_JOB_RETRY_DELAY_SECONDS