oc2token
v0.1.24
Published
A macOS-first OpenCode 2 token usage dashboard CLI
Readme
Why OC2Token?
OpenCode 2 records rich token telemetry locally, but the raw message stream is noisy and per-provider. OC2Token is a single, fast, terminal-native console that:
- aggregates all three providers (
opencode,codex,antigravity) into one unified report - shows four exact windows — last 60 minutes · today · this week · last month (a rolling 30 days) — with local-day and ISO-week semantics
- visualises a sparkline trend, per-model / per-provider / per-project breakdowns, and coverage status without ever persisting prompts or keys
- stays instantly usable over pipes (
--json,--format table) for CI and scripting
recorded_total = input + output + reasoning + cacheRead + cacheWrite— an explicit accounting sum, not a billing or quota total.
✨ Highlights
- Unified multi-provider — one snapshot, one schema, three colours (purple · orange · cyan)
- Four exact windows rendered responsively, from one row down to 20-column terminals
- CodeBurn-inspired palette — violet structure, orange focus, cyan inputs;
--no-color/NO_COLORsafe - Trend sparkline per window, not one row per bucket
- Privacy by design — only normalized token counters are cached; no prompts, tool inputs, or session titles are stored
- Resilient collection — paginated assistant-message aggregation with retry, dedup by
tokenRevision, fallback when the filtered stats API ignores the range - Scriptable — stable JSON
schemaVersion: 4, deterministic table output, exit codes0/1/3
📸 Preview
Dashboard — day (100 cols) Four cards + inline provider stack + trend
Settings overlay — s
Toggle providers, scrub refresh interval
Projects — p
Per-project totals scoped to the selected window
Narrow — 60 cols Stacks vertically, never wraps
| View | Screenshot |
|------|------------|
| Hour (1 / oc2token hour) | |
| Help (
?) | |
| Table pipe (
--format table) | |
📦 Installation
npm / npx (recommended)
npm install -g oc2token
# or without installing
npx oc2tokenRequires Node.js ≥ 20. The OpenCode 2 local service must be reachable; OC2Token talks to it via @opencode-ai/client (beta API subject to change).
From source
git clone https://github.com/kaanyaren/OC2Token.git
cd OC2Token
npm install
npm run build
npm link # or: node dist/src/cli.js --helpVerify
oc2token doctor # human-readable health checks
oc2token doctor --json # machine-readable
oc2token --help🚀 Quick start
oc2token # interactive dashboard, refreshes every 5 min
oc2token hour # start on the rolling last-60-minutes card
oc2token day # start on today's local-calendar day
oc2token week # start on the ISO week (Mon–Sun)
oc2token --once --json # one-shot JSON for scripting
oc2token --format table # deterministic plain table (great for pipes)
oc2token --refresh 0 # manual-only; press r to refresh
oc2token --timezone Europe/Istanbul # override the local zone
oc2token --no-color # or NO_COLOR=1 for plain output🎮 Interactive dashboard
| Key / Click | Action |
|-------------|--------|
| r / R · click Refresh | Refresh now |
| 1 / 2 / 3 / 4 · click top card, digit, or period name | Select the visible card in that slot |
| Tab / ← → / ↑ ↓ | Cycle visible cards (or breakdown tables when Settings is closed) |
| Space · left/right-click a table header | Expand/collapse a breakdown table |
| p · click Projects | Projects panel (click again to close) |
| s · click Settings, a provider row, a table row, a card slot, or the refresh slider | Settings — toggle providers, show/hide Providers & Projects tables, choose top cards, adjust refresh |
| ? · click Help | Help |
| q / Ctrl+C · click Quit | Quit |
Settings persist under the cache directory (~/Library/Caches/oc2token on macOS by default; $XDG_CACHE_HOME/oc2token when XDG_CACHE_HOME is set, otherwise ~/Library/Caches/oc2token — see src/application.ts:47-50). Override with --cache-dir. At least one provider must stay enabled.
Top cards. Four or six cards are shown; the Settings panel chooses how many (Cards shown) and which windows occupy the slots (hour, day, week, month by default, plus dashboard-only 15m, 30m, 2h, 5h). The same card may occupy two slots. Six-card mode is fixed duration-ascending (30m, hour, 2h, 5h, day, Month); switching back to four restores your remembered four. Hour trends use 30-second buckets, week trends 3-hour buckets, and month trends 3-hour buckets, so the 15-minute graph shows 30 bars, the week graph 56, and the month graph 240. The 2-hour card uses dedicated 75-second range buckets (96 bars) collected alongside the day window, falling back to sliced day buckets on old caches; the 5-hour card slices sixty 5-minute day buckets. The short ranges are derived from already-collected data and never change collection totals, JSON, table, or CLI output: a card with no evidence in range shows n/a. Short-range model/provider splits combine per-bucket stats evidence with record evidence; project splits combine per-project stats calls for that exact range with records.
Theme. Purple carries structure, orange carries activity and focus, cyan carries inputs. Respects NO_COLOR and --no-color.
Help. ? opens a keymap panel above the footer (collapse, card slots, mouse actions, settings) instead of echoing the footer. ? again or Esc closes it.
Height. In an interactive terminal the status/footer block pins to the bottom row, using the full terminal height; cards, trend, and tables stay top-anchored. Pipes, tests, and --once output stay packed as before.
Collapsible tables. Breakdown tables with more than 3 rows start collapsed, showing the top 3 plus a +N more · click to expand hint. Left-click or right-click the table header (or Tab to focus a table, then Space) to expand/collapse. Collapse state resets to collapsed on each refresh.
🖥️ CLI reference
oc2token 0.1.4
Usage:
oc2token [hour|day|week]
oc2token [options]
oc2token doctor [--source <provider>] [--json]
Options:
--once Collect once and exit
--json Emit stable JSON and exit
--format <mode> auto, dashboard, table, or json
--refresh <seconds> Auto-refresh cadence; 0 is manual-only (0 or 60–14400)
--timezone <IANA> Zone for local day / ISO week boundaries
--project <id> Restrict collection to an OpenCode project
--cache-dir <path> Override the normalized metadata cache directory
--source <provider> Filter providers: opencode, codex, antigravity, or all (repeatable)
--no-color Disable ANSI colors
-h, --help Show help
-v, --version Show versionExit codes
| Code | Meaning |
|------|---------|
| 0 | Success, complete coverage |
| 1 | Connection / validation / usage error |
| 3 | Partial results — JSON emitted but coverage incomplete |
💰 Costs (estimates only)
costs, costsByProvider, costsByProject, and per-breakdown cost fields are rough USD estimates, not bills. Pricing is compiled from public provider pages as of 2026-09-02 (src/pricing/pricing.ts:11-16); rates change without notice and subscription/Zen at-cost pricing may differ from what you actually pay.
- Unknown models return
undefinedpricing (pricingForModel→undefined), surfaced asnullcost in JSON. There is no generic fallback price — an unknown model contributes tokens but no dollars. estimatedCostForBreakdowns(breakdowns, strict=true)returnsundefinedif any model is unknown; the non-strict/partial variant skips unknown models instead. Window-levelcostsuse the partial (skip-unknown) aggregation.recorded_totalremains the source of truth for usage; costs are a convenience overlay.
Filtering providers
oc2token --source codex # only Codex
oc2token --source opencode --source codex # two providers
oc2token --source all # reset filter (default)
oc2token doctor --source antigravity --jsonCombine with --once / --json for scripting:
oc2token --once --json --source codex | jq .totals.day.recorded_total🔌 Providers
| Provider | Source | Discovery |
|----------|--------|-----------|
| opencode | OpenCode 2 local service (@opencode-ai/client) | session/message pagination with tokenRevision dedup |
| codex | ~/.codex/sessions rollouts (JSONL) | file scan + parse |
| antigravity | Antigravity SQLite (gen_metadata protobuf via node:sqlite + src/antigravity/proto.ts) | proto + scanner |
All three emit the same UsageRecord shape and are merged by Aggregator → CachedUsageSource → Dashboard. doctor reports each independently:
oc2token doctor
# oc2token doctor: OK
# ✓ opencode: OpenCode 2 service OK
# ✓ codex: 14 sessions scanned
# ✓ antigravity: 3 projects indexed📊 Output formats
Table (pipes & logs)
No ANSI, deterministic — safe for grep, awk, less:
oc2token --format tableJSON (schemaVersion: 4)
oc2token --json | jq .
oc2token --once --json > snapshot.jsonEmits all four windows, costs, totalsByProvider/totalsByProject, trends, and coverage in one stable contract:
{
"schemaVersion": 4,
"source": "unified",
"version": "0.1.4",
"windows": { "hour": { "kind": "hour", "from": "…", "to": "…", "label": "last 60 minutes" }, "day": {}, "week": {}, "month": {} },
"totals": { "hour": { "recorded_total": 15582 }, "day": {}, "week": {}, "month": {} },
"costs": { "hour": 0.042, "day": null, "week": null, "month": null },
"trends": { "hour": [{ "label": "…", "totals": {}, "from": "…", "to": "…" }], "day": [], "week": [], "month": [] },
"providersByWindow": { "day": [ { "name": "opencode", "totals": {}, "cost": 0.01 } ] },
"totalsByProvider": { "day": { "opencode": { "recorded_total": 21785 } } },
"totalsByProject": { "day": { "/Users/you/project": { "recorded_total": 9001 } } },
"coverage": { "complete": true, "sessionsScanned": 14, "sessionsDiscovered": 14 }
}
costs/costfields are estimates only (see Costs). Unknown models yieldnull/undefinedcost, never a guessed price.totalsByProjectkeys are absolute project paths — treat snapshots as containing PII if you share them.
Use --format json for pretty-printed JSON without exiting interactive mode fallback.
⚙️ Configuration
| Option | CLI | Env / Persisted |
|--------|-----|-----------------|
| Timezone | --timezone Europe/Berlin | Local system zone by default; validated as IANA |
| Refresh | --refresh 300 (0 manual-only, or 60–14400) | Persisted via Settings panel (s) |
| Cache dir | --cache-dir /tmp/oc2 | ~/Library/Caches/oc2token, or $XDG_CACHE_HOME/oc2token when set |
| Project filter | --project my-project | Scopes OpenCode collection only |
| Providers | --source codex | Persisted via Settings panel |
| Color | --no-color | NO_COLOR=1 disables all ANSI |
A corrupt or future cache is ignored — the live service is always the authority; the cache is an optimization.
🔒 Privacy
OC2Token stores only normalized usage metadata:
input,output,reasoning,cacheRead,cacheWrite,recorded_total, model / provider / project names, timestamps
It does not persist:
- prompts, tool input/output, API keys, session titles, message bodies
All collection is local-first; no network calls besides the loopback OpenCode service.
Paths are PII
project is persisted as an absolute filesystem path (e.g. /Users/you/work/client-app) in the normalized cache, in --json output (totalsByProject, projects*), and in table/dashboard project rows. Sharing a snapshot, cache directory, or screenshot exposes your directory layout and username. There is no --redact-projects flag — if you need to share output, scrub paths manually before posting.
Snapshot sanitizer is denylist-based
src/cache/schema.ts:47,225-259 strips known-sensitive keys (prompt, tool_input/output, api_key, authorization, password, secret, session_title/name, raw_content, plus content/parts/title) from cached snapshots. This is a denylist, not an allowlist: a future field with a novel sensitive name would pass through. Treat the cache as usage-metadata-only by construction (records go through normalizeRecord), and review sanitizer keys when new snapshot fields are added.
doctor prints the endpoint
oc2token doctor prints Endpoint: <url> (loopback server URL) and per-provider status lines. The URL itself is not a credential (auth headers are never printed), but redact it in public bug reports anyway.
🧭 How it works
┌─────────────┐ ┌─────────────┐ ┌──────────────┐
│ opencode │ │ codex │ │ antigravity │
│ transport │ │ scanner │ │ scanner │
└──────┬──────┘ └──────┬──────┘ └──────┬───────┘
│ │ │
└────────────────┼────────────────┘
▼
┌─────────────┐
│ Aggregator │ dedup by sessionID/messageID + tokenRevision
└──────┬──────┘
▼
┌──────────────┐ file lock · schema 2 · atomic writes
│ CachedUsage │
└──────┬──────┘
▼
┌───────────────────┐
│ dashboard / json │ normalize → renderDashboard | renderTable | renderJSON
└───────────────────┘- Windows are derived with
Intl.DateTimeFormat— including DST-aware local-day resolution. - Filtered stats responses are range-validated; if the service ignores the range, OC2Token falls back to full pagination rather than mislabelling all-time usage as filtered.
🛠️ Development
npm install
npm test # build + node --test
npm run typecheck
npm run pack:check # dry-run npm pack
npm run buildProject layout:
src/
accounting/ reducer for normalized records
collector/ parallel/serial message collection
opencode/ transport & doctor
codex/ rollout discovery & scanner
antigravity/ sqlite + proto + scanner
cache/ filesystem lock & store (schema 2)
dashboard/ render, state, settings
domain/ windows, tokens, records, contracts
output/ dashboard / table / json renderersTesting. The suite uses node:test with a serial oracle vs parallel aggregator equivalence check, ANSI-gating invariants, and window-boundary coverage.
🤝 Contributing
Contributions are welcome — bug reports, new provider adapters, render polish, docs.
- Fork & create a feature branch
- Add tests for new behaviour (
npm test) - Run
npm run typecheck - Open a PR with a clear description and screenshots for UI changes
Please read the MIT License. By contributing you agree to license your contributions under the same terms.
Be kind. This project follows the spirit of the Contributor Covenant.
🙏 Acknowledgments
- OpenCode 2 for the local token telemetry API
- The CodeBurn palette inspiration (violet · orange · cyan)
- Built with TypeScript,
node:test, and a lot of terminal love
📄 License
MIT © 2026 Kaan Yaren
See LICENSE for details.
