Architecture
The repository holds two packages. @ayagmar/agent-sessions (packages/core) reads agent logs and turns them into normalized events. The llm-usage-metrics CLI (src/) stores, merges, prices, aggregates, and renders those events. The CLI imports the core package and its build bundles it, so dist/index.js carries both.
Core package
Section titled “Core package”packages/core/src only reads local files: it has no runtime dependencies and makes no network calls.
| Directory | What lives there |
|---|---|
sources/ |
One directory per source (17), the adapter contract (source-adapter.ts), parse diagnostics, fingerprints, multi-root and multi-directory discovery, JSON and SQLite readers |
registry/ |
The adapter list in registry order, createDefaultAdapters, worker eligibility, and readSources |
stream/ |
Async-iterable stages: readEvents, collect/collectFile, pipe, kinds, since/until, bySession, and read cursors |
dedup/ |
The content hash and the repeated-across-files and copied-session stages |
domain/ |
The event types and constructors, timestamp and provider normalization, active time |
utils/ |
File discovery, chunked reads, and the JSONL reader with byte and string prefilters |
otel/ |
OpenTelemetry GenAI attributes for events (experimental) |
internal/ |
Helpers the CLI uses that are not part of the public API |
The package’s exports map publishes ., ./registry, ./otel, ./internal, and ./sources/<id> for each source. Library documents the public API.
| Directory | What lives there |
|---|---|
src/cli |
Commander wiring, report definitions, the report runtime, per-report builders and runners, parse orchestration |
src/config |
config.toml loading, runtime overrides and defaults, the active-config and env-var displays |
src/persistence |
The SQLite event store: schema and migrations, ingest, history replay, prune |
src/machines |
Machine config edits, ssh export and sync, per-machine caches, merge |
src/pricing |
LiteLLM loading and cache, model-name matching, the cost engine, overrides, retired prices, cache savings, pricing explain |
src/aggregate |
Usage totals by period, source, model, and project; daily activity; project keys |
src/blocks, src/value, src/session, src/trends, src/efficiency, src/optimize, src/wrapped |
Each report’s own aggregation |
src/render |
Terminal, Markdown, and JSON output for every report, and the table primitives |
src/share-report |
The interactive HTML report: payload, redaction, HTML, and the browser client |
src/domain |
The CLI’s event type (core’s plus costMode and machine) and report row types |
src/sources |
create-default-adapters.ts, which re-exports the core registry for the CLI and the site |
src/otel-export |
OTLP/JSON for events --format otlp-traces and otlp-metrics: span tree, daily metric points, derived ids, encoding |
src/update |
The startup update notifier and its cache |
src/utils |
The logger, time buckets, config, data, and cache root directories, USD math, and re-exports of core utilities |
A report run
Section titled “A report run”Every report that reads usage builds its events through buildUsageEventDataset (src/cli/build-usage-event-dataset.ts) and prices them through applyPricingToUsageEventDataset:
- Discover. Each selected adapter lists its files.
- Look up. Each file’s fingerprint is compared with the event store. A match serves the stored events and skips steps 3 and 4.
- Read. The misses are parsed under one shared concurrency budget, on worker threads when a large claude, codex, or pi backlog justifies them.
- Store. Fresh parses replace their files’ stored events.
- Dedupe repeats. For pi, an event repeated across files counts once.
- Merge. History replays files that have left the disk (on unless
--no-history), and other machines’ cached events are merged in. A sync with other machines starts at the beginning of the run, alongside steps 1 to 5. - Dedupe copies. For claude, an event copied between sessions counts once.
- Filter. Date window, provider, model, and repo.
- Price, aggregate, render. Each report’s builder prices the events, aggregates them its own way, and hands the result to its renderer.
Parse pipeline covers steps 1 to 5, Event store steps 2, 4, and history, Dedup and cursors steps 5 and 7, Machine merge step 6, and Pricing pipeline step 9.
Report definitions and runtime
Section titled “Report definitions and runtime”src/cli/report-definitions/report-definitions.ts defines every report command, in this order: summary (the default command), hourly, daily, weekly, monthly, projects, compare, efficiency, optimize, trends, session, blocks, value, wrapped, events, pricing, statusline, doctor, and prune. A definition names the command, its shared option profile (shared-report-options.ts), and its examples. The same metadata registers the Commander command, builds the root help examples, and generates the CLI reference, so option descriptions are written once.
config (init, path, show), schema, completion, machine (add, list, remove, export), and sync are registered directly in src/cli/create-cli.ts.
src/cli/report-runtime/report-lifecycle.ts runs every report the same way: it validates the output format, builds the report data, builds the share file when --share is set, emits diagnostics on stderr, writes the share file, and prints the report on stdout.
Builders and renderers
Section titled “Builders and renderers”| Command | Builder | Renderer |
|---|---|---|
summary |
buildSummaryData |
renderSummaryReport |
hourly, daily, weekly, monthly |
buildUsageData |
renderUsageReportWithNotes |
projects |
buildProjectsData |
renderProjectsReport |
compare |
buildCompareData |
renderCompareReport |
efficiency |
buildEfficiencyData |
renderEfficiencyReportWithNotes |
optimize |
buildOptimizeData |
renderOptimizeReport |
trends |
buildTrendsData |
renderTrendsReport |
session |
buildSessionData |
renderSessionReport |
blocks |
buildBlocksData |
renderBlocksReport |
value |
buildValueData |
renderValueReport |
wrapped |
buildWrappedData |
renderWrappedReport |
pricing |
run-pricing-report.ts (reads no usage) |
renderPricingReport |
doctor, prune |
run-doctor-report.ts, run-prune-report.ts |
renderDoctorText, renderPruneReport |
events and statusline have their own runners (run-events-report.ts, run-statusline.ts): events exports the priced events as JSONL, CSV, or OTLP traces or metrics, and statusline prints one line for a status bar. --share builds its own dataset; see Share report.
aggregateUsage keeps per-model breakdowns by default; trends, optimize, and efficiency turn them off. src/render/unicode-table.ts lays out rows from explicit metadata (periodKey, and rowKind: detail, combined, or total), so sorting and separators stay deterministic without the table knowing report types.
Invariants
Section titled “Invariants”- Deterministic ordering for periods, sources, models, projects, and machines.
- Report body on stdout, diagnostics on stderr;
--jsonand--markdownoutput is data only. - Normalization happens once, in the core constructors; see Event model.
- An adapter’s events depend only on its file, its declared dependencies, and its
parserVersion, so stored events never go stale silently. - Incomplete pricing stays visible in output (
~,-, warnings, or incomplete flags).
Deep dives
Section titled “Deep dives”- Parse pipeline: how source files are discovered, filtered, parsed, and parallelized.
- Event store: the SQLite ledger: ingest, fingerprints, history, and prune.
- Pricing pipeline: how prices load, model names match, and costs are computed.
- Event model: the event union and its fields.
- Dedup and cursors: duplicate removal, fingerprints, and resumable reads.
- Machine merge: fetching and merging other machines’ usage.
- Share report: how the interactive HTML report is built.
- Config and logging: how settings resolve and how stderr is leveled.