Skip to content

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.

Package layers: the CLI on top of the core packageTwo layers. The llm-usage-metrics CLI on top owns commands and the report runtime (src/cli), config and env vars (src/config), the SQLite event store (src/persistence), other machines over ssh (src/machines), pricing (src/pricing), the per-report aggregation directories, terminal, JSON and Markdown rendering (src/render) and the interactive HTML report (src/share-report). It imports the core package @ayagmar/agent-sessions, which the CLI build bundles. The core package owns the 17 source adapters (sources), the adapter registry and readSources (registry), reading, collecting and cursors (stream), the dedup stages (dedup), the event types and constructors (domain) and OpenTelemetry GenAI attributes (otel). It has no runtime dependencies.llm-usage-metricsCLI: reads, stores, prices, reportssrc/clicommands, runtimesrc/configconfig.toml, env varssrc/persistenceSQLite event storesrc/machinesother machines (ssh)src/pricingLiteLLM rates, costssrc/aggregate, …per-report totalssrc/rendertext, JSON, Markdownsrc/share-reportinteractive HTMLimports; bundled intothe CLI build@ayagmar/agent-sessionscore: no runtime dependenciessources/17 source adaptersregistry/adapters, readSourcesstream/read, collect, cursorsdedup/hash, repeats, copiesdomain/event typesotel/GenAI attributes

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
Event flow of a report runA report run passes through eleven stages in order: discover each source's files; look each file up in the event store, where an unchanged fingerprint is a hit that skips reading; read the misses; store their fresh events; drop events repeated across one source's files; merge history from departed files and events from other machines; drop events copied between sessions; filter by date, provider, model and repo; price each event; aggregate per report; render as terminal, JSON, Markdown or HTML output. Discover, read and both dedupe stages come from the @ayagmar/agent-sessions core package; the other stages live in the CLI.core packageCLI01Discoveradapter.discoverFiles() per source02Look upevent store: same fingerprint is a hit03ReadcollectFile on misses, worker threads04Storereplace the file’s stored events05Dedupe repeatsevents repeated across files (pi)06Mergehistory replay, other machines07Dedupe copiescopied sessions (claude)08Filterdates, provider, model, repo09PriceLiteLLM rates, cost per event10Aggregaterows and totals per report11Renderterminal, JSON, Markdown, HTMLmisshit

Every report that reads usage builds its events through buildUsageEventDataset (src/cli/build-usage-event-dataset.ts) and prices them through applyPricingToUsageEventDataset:

  1. Discover. Each selected adapter lists its files.
  2. Look up. Each file’s fingerprint is compared with the event store. A match serves the stored events and skips steps 3 and 4.
  3. Read. The misses are parsed under one shared concurrency budget, on worker threads when a large claude, codex, or pi backlog justifies them.
  4. Store. Fresh parses replace their files’ stored events.
  5. Dedupe repeats. For pi, an event repeated across files counts once.
  6. 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.
  7. Dedupe copies. For claude, an event copied between sessions counts once.
  8. Filter. Date window, provider, model, and repo.
  9. 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.

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.

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.

  • Deterministic ordering for periods, sources, models, projects, and machines.
  • Report body on stdout, diagnostics on stderr; --json and --markdown output 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).