Skip to content

Latest commit

 

History

134 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

⏱ Counter

A local-first token-usage dashboard for your AI assisted coding sessions

How much have I burned in this 5-hour block? When does it reset? What am I spending it on, which models, which projects, and how has that changed over time?

Platform Swift Network Data


What it is

Counter is a native, macOS-first (iOS-ready) SwiftUI app that reads session logs your agents write to disk and turns them into a usage dashboard.

It is local : no network calls, no API keys, no accounts. It never writes to any agent's data, just reads it. Everything you configure (name, appearance, sources) lives in UserDefaults.

Originally, I just wanted to track my Claude Code usage; mapping out the 5-hour reset blocks and keeping tabs on how much time, money, and tokens I was burning through per model. But as I started flipping between different models for different projects, the tool kind of took on a life of its own.

Now, it's evolved into a (still slightly restricted) multi agent dashboard that recognises usage from Claude Code, Codex, Gemini CLI and Opencode and even surfaces local model runs through those agents.


Features

At a glance

  • The instrument row - "Session Usage" and "This Week" are drawn as flat composition strips — a big new-tokens number plus a segmented split showing new vs. cache-read share — summed across every enabled source (no budget to compare against — Anthropic doesn't expose one locally, and one cache-heavy session made a fixed-budget number meaningless anyway). Alongside them sits the one genuinely radial reading: a "Claude Block Reset" countdown dial with a two-phase fill (steady, then an amber "reset imminent" phase for the final 90 minutes). "Claude Block Reset" is the one instrument that's Claude Code only — it's counting down Anthropic's own rate-limit window, which Codex/Gemini/OpenCode don't have.

  • The header card - Grabs your display name and Claude plan tier straight from ~/.claude.json, alongside live lifetime totals (input + output + cache-creation, across every enabled source — cache-read tokens are tracked separately, see below) and estimated cost equivalents.

Observed limit

  • A measured 5-hour ceiling, not a guess. Claude Code writes every real rate-limit rejection into its own session transcript. Counter reads those (no network, no account API, nothing fetched) and reports the median new-token count that was standing in the block each time you were actually turned away — with the sample count shown honestly, and nothing rendered at all until this Mac has logged a real hit.

  • Review the samples. Settings lists every observed hit (date, tokens at limit, reset time) with a checkbox — uncheck an outlier and the ceiling recalculates immediately. Nothing is deleted; a discarded sample can be re-included any time.

  • Optional alerts. Turn on local notifications for 80%/95% of the observed ceiling and/or a new-block announcement, plus a "send test notification" button so you can confirm the permission path works without waiting to approach a real limit. Alerts stay silent until there's enough measurement to trust — a single hit, or hits that disagree too much, suppress them rather than guess — and Settings shows the live reason when they're paused.

  • The weekly cap gets the same treatment, surfaced as a footnote on the This Week gauge once a real weekly-scope rejection has been logged.

Where it all goes

  • Activity heatmap & streak - A 365-day, GitHub-style contribution heatmap plus a streak strip (current + longest), with a zoomable sparkline behind the same 7 / 30 / 90-day range picker for spotting trends.

  • Model breakdown - Shows per-model tokens and estimated costs (local models get tagged with a neat little · local label).

  • Per-project active time - This tracks real focus time by looking at the gaps between events. It caps idle periods automatically so a coffee break won't mess up your stats.

  • Agent breakdown card - This only shows up once you actually start pulling data from more than one agent, keeping the UI clean.

  • Project↔model graph - A force-directed card mapping which projects used which models, edges weighted by token volume. Caps to your top 10 projects by default, with a "Choose" picker to include specific others explicitly.

  • Vitals strip - Streaks, session count, busiest day, cost saved by caching, and the raw cache-read token count (context re-sent from cache every turn — not counted in the lifetime total above it).

  • Opt-in data archive - Turn on an append-only daily rollup in Settings so the Activity heatmap and range-picker charts keep their full history even after an agent prunes its own old session logs. See Data archive below for the file format.

  • Weekly report export - Generate a Markdown summary (optionally with a PNG chart) of the last 7 days — totals, per-project time, per-model cost, streak, busiest block — saved wherever you pick.

Project drill-down

  • You can click any project to dive into a detailed view. It pulls specific totals, project-filtered usage chart, model/agent breakdowns, and a scrollable session history.

Sessions view

  • Every session across every enabled source, sortable by any column, with a project filter and search. Selecting one opens a timeline detail pane bucketing that session's activity over wall-clock time. It's the same table the project drill-down's session history uses — one session list in the app, not two.

Insights view

  • A dedicated cost/cache-trend screen, reachable from the dashboard toolbar: a "what caching saved you" hero number, estimated cost over time stacked by model, a cache hit-rate trend line, and a cost-by-project table. Costs are clearly labeled as estimates at published API rates, not subscription billing.

Menu-bar companion

  • A MenuBarExtra companion app where the icon itself is a miniature ring, split new-vs-cache-read just like the dashboard's Session Usage gauge.

  • The dropdown displays this session's new/cache-read tokens, a live reset countdown, today's total spend, and quick buttons to open the dashboard or quit. Plus, it stays live in the background even if you close the main window.

Multi agent and local models

  • You can toggle sources on or off in Settings (if an agent isn't detected, it just displays as "not detected").

  • For sessions run against local endpoints (like Ollama via OpenCode, Qwen Code, or Codex), the tool costs them at $0 but tracks them against a cloud-equivalent reference rate. It surfaces all of this in a dedicated card so you can see exactly how much cloud spend you've avoided by running local.

User Interface

  • Built around a "Tachometer" theme using ink (#101418) and cream (#F7F5F0) surfaces, a signal-orange (#FF5C39) accent, teal (#2EC4B6) for positives, and amber (#FFB020) for warnings.

  • Supports Light, Dark, and Auto modes (defaults to Auto). Every colour is driven by the theme layer—zero hardcoded colors in the views.


Supported sources

Agent Reads from Notes
Claude Code ~/.claude/projects/*/*.jsonl, ~/.claude.json Name, plan tier, four token counts per assistant line
Codex ~/.codex/sessions, ~/.codex/archived_sessions input_tokens includes the cached portion (normalised); codex fork replays are de-duplicated
Gemini CLI ~/.gemini/tmp/<dir>/chats/session-* Cumulative token counters are turned into per-message deltas
OpenCode ~/.local/share/opencode/storage/ File-based storage only; the newer SQLite backend is not yet supported
Local models (via the agents above) Ollama writes no per-request token log, so local usage is picked up through agents pointed at its OpenAI-compatible endpoint

Data archive (opt-in)

Session logs get pruned by the agents themselves over time, so any chart wider than that retention window will eventually lose its tail. Turn on Archive daily totals in Settings and Counter keeps a tiny rollup of its own that outlives the pruned logs — the 365-day Activity heatmap and the 7/30/90-day charts read archive and live data together, so old history doesn't quietly go blank.

  • Off by default. Nothing is read or written until you enable it, and turning it back off stops Counter from touching the file at all (existing history stays on disk, but stops influencing what charts show).

  • Location: ~/Library/Application Support/Counter/archive.jsonl.

  • Format: one compact JSON object per line, no envelope:

    {"day":"2026-08-11T00:00:00Z","newTokens":123456,"estimatedCostUSD":1.23}

    day is the calendar day (midnight, ISO 8601). newTokens is input + output + cache-creation tokens for that day, across every enabled source — the same figure every other chart in the app calls "new tokens" (cache-read excluded; see below). estimatedCostUSD is the same day's total at the offline pricing table's rates. That's it — no per-event, per-project, or per-model detail, on purpose: this is a whole-app fallback for when the real logs are gone, not a second copy of them.

  • Append-only, never rewritten. Each refresh appends a fresh line for any day whose total has changed since the last line written for it — in practice, that's "today" on almost every refresh (still accumulating) and nothing else, since a finished day's total stops changing and stops being re-appended. The same day can therefore appear on more than one line; a reader collapses repeats by keeping the largest newTokens for that day, not simply the last line, so the file stays correct regardless of write order.

  • Live always wins. For any day the running agents can still see on disk, Counter uses that (full per-event precision) — the archive only fills in days that have already dropped out of live data.


How it works

CounterCore/                 ← pure Swift package (macOS 14 / iOS 17, no UI imports)
  UsageEvent.swift           ← value types: UsageEvent (+ AgentSource), AccountInfo
  SessionLogParser.swift     ← Claude JSONL parsing + message-id dedupe
  CodexSessionParser.swift   ← Codex rollouts (cache normalisation, fork gate)
  GeminiSessionParser.swift  ← Gemini chats (cumulative-counter deltas, dir resolution)
  OpenCodeParser.swift       ← OpenCode session/message join
  AgentConfig.swift          ← per-agent root paths, detection, chart color, and parse
                               dispatch registry — adding a source is one entry here
  UsageCollector.swift       ← source roots, detection, merged parseAll
  IncrementalCollector.swift ← mtime-gated reparse (only changed files) for live updates
  UsageAnalytics.swift       ← totals, by-model/project/agent, daily series, 5h blocks,
                               active time, streaks, cache efficiency, session summaries,
                               observed-limit calibration, cost/cache trend series
  LimitEventParser.swift     ← reads real 429 rejections out of Claude's own transcripts
  LimitAlerts.swift          ← pure threshold-alert decision logic (suppression, dedupe)
  ProjectModelGraph.swift    ← project↔model bipartite graph as plain, capped, ranked data
  WeeklyReport.swift         ← pure rolling-7-day summary + Markdown rendering
  Archive.swift              ← opt-in daily-rollup archive: pure rollup/diff/collapse/
                               merge logic plus the JSONL read/append I/O
  Pricing.swift              ← offline per-model $/MTok table + local-model detection
Counter/                     ← thin SwiftUI app shell (macOS)
  CounterApp.swift           ← app entry, Window + MenuBarExtra + Settings scenes
  Theme.swift                ← Tachometer palette (light+dark), type scale
  DataStore.swift            ← @Observable: scans sources off-main, refreshes, account
  FileWatcher.swift          ← FSEvents directory watcher for near-real-time refresh
  LimitNotifier.swift        ← UNUserNotificationCenter delivery for limit alerts
  WeeklyReportExporter.swift ← NSSavePanel + PNG chart glue for the weekly report
  Views/                     ← Dashboard, Sessions, Insights, ProjectDetail, MenuBar,
                               Settings, plus the shared Chart/Gauge/Card components

Design rules: all parsing and analytics are pure, deterministic functions over value types, living in CounterCore and covered by fixture-based unit tests (never against your live logs). The app target holds no parsing logic. The parser is tolerant: unknown line types are skipped, malformed lines are skipped, streaming duplicates are deduped by message id, and one bad line never aborts a file.


Build & run

Requirements: macOS 14+, Xcode 15+, and XcodeGen (brew install xcodegen). The .xcodeproj is generated and gitignored.

# regenerate the project from project.yml
xcodegen generate 
xcodebuild -scheme Counter -destination 'platform=macOS' build
# Run unit tests
swift test --package-path CounterCore

Then open Counter.xcodeproj and run, or launch the built .app.

Important

Counter is deliberately *un-sandboxed as it needs to read logs under your home directory (~/.claude, ~/.codex, ~/.gemini, ~/.local/share/opencode). It makes no network calls and only ever reads those files.

If you ever add an App Sandbox, you must add a folder-picker plus security-scoped bookmark flow at the same time.

Note

The Activity heatmap and the project↔model graph come from two local Swift packages, Spool and Filament, added as local path dependencies (sibling directories ../Spool and ../Filament relative to wherever you clone Counter). Neither is published or vendored anywhere yet, so xcodegen generate / xcodebuild will fail without both present alongside this repo.


Caveats & assumptions

There are a number of caveats and assumptions you should bear in mind. Read these before trusting a number:

  • There's no guessed plan-limit number anywhere, on purpose.

    No agent actually exposes your real plan limits on disk (and if it does i havent found a way to read that), so rather than have you guess at a budget in Settings, "Session Usage" and "This Week" just show composition instead — new tokens vs. cache-read, with no denominator to argue about. The one exception is the Observed limit card: once Claude has actually rate-limited you at least once, Counter reports the measured ceiling from that real rejection, not a number anyone typed in or guessed. Session Usage and This Week deliberately aren't repointed at it — they stay pure composition, no matter what the Observed limit card shows.

  • The 5-hour block is reconstructed.

    A block opens at the very first event after a quiet gap, floors it to the top of the hour, and stretches for 5 hours. Because of that hour-flooring, the dashboard's countdown might run up to an hour earlier than the agent’s actual reset (which typically triggers off your exact first-message time).

  • Costs reflect published API rates, not subscription billing.

    Pricing lives in a hardcoded offline table. The dashboard is strictly no-network by design. The OpenAI and Gemini rows, in particular, are best-effort guesses (feel free to add those or let me know and I'll add them).

  • Two different token totals, on purpose.

    The lifetime total, model/project/agent breakdowns, and session history all count input + output + cache-creation tokens — not cache-reads, which get re-sent from cache on nearly every turn and would otherwise dwarf everything else (in practice, often 90%+ of the raw total). That cache-read count is shown on its own in the Vitals strip instead.

    "Session Usage" and "This Week" show the same split, just scoped to the current block/week instead of all time: the big number is new tokens, and cache-read gets its own labeled number in the legend underneath. Since a single long session can rack up tens of millions of raw cache-read tokens while contributing only a few hundred thousand genuinely new ones, these two are drawn as a flat composition strip divided by that ratio (not a fraction of some budget) — the composition is the point, not a total vs. a limit. Only the reset countdown is drawn as a dial; the other two intentionally aren't, since a ratio reads more clearly off a straight strip than an arc.

  • Local models use a reference rate.

    Obviously, local usage costs $0. The "cloud value avoided" stat just prices those tokens at a Haiku-class rate to give you a rough estimate of what you're potentially saving. You may well be burning that on electrics, ram and cpu usage.

  • Per-agent quirks happen. A few things to keep in mind:

    • Codex input_tokens includes the cached portion (which I subtract out before billing), and codex fork replays parent history (which is gated out so it doesn't double-count).
    • Gemini's token fields are cumulative per session, so I turn them into deltas.
    • Finally, OpenCode’s SQLite backend isn't parsed yet, so DB-only installs will just show up as "not detected."
  • Multiple enabled sources pool together — except Claude Block Reset. Every chart, total, and breakdown sums across every source you toggle on (a combined workspace view, not a per-agent breakdown), including Session Usage and This Week — except the Claude Block Reset countdown, which only ever counts Claude Code, since it's tracking Anthropic's own rate-limit window regardless of what else is enabled. Under the hood this is one AgentScope switch (.allEnabled vs. .claudeOnly) that every aggregate in UsageAnalytics takes as a parameter, so which gauges pool and which stay Claude-only is a one-line, explicit choice rather than something you have to infer from the code around each call site.

  • Session cwd can drift. If you do a mid-run cd or rename a project folder, things can get messy. The app handles this by pinning each session to its dominant root, but it's a heuristic, not a guarantee.


Roadmap

Everything that was here before has shipped — live file tracking, the Insights view, a full Sessions view, and weekly report export are all in Features now. What's left, roughly in value order:

  • iOS companion - A WidgetKit block-gauge and iPhone dashboard fed by an iCloud snapshot the Mac app generates.

  • Multi machine view - Combining usage from more than one mac, completely de-duplicated using (sessionId & messageId). Depends on the iOS companion's export format existing first.


Project layout & conventions

  • CounterCore is the key The app itself is a shell. All testable logic lives in the package ith fixture tests.

  • Don't modify .xcodeproj manually If you add or remove files, edit project.yml instead and run xcodegen generate.


Acknowledgements

  • 5-hour block reconstruction follows the semantics popularised by ccusage.
  • The notion of showing per project session history is influend by agentview.
Built with SwiftUI · Strictly read-only, never phones home..

About

A local first token usage dashboard for your AI assisted coding sessions

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Contributors

Languages