Metadata-Version: 2.4
Name: agemetry
Version: 0.0.1
Summary: A ticker plant for coding agents: local-first, append-only telemetry store for Claude Code, Codex and friends.
Author: Josh Reid
License-Expression: Apache-2.0
Project-URL: Repository, https://github.com/Jwrno/agemetry
Requires-Python: >=3.10
Description-Content-Type: text/markdown
License-File: LICENSE
License-File: NOTICE
Requires-Dist: duckdb>=1.1
Requires-Dist: pytz
Provides-Extra: dev
Requires-Dist: pytest; extra == "dev"
Provides-Extra: mcp
Requires-Dist: mcp<3,>=2; extra == "mcp"
Requires-Dist: duckdb>=1.2; extra == "mcp"
Provides-Extra: replay
Requires-Dist: anthropic>=1; extra == "replay"
Dynamic: license-file

# agemetry

**A ticker plant for coding agents.** Local-first, append-only telemetry store for
Claude Code, Codex, GitHub Copilot (CLI and VS Code), Cursor and (soon) every
other coding agent — with one schema and one SQL view over all of them.

```
feed handlers        tickerplant log            HDB                     query
──────────────       ────────────────           ───────────────         ──────────────
Claude Code hooks ─┐
Codex rollouts    ─┤
Copilot sessions  ─┼─▶  ~/.agemetry/log/   ─▶  ~/.agemetry/hdb/  ─▶  DuckDB `events`
VS Code chat      ─┤
Cursor            ─┘    YYYY-MM-DD.jsonl         date=YYYY-MM-DD/        canned queries,
                        (today, stdlib-only)     events.parquet (zstd)   MCP, replay
```

Your agent logs rot in `~/.codex/sessions`, `~/.claude/projects`, `~/.copilot`,
VS Code's and Cursor's storage. Nothing
compacts, indexes, or joins them. agemetry treats agent activity as what it
is — a high-rate, timestamped, append-only event stream — and stores it the way
market data is stored: a hot log for today, columnar partitions for history,
and a single query surface over both.

Nothing leaves your machine. Credentials that show up in tool output (API keys,
tokens, private keys, passwords in URLs and config) are replaced with
`[REDACTED:<kind>]` before anything is stored. That covers recognisable
formats, not every possible secret, so `--redact` stays the rule before sharing.

Data stored before scrubbing existed can be cleaned with `agemetry scrub
--rewrite`. It rewrites the stored files with each credential replaced and
nothing else changed: same rows, ids and timestamps. It's the one exception to
append-only. The agents' own transcripts aren't agemetry's to edit, so rotate
any credential that leaked.

## Install

One line, with [uv](https://docs.astral.sh/uv/) (or `pipx install` with the same argument):

```bash
uv tool install "agemetry[mcp]"
agemetry install          # Claude Code hooks for every session
agemetry doctor           # checks hooks, adapters, MCP, billing and the store
```

`uv tool upgrade agemetry` updates it. From a clone: `pip install -e ".[dev,mcp]"`.

## Quick start

```bash
agemetry install          # hooks for every session, in ~/.claude/settings.json (idempotent)
agemetry doctor           # is everything wired up and is data arriving?
agemetry scrub            # credentials stored before scrubbing existed (--rewrite removes them)
agemetry ingest           # one-off backfill of Claude Code transcripts (and Codex, which every command also reads)
agemetry q cost           # cost & tokens by day / provider / model
agemetry q tools          # failure rate and p50/p95 latency per tool
agemetry q retries        # same tool on same file 3+ times = thrash hotspot
agemetry q sessions       # roll-up per session
agemetry q context --session <id>   # context-pressure timeline
agemetry sql "SELECT ..." # anything, over the `events` view
agemetry report           # one HTML page over everything, opened in your browser
agemetry top              # live view of what your agents are doing right now (q quits)
agemetry compact          # closed days' JSONL -> Parquet (also done before every command)
```

No cron or timer is needed: every command, and every MCP tool call, first
drains the Claude Code spool, reads new Codex rollout lines and compacts
closed days.

## The schema is the product

One wide, flat, nullable table. Provider-specific junk goes in `raw` (JSON) until
two providers need a column. See `agemetry/schema.py`.

| column | notes |
|---|---|
| `ts`, `seq` | µs UTC; `seq` breaks ties |
| `event_id` | derived from the source (spool line, rollout line, API message id); re-reading a source after a crash gives the same id, and the `events` view and compaction keep one row per id |
| `provider`, `session_id`, `parent_session_id` | subagents link to parents |
| `event_type` | `session_start/end · prompt · assistant · tool_call · tool_result · token_usage · diff · permission · error · subagent_start/end · notification` |
| `model`, `tool`, `tool_call_id` | `tool_call_id` joins call ↔ result ↔ duration |
| `repo`, `branch`, `cwd`, `file`, `host`, `user` | the "symbols" you group by |
| `tokens_in/out/cache_read/cache_write`, `cost_usd` | from transcripts / token_count events |
| `duration_ms`, `lines_added/removed`, `ok` | tool outcome |
| `text`, `raw` | truncated human text; full payload for replay |

## Adapters

* **Claude Code** — hooks push events live, and `agemetry ingest claude`
  backfills token usage from existing transcripts. Details below.
* **Codex** — polls `~/.codex/sessions/**/rollout-*.jsonl` from saved offsets
  (checked against Codex CLI 0.166.0). `exec` tool calls carry the shell command
  they ran; each command is also a `shell` tool_result with its exit code. Model
  comes from `turn_context`, branch from `.git`. Injected context
  (`<environment_context>`, developer instructions) is a `notification`, not a
  prompt. Token counts are normalised to Claude Code's meaning: `tokens_in`
  excludes cached tokens. Codex logs no cost; `cost_usd` is priced from `prices.json`.
  Unknown lines are kept as `notification` with `raw` intact.
* **GitHub Copilot** (`provider = 'copilot'`), two sources; `session_start.text`
  says which client:
  * **Copilot CLI and VS Code's agent host** (its `client_name`, e.g. `vscode-agent-host`) —
    tails `~/.copilot/session-state/<id>/events.jsonl`; repo and branch from its
    `workspace.yaml`. Tool calls with success and duration, permission prompts,
    `apply_patch` line counts. Copilot reports usage in AI units; one is a $0.01
    AI Credit, so `cost_usd` is Copilot's own figure, not a price-table guess.
    Token totals arrive at session end.
  * **VS Code chat sessions** (`vscode-chat`) — the files VS Code wrote before
    its agent host, under `workspaceStorage/*/chatSessions/`. Each turn is read
    once it finishes. Model is the one Copilot actually routed to. Terminal
    commands carry exit codes and durations; other tools have no success flag
    in these files. Tokens have no cache split, `cost_usd` comes from
    `copilotCredits` where recorded, and branch is unknown.

* **Cursor** — reads agent and chat sessions from Cursor's own database
  (`globalStorage/state.vscdb`), from a copy so the running app is never
  touched, and only when it has changed. Repo from the workspace, branch from
  Cursor's tracked git repos, tool calls with their targets and outcomes
  (completed, error, rejected, cancelled), lines added and removed per session.
  Cursor keeps no token counts or costs locally (it bills on its servers), no
  tool end times and no exit codes, so those stay empty.

Every adapter keeps the original payload in `raw`, so a renamed upstream field
costs a null column, not lost data.

## Claude Code hooks

A hook runs on every tool call, so it has to be close to free. The hook does
no work beyond recording:

1. **Per tool call** (`PostToolUse`, `UserPromptSubmit`, ...): a ~60-line C
   program, `agemetry/hook/spool.c`, appends the raw payload with a
   timestamp to `~/.agemetry/spool/YYYY-MM-DD.jsonl` in a single `write()` and
   exits 0. It never parses its input. About 2 ms, against 1.3 ms for
   spawning `/usr/bin/true` and ~32 ms for a Python hook.
2. **Once per turn** (`Stop`, `SubagentStop`, `SessionEnd`): the Python hook
   drains the spool into events and tails the transcript for token usage.
3. **Before any query**, the CLI drains whatever is left.

`agemetry install` compiles the C program with the system `cc` into
`~/.agemetry/bin/`. Without a compiler (or with `--python-only`) every hook
runs the Python version instead: same results, ~32 ms per call.

| option | choices |
|---|---|
| `--scope` | `user` (default, every session on the machine), `local` (this repo, `.claude/settings.local.json`, not committed), `project` (this repo, `.claude/settings.json`, committed) |
| `--events` | `minimal` (default: one hook per tool call) or `full` (adds `PreToolUse`, `PermissionRequest`, `Notification`, `PreCompact`) |

`agemetry uninstall --scope <scope>` removes only agemetry's hooks. Hooks you
wrote yourself are left alone. Windows currently gets the Python hook.

## Billing: what you actually pay

`cost_usd` on every event is the usage at **list prices**: what those tokens
would cost on the API. Many people don't pay that. Subscriptions cost the same
however much you use them, and enterprises negotiate rates. Tell agemetry your
plan and it prices everything accordingly when you read. Stored events never
change, so you can edit a plan at any time.

```bash
agemetry billing set claude_code --subscription 200 --name Max --start 2026-09-01
agemetry billing set claude_code --subscription 90 --currency GBP --fx 1.34   # priced in pounds
agemetry billing set copilot --subscription 19 --name Business
agemetry billing set codex --discount 20        # or --rate 0.8: 80% of list price
agemetry billing                                # last 30 days: list value vs paid, per provider
agemetry billing unset codex
```

A subscription is prorated over whatever range you look at: 30 days of $200
a month is $197.13. Plans live in `~/.agemetry/billing.json`.

To see every amount in your own currency, in the CLI and the report, set a
display currency. Stored data stays in dollars, and `USD` turns it back off:

```bash
agemetry billing currency GBP --fx 1.34
```

List prices are in US dollars, so a plan priced in another currency is
converted at the rate you give with `--fx` (US dollars per unit). The plan
keeps the original amount, the rate and the date it was set. agemetry
doesn't fetch rates; set the plan again to update one.

## Usage limits (recorded, not shown yet)

agemetry records subscription usage windows as usage_limit events:
- Claude's 5-hour and 7-day windows, from the figures Claude Code caches in
  `~/.claude.json`
- Codex's limits, from its rollouts
`agemetry q limits` lists the latest readings.

They are kept out of the report, `agemetry top`, the MCP tools and alerts.
Claude Code refreshes its cache only now and then, and a reading can be days
old (one on this machine was 8 days old, and both windows had reset since). A
stale "29% used" shown as current, or an alert on it, would mislead. The
history is still recorded, so this can come back once there's a live source.

## Outcomes and cost per commit

```bash
agemetry q outcomes  # sessions by outcome: PR opened, committed, edited not committed, explored
agemetry q commits   # recent commits with the agent cost behind each
```

agemetry reads `git log` (read-only) for every repo its agents worked in, found
from the sessions' working directories. A linked worktree counts as its main
repo. Commits go in their own `commits` view, not in `events`.
- **A commit's cost** is the agent work in its repo since the previous commit.
- **A session's outcome** is the strongest of these:
  - *PR opened*: a successful `gh pr create` in a shell tool
  - *committed*: its repo got a commit while it ran or within 30 minutes after
  - *edited, not committed*
  - *explored*
- **Subagent work** counts towards its parent session.

The report's Outcomes card shows sessions by outcome, and cost per commit at
list price and under your plans. Commit messages are stored, scrubbed, for
the CLI, and never put in the report.

## Live view

```bash
agemetry top          # refreshes every 3 s; q quits, r refreshes now
agemetry top --once   # one snapshot, for scripts
```

- **Header:** today's spend at list prices, in your display currency, and
  today's active time.
- **One row per session active in the last 30 minutes:** repo and branch, time
  since its last event, model calls, tool calls and failures, cost, current
  context size and last tool. Subagents sit under their parent.

## Alerts

```bash
agemetry alerts set --session-cost 20    # a session passes $20 at list prices
agemetry alerts set --daily-cost 50      # a day passes $50 at list prices
agemetry alerts set --failures 15        # a session has 15 failed tool calls
agemetry alerts                          # rules and recent alerts (0 turns a rule off)
agemetry alerts test                     # see what a notification looks like
```

Rules are checked at the end of every Claude Code turn, inside the hook that
already runs then. The check is stdlib only and uses no database. Each alert
fires once per session or day, as a local desktop notification
(macOS, or `notify-send` on Linux). Nothing is sent over the network.

## Time in use

```bash
agemetry q time      # per day and provider: active hours, agent hours, tokens, cost per active hour
```

- **Active time** is wall-clock: the stretches where events arrive no more
  than 5 minutes apart. Parallel sessions and subagents are merged, so it can
  never exceed the clock.
- **Agent time** sums each session's active time, so three agents working for
  an hour count three hours. The gap between the two is how much your agents
  ran in parallel.

## Report

```bash
agemetry report                 # writes ~/.agemetry/report.html and opens it
agemetry report --redact        # repo, branch and file names become repo-1, branch-1, file-1
agemetry report --out x.html --no-open
```

One self-contained HTML file covering:
- what you paid under your plans, next to the list-price value (see Billing below)
- active time and cost per active hour, sessions, tool calls, failure rate and tokens
- API value, tokens or active time over time, by provider
- spend by model, across providers
- spend by repo, each opening to show its branches
- tool reliability (failure rate, p50/p95 latency)
- a session timeline: context, tool calls (failures marked) and prompts on active time
- retry hotspots

A date-range filter (7/30/90 days, all time) re-scopes every number, and
`#range=30&m=time` in the URL opens on that range and measure. Every chart has a table view.
Light and dark themes are both supported. The page embeds aggregates only, never prompt or
response text, and loads nothing from the network. Use `--redact` before
sharing a screenshot.

## Team mode

```bash
agemetry team init ~/Dropbox/agemetry-team   # once per machine: the shared folder and your member name
agemetry team push                           # share your last 90 days of aggregates
agemetry team report                         # everyone in the folder, in one page
agemetry team status                         # who has pushed, and when
agemetry team demo --members 3               # synthetic teammates, to try it on your own
agemetry team demo --remove
```

The team folder is any directory everyone can reach: a synced folder or a
network share. Each member owns `members/<member>/`, replaced whole on every
push, with three small Parquet files and a `meta.json`:
- daily sessions, prompts, tool calls and failures, tokens, list-price cost and lines changed, by provider, model, repo and branch
- daily active and agent time, and what you paid under your own plans
- your commits per repo per day, and the agent cost behind them

Only aggregates leave the machine: no prompts, responses, code, tool output,
file paths, commit messages or session ids. Repo and branch names are shared,
because that's what a team report groups by; `team push --redact` replaces
them with placeholders. Only commits by your git email (`git config
user.email`, or `team init --email`) are shared, so a repo two people work in
isn't counted twice.

The team report shows paid and list-price spend, active time, sessions,
commits and failure rate per member, spend over time stacked by member, and
spend by repo and by model across the team. `team demo` builds teammates from
your own aggregates: different amounts, days, model mix and plans, renamed
repos, marked as demo on the page.

## Replay: would another model have done the same?

```bash
agemetry replay --list                            # recent Claude Code sessions
agemetry replay <session> --steps 10              # dry run: the plan and an estimated cost, nothing sent
agemetry replay <session> --model claude-sonnet-5-5 --run   # calls the Anthropic API
```

Replay rebuilds a recorded Claude Code session from its transcript and samples
its decision points (each API call the agent made). At each one it gives
another model the conversation as it stood, with the session's own system
prompt and tools, and compares that model's next action with what was
recorded:
- **same:** the same tool on the same file, command or pattern, or both stopped
- **same tool:** the same tool on a different target
- **different:** anything else

The recorded tool results stand in for running the tools, so nothing is
executed and a replay can be repeated.

How faithful a replay is:
- The system prompt, tool definitions, effort and max_tokens come from the
  transcript's own request records (Claude Code 2.1.29x and later). Decisions
  without them are skipped and counted.
- After a compaction, the replayed model sees the summary onward, as the
  original did.
- Messages are rebuilt from the main conversation. Per-turn context Claude
  Code injects outside it is not.
- The original model's thinking isn't replayed.

A dry run reads the transcript locally and sends nothing. `--run` needs
`pip install -e ".[replay]"` and an Anthropic API key or `ant auth login`. It
sends that session's content (code, file contents, command output) to Anthropic
and costs about the dry run's estimate. Prompt caching keeps it low, because
each step's request extends the previous one. Results go to
`~/.agemetry/replays/`.

## MCP server

Lets the agent ask about itself: *"what has this branch cost me?"*

```bash
pip install -e ".[mcp]"
claude mcp add agemetry -- agemetry mcp      # Claude Code
```

For Codex, add to `~/.codex/config.toml`:

```toml
[mcp_servers.agemetry]
command = "agemetry"
args = ["mcp"]
```

Tools: `branch_summary` (pass the working directory), `list_queries` /
`run_query` (the canned queries above), `describe_events`, and `sql`. Every
tool is read-only. `sql` accepts one SELECT, on a DuckDB connection that can
read only `log/` and `hdb/` in the store: no other files, no network, no
extensions. Results are capped at 200 rows and 2000 characters per value.
`branch_summary` reports both the list-price value and what the branch cost
under your billing plans, shown in your display currency. A subscription is
shared out by use: the branch's list value × (paid ÷ list value) for that
provider over the same days.

## Roadmap (in order)

1. **OpenTelemetry receiver**: a local OTLP endpoint, so any agent that emits OpenTelemetry is captured without its own adapter.
2. **Report polish**: the screenshot that gets it shared. Storage is invisible; the picture isn't.
3. **Adapters**: Claude Code, Codex, Copilot (CLI, VS Code) and Cursor are checked against real sessions. Next: Gemini CLI, OpenCode.
4. **Replay**: decision replay against another model is in (above). Next: a same-model baseline, replay results in the report, and multi-step replays that let the new model's own choices play out.
5. **Team mode**: a shared team folder and team report are in (above). Next: push to S3/R2 directly.

## Development

CI runs the tests on Linux with Python 3.10 and 3.13, and with the oldest
supported duckdb (1.2.0), in one job. macOS runs weekly and on demand.

```bash
pip install -e ".[dev]"
pytest                      # end-to-end, plus the C hook under address/undefined-behaviour sanitizers
AGEMETRY_HOME=/tmp/x agemetry stats   # point at a scratch store
```

Licensed under the Apache License 2.0. See `LICENSE`.
