agent-stats is a Go CLI for inspecting local Codex usage without calling an API or uploading prompt content. Codex stores local session rollouts under ~/.codex/sessions/; those rollouts include token_count events containing token usage totals for a session.
The goal of this project is to turn those local usage events into a fast, readable terminal dashboard that shows where tokens are going over time.
Codex emits token_count events with cumulative totals similar to:
{
"type": "token_count",
"info": {
"total_token_usage": {
"input_tokens": 0,
"cached_input_tokens": 0,
"output_tokens": 0,
"reasoning_output_tokens": 0
}
}
}Because the values are cumulative within a session, agent-stats should calculate usage by taking the delta between consecutive token_count events instead of summing every event directly. Repeated token_count events with the same cumulative totals should be skipped so resumed or duplicated Codex events do not double-count usage.
- Scan
~/.codex/sessions/for local Codex session logs. - Parse JSONL rollout files without reading or displaying prompt or response text.
- Aggregate token usage by day and session.
- Track:
- input tokens
- cached input tokens
- output tokens
- reasoning output tokens
- total tokens
- Calculate prompt cache hit rate:
hit_rate = cache_read / (cache_read + cache_creation + uncached_input)
For local Codex logs, cached_input_tokens is treated as cache_read, input_tokens - cached_input_tokens is treated as uncached_input when possible, and cache_creation is currently 0 because Codex rollouts do not expose a separate cache creation field.
- Render a fast terminal graph UI for quick inspection.
- Support a quiet machine-readable mode for scripts.
Example commands:
agent-stats summary
agent-stats today
agent-stats graph
agent-stats graph --since 7d
agent-stats sessions --limit 20
agent-stats commands --limit 20
agent-stats payload
agent-stats payload <session-id>
agent-stats export --format jsonThe CLI should support a small set of useful views over the same parsed token data. Each view should make the grouping explicit so users can quickly understand whether token usage is driven by time, individual sessions, or cache behavior.
Useful views:
summary: weekly credit spend, cache hit rate, function-call totals, and budget progress.today: current-day session totals with a custom time-block token graph and time-block drilldown.sessions: usage grouped by Codex session, sorted by latest activity, with model and credits.commands: function calls grouped by command name, with call count, session count, directory count, and first/last seen timestamps.payload: payload events grouped by top-level type, payload type, and phase. When opened for a session, it shows session timing rollups and token usage per interaction.tokens: input, cached input, output, and reasoning output grouped together for direct comparison.top: highest-usage sessions grouped by total tokens.
Useful group-by combinations:
| View | Primary group | Secondary group | Useful for |
|---|---|---|---|
summary |
Monday week start | budget/cache/function calls | Understanding weekly spend and cache efficiency |
today |
session | token graph | Spotting current-day heavy sessions |
sessions |
session | token type | Finding expensive sessions |
commands |
command name | function call metadata | Finding the tools and shell commands used most often |
payload |
top-level type/payload type/phase | payload size and timing | Inspecting local Codex event shapes for later analysis |
payload <session-id> |
interaction timestamp | token usage and payload timing | Drilling into one session's interaction-level cost |
top --by total |
session | total tokens | Ranking the biggest sessions |
top --by cached |
session | cached input tokens | Seeing where cache reuse is high |
top --by output |
session | output tokens | Finding output-heavy sessions |
The graph UI should make these groupings quick to switch between, ideally using keyboard shortcuts or a compact selector rather than requiring users to re-run commands for every comparison.
The CLI should include an interactive terminal mode with a lightweight command prompt and tabbed views.
Start interactive mode:
agent-stats
agent-stats tuiInteractive mode should support colon commands for switching views:
:summary
:today
:sessions
:commands
:payload
:tokens
:top
:help
:quit
The same views should also be available as numbered tabs across the top of the UI:
1 Summary 2 Today 3 Sessions 4 Commands 5 Payload 6 Tokens 7 Top
Keyboard behavior:
- Press
:to focus the command prompt. - Type a view command such as
:todayand pressEnterto switch views. - Press
1through9to switch directly to the first nine tabs. - Press
TabandShift+Tabto move to the next or previous view, including tabs beyond9. - In
sessions, usej/kto select a session andEnterto open itspayloaddrilldown. - Press
?to show available commands. - Press
qor run:quitto exit.
The command prompt should be useful but unobtrusive. It should sit at the bottom of the screen, show validation errors inline, and keep the current view visible while entering commands.
The default experience should be snappy and terminal-native:
- Use streaming file reads where possible.
- Avoid loading prompt or response text into display structures.
- Keep aggregation simple and incremental.
- Prefer compact charts over verbose tables.
- Make the graph UI responsive on small terminal widths.
- Render token usage as labelled terminal bar graphs.
The terminal UI is built with:
github.com/charmbracelet/bubbleteafor the interactive TUI loop.github.com/charmbracelet/lipglossfor styling.
Requirements:
- Go 1.25 or newer.
Build the CLI:
go build -o bin/agent-stats ./cmd/agent-statsRun it locally:
./bin/agent-stats summaryConfigure the weekly credit budget shown in summary progress bars:
AGENT_STATS_WEEKLY_CREDIT_BUDGET=5000 ./bin/agent-stats summaryIf unset or invalid, the budget defaults to 10000 credits per week.
Install it into your GOBIN:
go install ./cmd/agent-statsRun all tests:
go test ./...Run tests with the race detector:
go test -race ./...Run tests with coverage:
go test -cover ./...Suggested test coverage:
- JSONL parsing with valid, empty, and malformed lines.
- Delta calculation for cumulative
token_countevents. - Session boundary handling.
- Aggregation by day.
- Cache hit rate calculation.
- TUI graph rendering with narrow and wide terminal sizes.
Start with a local pre-hook that formats all Go files before committing or running checks. More hooks can be added here as the project grows.
Example .githooks/pre-commit:
#!/usr/bin/env bash
set -euo pipefail
echo "Formatting Go files..."
go fmt ./...
if ! git diff --quiet; then
echo "go fmt changed files. Review and stage the formatting changes, then commit again."
exit 1
fi
go test ./...Enable it for the repository:
git config core.hooksPath .githooks
chmod +x .githooks/pre-commitagent-stats should only read token metadata. It should not print, store, export, or transmit prompt text, response text, file contents, or tool output from Codex sessions.
The useful metrics are available from token counts alone, so the CLI should keep the privacy boundary clear:
- Read local files only.
- Extract token usage fields only.
- Display aggregate usage only.
- Do not require an API key.
- Do not send data to a remote service.
The parser should walk files under ~/.codex/sessions/, decode each JSONL line, filter for type == "token_count", and extract info.total_token_usage.
For each session:
- Keep the previous cumulative token total.
- For each new
token_countevent, subtract the previous total from the current total. - Add only the positive delta to the aggregate.
- Replace the previous total with the current total.
This avoids double-counting because Codex records running totals rather than independent per-event usage.
Recent Codex logs may also include info.last_token_usage. When present, prefer last_token_usage because it already represents the per-event increment. Fall back to the cumulative delta method when last_token_usage is missing. In both cases, dedupe by comparing the cumulative total_token_usage checkpoint before counting an event.
The app should avoid reparsing all raw Codex JSONL on every startup once the history grows. The current local session directory is already large enough to justify an index:
- Session path:
~/.codex/sessions/ - Current shape observed locally: dated folders containing
rollout-*.jsonl - Current volume observed locally: about 70 JSONL files, 26 dated folders, 225 MB, and roughly 14k
token_countevents
Recommended approach:
- On first run, perform a full scan of
~/.codex/sessions/. - Extract only token metadata into a local cache.
- On later startups, stat the source files and only process new or changed bytes.
- During interactive mode, stream updates from active session files.
- On quit, flush the in-memory aggregates and file checkpoints to disk.
Store the cache under:
~/.cache/agent-stats/codex-usage.db
Use SQLite as the first storage backend. It is small, local, durable, easy to inspect, and good enough for this data size. It also avoids inventing a custom file format too early.
Suggested tables:
CREATE TABLE source_files (
path TEXT PRIMARY KEY,
size_bytes INTEGER NOT NULL,
mod_time_unix INTEGER NOT NULL,
processed_offset INTEGER NOT NULL,
session_id TEXT NOT NULL,
started_at TEXT,
last_seen_at TEXT,
last_total_input_tokens INTEGER NOT NULL DEFAULT 0,
last_total_cached_input_tokens INTEGER NOT NULL DEFAULT 0,
last_total_output_tokens INTEGER NOT NULL DEFAULT 0,
last_total_reasoning_tokens INTEGER NOT NULL DEFAULT 0,
last_total_tokens INTEGER NOT NULL DEFAULT 0
);
CREATE TABLE token_events (
id INTEGER PRIMARY KEY AUTOINCREMENT,
session_id TEXT NOT NULL,
source_path TEXT NOT NULL,
timestamp TEXT NOT NULL,
input_tokens INTEGER NOT NULL,
cached_input_tokens INTEGER NOT NULL,
output_tokens INTEGER NOT NULL,
reasoning_output_tokens INTEGER NOT NULL,
total_tokens INTEGER NOT NULL,
model_context_window INTEGER
model TEXT NOT NULL DEFAULT ''
);
CREATE TABLE command_events (
id INTEGER PRIMARY KEY AUTOINCREMENT,
session_id TEXT NOT NULL,
source_path TEXT NOT NULL,
timestamp TEXT NOT NULL,
event_type TEXT NOT NULL,
command_name TEXT NOT NULL,
session_dir TEXT NOT NULL DEFAULT ''
);
CREATE TABLE payload_events (
id INTEGER PRIMARY KEY AUTOINCREMENT,
session_id TEXT NOT NULL,
source_path TEXT NOT NULL,
timestamp TEXT NOT NULL,
top_level_type TEXT NOT NULL,
payload_type TEXT NOT NULL,
phase TEXT NOT NULL DEFAULT '',
payload_bytes INTEGER NOT NULL,
completed_at TEXT NOT NULL DEFAULT '',
duration_ms INTEGER NOT NULL DEFAULT 0,
time_to_first_token_ms INTEGER NOT NULL DEFAULT 0,
command_name TEXT NOT NULL DEFAULT '',
normalized_command TEXT NOT NULL DEFAULT '',
call_id TEXT NOT NULL DEFAULT '',
input_tokens INTEGER NOT NULL DEFAULT 0,
cached_input_tokens INTEGER NOT NULL DEFAULT 0,
output_tokens INTEGER NOT NULL DEFAULT 0,
reasoning_output_tokens INTEGER NOT NULL DEFAULT 0,
total_tokens INTEGER NOT NULL DEFAULT 0,
model_context_window INTEGER NOT NULL DEFAULT 0,
model TEXT NOT NULL DEFAULT '',
payload_json TEXT NOT NULL DEFAULT '',
raw_json TEXT NOT NULL DEFAULT ''
);
CREATE TABLE model_credit_rates (
model TEXT PRIMARY KEY,
input_credits_per_million REAL NOT NULL,
cached_input_credits_per_million REAL NOT NULL,
output_credits_per_million REAL NOT NULL,
reasoning_credits_per_million REAL NOT NULL
);
CREATE INDEX token_events_timestamp_idx ON token_events(timestamp);
CREATE INDEX token_events_session_idx ON token_events(session_id);
CREATE INDEX command_events_command_name_idx ON command_events(command_name);
CREATE INDEX payload_events_type_idx ON payload_events(top_level_type, payload_type, phase);The token_events table should store per-event increments, not cumulative totals. That keeps every view simple:
summary: group token and command totals by Monday week start, such as2026 May 18th.today: group the current day's token totals bysession_id, render00:00-08:00,08:00-18:00, and18:00-00:00graph buckets, and allow drilling into a time-block-filtered sessions view.sessions: group bysession_id.commands: groupcommand_eventsbycommand_name.payload: grouppayload_eventsby event shape, or by token-count interaction within one session.credits: join token rows tomodel_credit_ratesby model and apply the per-million-token relationship.
Startup algorithm:
- Open the SQLite cache.
- Walk
~/.codex/sessions/**/*.jsonl. - For each file, compare path, size, and modified time with
source_files. - If unchanged, skip it.
- If new, parse from byte offset
0. - If changed and larger, seek to
processed_offsetand parse only appended lines. - If changed and smaller, delete that file's cached events and reprocess from
0. - Store
response_itemfunction calls incommand_eventswith command name, timestamp, session, and session directory. - Store every JSONL payload in
payload_events, including raw payload JSON, payload length, timing fields, token usage fortoken_count, model, and normalized shell command names. - Seed
model_credit_ratesfor known Codex models. The table is local and editable if Codex changes credit accounting. - Update
source_filesin the same transaction as inserted events.
Interactive streaming algorithm:
- Load the SQLite aggregates into memory on startup.
- Identify active files as files modified recently or files whose size changed during startup.
- Poll active files on a short interval, such as 1 second.
- Read from the saved
processed_offset. - Buffer partial trailing lines until the next poll.
- Insert new token events and update the visible aggregates.
This gives the app a fast startup path while still staying live during a Codex session. For the current local data size, a full Go scan at startup would probably be acceptable, but the indexed approach will feel better once historical logs become hundreds of megabytes or more.
Implementation notes that changed or clarified the initial plan:
- The default cache path uses Go's
os.UserCacheDir(). On macOS this resolves under~/Library/Caches/agent-stats/instead of the originally documented~/.cache/agent-stats/. - The module currently targets Go 1.25 because that is the local toolchain used to resolve and verify dependencies.
- Current Codex logs include
payload.info.last_token_usage, so the implementation prefers that over cumulative delta calculation. Cumulative delta fallback is still implemented for older log shapes. - Incremental byte-offset parsing stores the last cumulative
total_token_usagecheckpoint per file so appended Codex events can be deduped safely across restarts. - The pre-hook uses
go fmt ./...instead ofgofmt -w .so it does not traverse local build/module caches or unrelated hidden directories. - The first implementation uses polling for active files rather than filesystem notifications. This keeps the dependency surface smaller and is fast enough for the current data volume.
- The non-interactive
graphcommand currently maps to thetodaygraph view. A dedicated--sincefilter still needs to be added.
Implementation details are based on the local Codex logging behavior described in: