{"id":"30dfd954-5449-477b-a1dc-243eae204789","slug":"agentrava","kind":"mcp","name":"agentrava","summary":"Strava for coding agents — turns a session into an achievement card. Measured from the transcript, not self-reported.","owner":"lukisimi","repoUrl":"https://github.com/lukisimi/agentrava","license":"MIT","licenseTier":"permissive","trust":"community","stars":4,"match":null,"fewStars":true,"format":"mcp","createdAt":"2026-09-04T12:25:42.000Z","addedAt":"2026-10-21T05:07:03.698Z","logoUrl":"https://avatars.githubusercontent.com/u/200694933?v=4","linkOnly":false,"collection":null,"skillPath":null,"categories":["developer-tools","cloud-infra"],"tags":["ai-agents","claude-code","cursor","developer-tools"],"runtime":"node","local":true,"cloudCapable":false,"cloudCapability":"unknown","cloudReadyOnPlatform":false,"needsApiKey":false,"reviewed":false,"syncedAt":"2026-10-27T21:57:17.596Z","sample":false,"url":"https://conduits.dev/tools/agentrava","language":"JavaScript","transports":["stdio"],"install":{"npm":"agentrava"},"requirements":{"env":[],"upstreamMayCharge":false},"tools":null,"readme":"# Agentrava\n\nYour agent worked hard today. It deserves kudos.\n\nStrava, for coding agents. An MCP server that turns a finished session into a\nbragging card — route map, climb profile, headline stats, badges, personal records.\n\nPart joke, part serious: **the numbers are real, the scales are made up.**\n\n<p align=\"center\">\n  <img src=\"docs/example.png\" width=\"46%\" alt=\"A single session card\">\n  <img src=\"docs/recap.png\" width=\"46%\" alt=\"A season recap card\">\n</p>\n<p align=\"center\">\n  <img src=\"docs/weekly.png\" width=\"46%\" alt=\"A Weekly Snap card\">\n  <img src=\"docs/monthly.png\" width=\"46%\" alt=\"A Monthly Snap card\">\n</p>\n<p align=\"center\"><em>A session · a season · a week · a month. All four belong to a synthetic athlete — no real usage data ships in this repo.</em></p>\n\n**Nothing on a card is self-reported.** A hook parses the session transcript for\ntool calls, tokens, diff hunks, recovered errors and moving time. The agent never\ngets to describe its own workout. Works with Claude Code, Codex CLI and Cursor.\n\n## Warm-up: install\n\n```bash\nnpx agentrava setup\n```\n\nCopies itself to `~/.agentrava/app`, registers the MCP server with Claude Code,\nadds the Stop hook, and offers to log your past sessions so the first card you\nsee is one of your own. If Codex CLI or Cursor is installed, it asks before\ntouching either. Idempotent, backs up every file it edits, and reversible:\n\n```bash\nnpx agentrava setup --manual      # keep the tools, stop logging every turn\nnpx agentrava setup --auto        # put automatic logging back\nnpx agentrava setup --codex       # register with Codex CLI without being asked\nnpx agentrava setup --yes         # accept every offer, ask nothing\nnpx agentrava setup --uninstall   # remove everything (your data is left alone)\nnpx agentrava@latest setup        # update\n```\n\nRestart Claude Code, then ask it to \"show my agentrava\". Every script below is\nalso a subcommand — `npx agentrava card --best`, `npx agentrava snap week` —\nand `npx agentrava help` lists them.\n\n**Why it copies itself:** the hook and the server are registered by absolute\npath, and `npx` runs packages from a cache npm can clear at any time. A copy in\n`~/.agentrava/app` keeps working after that. Moving between a clone and the copy\nre-points the existing hook instead of adding a second one.\n\nAny other MCP client works — it speaks stdio:\n\n```json\n{ \"mcpServers\": { \"agentrava\": { \"command\": \"npx\", \"args\": [\"-y\", \"agentrava\"] } } }\n```\n\n**From a clone**, for hacking on it: `git clone https://github.com/lukisimi/agentrava && cd agentrava && node scripts/install.mjs`.\nThe setup then registers the clone itself, and the `node scripts/…` commands in\nthe rest of this page work as written.\n\n## Tools\n\n- **`agentrava`** — start here: every tool with an example, plus your totals and streak.\n- **`snapshot`** — card for the session in progress, measured from the live transcript.\n  Takes `photo` (`\"chat\"` uses an image you just pasted), `title` to rename it inline,\n  and `share` (`full` · `friends` · `anonymous`) to strip it [before you post it](#before-you-share-one).\n- **`log_activity`** — log a session by hand; unreported fields count as zero.\n- **`recap`** — one card for a whole period: totals, activity heatmap, hour-of-day\n  histogram, trophy case, longest streak, biggest session. Optional `from` / `to`.\n- **`weekly_snap`** · **`monthly_snap`** — a week or a month on one card, same size as a session card.\n  Take `period` (`this`, `last`, `last7` / `last30`, or a date), `title`, `hide_projects`, and `pick`.\n- **`get_profile`** — career totals, streak, personal records, trophy case.\n- **`rename_session`** · **`rename_project`** · **`list_projects`** — your own names, kept across re-logs.\n- **`list_activities`** · **`leaderboard`** · **`set_athlete`**\n\n## The metaphor\n\n| Strava | Agentrava | Formula |\n|---|---|---|\n| Distance | ground covered | `churn / 100 + tool_calls / 25` km |\n| Elevation | the parts that hurt | `files × 37 + errors × 120 + tests_failed × 45` m |\n| Moving time | session time, idle excluded | gaps over 5 min don't count |\n| Pace | minutes per km | `time / distance` |\n| Suffer score | Effort, 0–100 | cadence, elevation, tokens, retries |\n| Calories | tokens burned | input + cache writes + output |\n| Economy | tokens per km | lower is leaner |\n| Gear | the model | measured, never assumed |\n| — | API cost | priced per message at list rates |\n\nEvery weight is **fitted to real sessions**, not guessed. Churn alone left the\nmedian session at 0.00 km — most sessions read and search far more than they\nwrite — which is why tool calls carry distance too.\n\n## Training science: what the numbers actually mean\n\nThe inputs are measured. The **scales are invented** — 100 lines = 1 km, an error\n= 120 m — chosen so a median session lands near a plausible 4.4 km. That makes\ncards comparable **between your own sessions**, which is what records and the\nleaderboard rest on, and meaningless outside Agentrava.\n\nMeasured across 134 sessions:\n\n| | correlates most with | r |\n|---|---|---|\n| Distance | tool calls | **0.94** |\n| Distance | churn | 0.86 |\n| Elevation | errors recovered | **0.91** |\n| Elevation | files changed | 0.88 |\n\nSo distance is *volume of activity* — 69% of it from the tool-call term — and\nelevation is *friction*, 58% of it from errors. They correlate 0.79 with each\nother: overlapping, but about a third of elevation is information distance\ndoesn't carry, which is what separates a long easy session from a short brutal one.\n\n### Not every failed tool call is friction\n\nAn error is a tool result the client flagged as failed. Sampled across 166 real\nClaude Code errors: 45% a shell command exiting non-zero, 11% bad arguments or a\nmissing file, 7% a browser step, 19% app-level errors from the user's own tools —\nand **12% nothing to do with the agent at all**: the human declining a tool call,\nthe permission layer blocking one, a model or MCP server briefly unavailable.\n\nThose last ones are counted separately (`errors_environmental`) and **left out of\nthe climb**, because 120 m of elevation and a loop in the route should mean work\nthat had to be redone, not a moment where someone hit Escape. Across this store\nthat removed 89 errors and 8 km of climb, and demoted 4 cards from Debug.\n\nThe test, in [`src/errors.js`](src/errors.js), is a phrase list run against the\n**first 300 characters** of the failure text — these failures announce themselves\nin their first line, while a long command output can mention \"rate limit\"\nanywhere. 609 *successful* Cursor tool results contain that phrase and not one of\nthem is a rate limit. Deliberately excluded from the list: `permission denied`\n(a real filesystem failure the agent must work around), a bare `timed out`\n(usually its own command hanging) and `connection refused` (usually a dev server\nit forgot to start).\n\nEach client hides the text somewhere different — Claude Code in the\n`tool_result`, Cursor in `toolFormerData.result`, Codex in a failed item's\n`result` for an MCP call or `stderr` for a shell one. Serialising a whole Codex\nitem instead tested the command line and the id, which counted a session that\nmerely *printed* the word \"rejected\" as a refusal.\n\n**Errors are not a measure of inefficiency.** Their raw count is mostly a size\nmeasurement — 0.87 against tool calls. As a rate (errors per 100 tool calls) they\ncorrelate 0.08 with tokens per line changed and −0.01 with seconds per line\nchanged. Across 78 sessions, a 5× difference in error rate bought 20% more tokens\nper line and 18% *less* time per line: no signal. Elevation honestly means\neventful, not wasteful.\n\n**Raw tokens cannot rank efficiency.** They correlate 0.72 with distance, so the\nnumber mostly says how big a session was. Economy (tokens per km) correlates 0.08\nwith distance — size-independent, and therefore actually comparable. It measures\ntoken cost per unit of *volume*, not of *value*: a session that finds the right\nanswer in five calls scores badly on it.\n\n**None of this measures whether the work was any good.** A session that flails for\n800 tool calls outscores one that fixes the bug in five. Nothing in a transcript\nreliably encodes outcome — that's a ceiling, not a tuning problem.\n\n## The route and the climb profile\n\n**The route map** is a random walk seeded by the activity id, so a card always\nredraws identically. Two things in it are real: its length and density come from\ntool calls, and **every error you recovered from draws as a loop** — the trace\nshows where you went in circles.\n\n**The climb profile** under it is cumulative elevation: flat where the session ran\nsmoothly, stepping up wherever a file was written or an error recovered, bucketed\nby moving time so an idle gap doesn't collapse it. The area under the curve is the\nelevation figure on the card.\n\nThat strip was decoration until recently — a seeded random walk reading no session\ndata at all, the same label over pure noise. Sessions with fewer than three climb\nevents now get **no strip at all** rather than an invented one. 81% have a profile.\n\n## Weekly and Monthly Snap\n\n```bash\nnode scripts/snap.mjs week                 # this calendar week so far\nnode scripts/snap.mjs week last            # the previous calendar week\nnode scripts/snap.mjs week last7           # rolling seven days, ending today\nnode scripts/snap.mjs month                # this calendar month so far\nnode scripts/snap.mjs month last30         # rolling thirty days (14d, 90d… also work)\nnode scripts/snap.mjs month 2026-08 --pick 086f7bf6   # feature a session you chose\nnode scripts/snap.mjs week --title \"Shipped the new onboarding\" --hide-projects\n```\n\n**Calendar or rolling.** A calendar week shared on a Wednesday is a stub — it\nsays \"this week so far\" and two days are empty. `last7` and `last30` are always\nwhole windows ending today, which is usually what you want to post. Calendar\nperiods stay the default because streaks, months and heatmaps are calendar\nthings. Rolling cards label themselves by range and by weekday, since a rolling\nweek does not start on a Monday.\n\nBoth are **1080×1350, the same frame as a session card**, so a snap and a card sit\nside by side in a feed. A six-row month is the tightest case and still clears the\nfooter.\n\n**Weekly** — agent time per day, sessions / active days / projects, tool calls,\nestimated cost, and the longest session. **Monthly** — a Monday-first heatmap,\nthe same headline counts, the top three projects by time, and a featured session.\n\nWhat makes the numbers hold together:\n\n- **Daily buckets are recorded, not inferred.** Both parsers credit each stretch\n  of moving time to the local day it happened, splitting at midnight, so a session\n  from 23:50 to 00:05 counts 10 minutes on one day and 5 on the next. Every bar\n  and heatmap cell comes from those buckets, and so does Active Days — seven bars\n  can't sit next to a five-day headline.\n- **A session crossing a period boundary appears in both**, with its time\n  apportioned. Tool calls and cost have no per-day record, so they are apportioned\n  by the same time share.\n- **\"Agent time\" is summed across sessions.** Nine agents running in parallel for\n  five hours is 45 hours of agent time on one day. The card says so under the\n  chart; it is not your working hours.\n- **Cost says when it is partial** — `partial · 38 of 40 priced` — and reads\n  \"not recorded\" rather than $0 when nothing could be priced.\n- **\"Month's pick · Selected by you\" appears only when you picked it.** Otherwise\n  the feature is labelled \"Longest session\", which is what it is.\n- **An unfinished period says so** — \"This week so far\".\n- **Nothing is inferred about outcome.** A `--title` is yours to write; the card\n  never claims anything shipped.\n\n## Badges\n\nEarnable, not participation trophies:\n\n`Negative Splits` deleted more than you wrote · `Flawless` no errors, no failed tests ·\n`Hill Repeats` climbed out of it 3+ times · `Marathon` 1h+ · `Ultra` 3h+ ·\n`Sprint` under 3 minutes with a diff · `Yak Shave` 30+ tool calls, barely a diff ·\n`All Green` full suite, zero red · `Furnace` 5M+ tokens · `Nocturnal` 11pm–5am ·\n`Everest` 3000m+ · `10K Club` 10 km · `Gran Fondo` 40 km · `Polyglot` 3+ languages ·\n`Red Zone` effort 90+ · `Sightseeing` all reading, no writing ·\n`Signed Off` 10+ edits accepted, none sent back (Cursor only)\n\nMeasured frequency: `Hill Repeats` 47%, `Marathon` 44%, `Yak Shave` 36%,\n`Polyglot` 31%, `10K Club` 25%, `Ultra` 22%, `Flawless` 22%, `Nocturnal` 19%,\n`Red Zone` 14%, `Furnace` 8%, `Everest` 8%. Average 3.1 badges per card.\n\nPersonal records only fire once there is something to beat, so the first activity\nnever claims one.\n\n## Logging every workout\n\nThree modes, in descending cost:\n\n| | per-turn cost | logs sessions | keeps streaks honest |\n|---|---|---|---|\n| **auto** (Stop hook) | ~300 ms | automatically | yes |\n| **manual + day stamp** (default of `--manual`) | ~10 ms | when you ask | yes |\n| **manual only** (`--manual --no-stamp`) | none | when you ask | no |\n\nThe full hook re-parses the transcript and redraws the card every turn. The day\nstamp is a two-line shell script that appends today's date and nothing else — it\nnever starts a Node process, and writes one line per *day*. Streaks count the\nunion of logged-activity days and stamped days, so both modes mean the same thing.\n\nBy hand, any time:\n\n```bash\nnpm run log                       # the session you are in\nnode scripts/log-now.mjs --cursor # the Cursor conversation you are in\nnode scripts/log-now.mjs --codex  # the Codex CLI session you are in\nnode scripts/log-now.mjs --codex --list\nnode scripts/log-now.mjs --list   # 15 most recent, newest first\nnode scripts/log-now.mjs 9e22ccfa # one session by id prefix\n```\n\nLogging **upserts** — running it repeatedly on one session updates that activity\ninstead of stacking duplicates. Sessions under 8 tool calls or 2 minutes are\nignored.\n\n### What the Claude Code hook measures\n\n| Field | Source |\n|---|---|\n| Tool calls | `tool_use` blocks in assistant messages |\n| Tokens | `usage` input / output / cache write / cache read, per model |\n| Lines ± | `structuredPatch` hunks from Edit/Write results |\n| Files | Edit/Write paths, **plus** shell redirect / `tee` / `sed -i` targets |\n| Errors recovered | `tool_result.is_error` |\n| Moving time | consecutive timestamp gaps, each capped at 5 min |\n| Model | `message.model`, most frequent in the session |\n\nKnown limits: **cache reads are excluded from the token total** (replayed context\nis not work done); **shell writes are detected by regex**, deliberately\nconservative — it misses writes rather than inventing them, and line counts for\nthose files are not recovered, so churn under-reports on shell-heavy sessions;\n**session type is a guess** from files, churn and error count.\n\n## Backfill: importing past seasons\n\n```bash\nnode scripts/backfill.mjs --dry-run    # report only, writes nothing\nnode scripts/backfill.mjs              # log everything not yet logged\nnode scripts/backfill.mjs --force      # rebuild from empty (backs up first)\nnode scripts/cursor-backfill.mjs       # same, for Cursor\nnode scripts/codex-backfill.mjs        # same, for Codex CLI\n```\n\nSorted by session time, because personal records are judged against prior history\n— replaying out of order would award them to whichever session happened to be\nprocessed first. Walks `~/.claude/projects/` recursively (git-worktree sessions\nlive several levels deep). Roughly 900 MB of transcripts takes ~25 s including\ncard rendering.\n\n## Cursor\n\nCursor stores chat in SQLite at\n`~/Library/Application Support/Cursor/User/globalStorage/state.vscdb`, one row per\nmessage, keyed `bubbleId:<conversationId>:<bubbleId>`.\n\nThe database is **WAL-mode**, and while Cursor runs it usually has megabytes of\nuncommitted log. Opening it with `immutable=1` makes SQLite ignore the WAL — which\nhides the newest conversations entirely and throws \"malformed\" when a checkpoint\nlands mid-read. Reads use `mode=ro`, falling back to a snapshot of the db plus its\n`-wal` and `-shm`. The whole database is read in **one grouped pass** (~60 s for\n289 conversations); per-conversation `LIKE` queries each scan a multi-GB table.\n\n### What Cursor actually records\n\nMeasured across 222 logged conversations:\n\n| signal | coverage | |\n|---|---|---|\n| tool calls | 100% | ✅ |\n| moving time | 100% | ✅ |\n| climb profile | 89% | ✅ |\n| errors | 57% | ✅ a real `status` field, cleaner than Claude Code's boolean |\n| `userDecision` | 40% | ✅ **accepted / rejected per edit** — no equivalent in Claude Code |\n| tokens | 4% | ❌ `tokenCount` unpopulated since Jan 2026 |\n| **files changed** | **6%** | ❌ see below |\n| **line churn** | **0%** | ❌ see below |\n\nCursor stores arguments for only **477 of 15,142** `edit_file_v2` calls; the rest\nhave empty `rawArgs`, and the result holds content hashes rather than paths. So\n**which file an edit touched is usually unrecoverable**, and `files_changed`\ncounts only the subset that is.\n\nThis was worse before: the parser took a path from *any* tool carrying one,\nincluding `read_file_v2`, so files merely opened counted as changed and inflated\nelevation (median Cursor elevation 555 m → 120 m once restricted to real edits).\nUnder-reporting something unmeasurable beats inflating it, so Cursor elevation\nrests mainly on errors — which it does record reliably.\n\nCursor has a `stop` hook with the same stdio-JSON contract as Claude Code\n(`conversation_id`, `transcript_path`, `workspace_roots`, `status`), and\n`hooks/cursor-probe.mjs` captures one real payload. Live auto-logging is **not**\nwired up: a full scan takes ~60 s, too slow for every turn.\n\n## Codex CLI\n\nCodex writes one JSONL rollout per session under\n`~/.codex/sessions/YYYY/MM/DD/rollout-<timestamp>-<uuid>.jsonl`, with a timestamp\non every line, so moving time, daily buckets and the climb profile come out the\nsame way they do for Claude Code. 130 rollouts totalling 330 MB parse in about\ntwo seconds; the largest single file, 119 MB, takes 566 ms.\n\n**It records file changes better than the other two clients.** An `item_completed`\nevent of type `FileChange` carries a `changes` map: the full body for an added\nfile, a unified diff for an edited one. Churn is counted from those diffs rather\nthan inferred from shell commands (Claude Code) or given up on (Cursor).\n\nMeasured across the 24 sessions above the logging floor:\n\n| signal | coverage | |\n|---|---|---|\n| tool calls | 100% | ✅ `custom_tool_call`, `function_call`, `tool_search_call` |\n| moving time | 100% | ✅ |\n| tokens in/out/cached | 100% | ✅ `token_count.info.total_token_usage` |\n| files and line churn | 100% of sessions that changed a file | ✅ exact, from `FileChange` diffs |\n| errors | 54% | ✅ `item_completed` with `status: \"failed\"` |\n| model | 100% | ✅ from `turn_context` |\n| est. API cost | 100% | ✅ OpenAI list prices, per model and tier |\n| accepted / rejected edits | 0% | ❌ not recorded |\n\nToken totals are cumulative in the log, so the last `token_count` wins rather\nthan being summed, and `input_tokens` includes the cached portion — the cached\ntokens are subtracted back out so a Codex card's token count means what a Claude\nCode card's does.\n\n**Pricing a Codex session takes two passes.** The cumulative totals say how many\ntokens there were; the per-request `last_token_usage` figures say which model and\nwhich price tier they ran under. Summing the per-request figures instead would\noverstate a session that replays part of its own history — one rollout in four\ncame out 5% high — so the totals stay authoritative and the per-request numbers\ndecide only how the tokens divide.\n\nOpenAI charges a whole request at 2× input and 1.5× output once its prompt passes\n272K input tokens. Across 3,329 requests here the largest prompt was 245K, and\nCodex ran a 258K context window, so the higher tier never applied — but it is\nimplemented rather than assumed away.\n\nCodex's own `codex-auto-review` model has no published rate. Its turns price at\nzero rather than being guessed at, exactly as an unrecognised Claude model does.\n\nTwo things in the log are not what they look like:\n\n- **The first user message is not the prompt.** Codex opens a session by feeding\n  itself the environment block, `AGENTS.md`, the plugin list and a replayed\n  approval history as user messages. Taken verbatim, sessions were titled\n  `<recommended_plugins>`. Those are skipped.\n- **Each Codex conversation gets its own directory** when Codex runs from the\n  ChatGPT app: `<workspace>/Codex/<date>/<conversation-slug>`. One project per\n  session, each named after the conversation — which would put chat titles on a\n  card meant to be shared. They group under the Codex workspace instead. A Codex\n  session started inside a real repository still reports that repository.\n\nOf 130 rollouts, 105 fall below the 8-tool-call floor: most are chat-only threads\nwith no tools at all, which the same floor would reject in any client.\n\nThere is **no auto-logging hook**. Codex's `notify` takes a single program and is\nusually already claimed by something else, so taking it over would break whatever\nwas there. Log from Codex with the `snapshot` MCP tool (`npx agentrava setup --codex`\nregisters the server in `~/.codex/config.toml`) or with\n`node scripts/log-now.mjs --codex`.\n\n## Cards\n\n### Athlete and gear\n\nThe athlete is **you**, not the model — Strava does not file your rides under the\nbike. The model is gear, shown under the title with the client:\n`Claude Opus 5 · Cursor`.\n\n```bash\nnode scripts/whoami.mjs \"Your Name\"        # the name on every card, past and future\nnode scripts/whoami.mjs --avatar ~/me.jpg  # a picture for the circle\nnode scripts/whoami.mjs --avatar chat      # use an image you just pasted into the chat\nnode scripts/whoami.mjs --no-avatar        # back to the initial\nnode scripts/whoami.mjs                    # show both\n```\n\nThe avatar replaces the initial in the circle on every card and snap, clipped to\nthe circle. It is copied into `~/.agentrava/avatars/` on set, so moving or\ndeleting the original does not blank your cards, and it is swappable any time —\nsetting a new one redraws everything. `set_athlete` takes `avatar` and\n`avatar_reset` for the same thing from chat.\n\n`set_athlete` does the same from chat. Unset, cards read \"Athlete\" — the safe\ndefault for sharing.\n\n### Renaming sessions and projects\n\n```bash\nnode scripts/rename.mjs session 4e374e0a \"The photo that kept vanishing\"\nnode scripts/rename.mjs session 4e374e0a --reset          # back to the generated title\nnode scripts/rename.mjs projects                          # list, with paths\nnode scripts/rename.mjs project \"acme-api-service\" \"API\"\nnode scripts/rename.mjs project API --reset\nnode scripts/rename.mjs project API --hide                # keep it off every card\n```\n\nOr from chat: `rename_session`, `rename_project`, `list_projects`. Every card\nresult ends with the options that apply to it, phrased for the assistant to put\nto you —\nadding a photo, renaming, hiding project names, or picking a share level before you share. Affected cards\nare redrawn immediately — changing a name in a table does nothing to a PNG\nalready on disk.\n\nNames are presentation, stored beside the measured data rather than in it —\nsession titles in `overrides.json`, project names in `projects.json` — so a\nre-log or a forced backfill keeps them, and a reset always restores the\ngenerated name.\n\n**Which project a session belongs to comes from the transcript**, not from where\nyou happened to run the logger: the most frequent working directory recorded in\nthe session that resolves to a repository. Logging the same session from `$HOME`\nused to drop its project entirely, so a project could vanish from a snap with no\nwork having happened. Agent scaffolding — `~/.claude`, `~/.cursor`, Claude's\nscratch workspaces — is never a project.\n\n**A project is its repository path, not its name.** Two repositories both called\n`web` stay separate, and giving two projects the same display name does not merge\nthem. A name that matches more than one project is refused with the paths listed,\nrather than guessed; so is a session id prefix that matches more than one session.\nAnything under a repository's `.claude/` or `.cursor/` — worktrees, skills —\ncounts as that repository.\n\nHiding a project removes its name from session cards and shows it as\n\"Project A\" on snaps. The route is seeded by activity id only, so renaming a\nsession never redraws its route.\n\n### Client names and logos\n\nCards name the client in the header. Known ids: `claude-code`, `claude`, `cursor`,\n`openai`, `codex`, `grok`, `copilot`, `windsurf`, `zed`.\n\n**With a logo installed, the mark sits large in the top-right corner** — 58px,\nbalancing the athlete's avatar on the left, with the activity type and streak\nshifted in beside it. Without one there is nothing to put there, so the client\nstays a **tinted chip** under the title instead — Claude Code terracotta, Codex\ngreen, Cursor white — rather than trailing the gear line as grey text.\n\n**No logo artwork ships with this repo.** Those marks are trademarks, and bundling\nthem into an MIT repo means redistributing brand assets that most brand guidelines\nrestrict. Naming a product is ordinary nominative use; shipping its logo is not.\nUntil you install one the chip shows a coloured dot.\n\n```bash\nnode scripts/logo.mjs                      # what is installed, and for how many sessions\nnode scripts/logo.mjs codex ~/openai.svg   # install one\nnode scripts/logo.mjs codex chat           # use an image you just pasted into the chat\nnode scripts/logo.mjs codex --remove\n```\n\nThe file lands in `~/.agentrava/logos/<client>.svg` (or `.png`, under 512 KB) and\nevery card for that client is redrawn. Whether you may put a given mark on a card\nyou post is between you and that owner's brand guidelines — this repo takes no\nposition beyond not shipping the artwork itself.\n\n### Photos\n\nStrava lets you put your ride photo behind the route. So does this.\n\n```bash\nnode scripts/card.mjs <session> --photo ~/me-in-a-hammock.jpg\nnode scripts/card.mjs <session> --photo chat   # the image you just pasted\nnode scripts/card.mjs <session> --no-photo\n```\n\n`--photo chat` needs no file: an image pasted into Claude Code never becomes a\nfile on disk — it lives as base64 in the transcript — so this recovers the most\nrecent one. jpg/png/gif/webp under 8 MB, embedded so the card stays one\nself-contained file.\n\nA centred full-size route sits right on whoever is in the photo, so photo cards\ndefault to a **smaller route on the left**. Move it yourself if the subject is\nelsewhere — there is no face detection, on purpose:\n\n```bash\nnode scripts/card.mjs <session> --route right --route-scale 0.6\nnode scripts/card.mjs <session> --route auto      # back to the default\n```\n\nPhoto and route placement live in `~/.agentrava/overrides.json`, apart from the\nmeasured data, so re-logging a session or a forced backfill keeps them. (Before\nthis, every re-log redrew the card from scratch and silently dropped the photo.)\n\n### Before you share one\n\n**The title is a guess.** \"Evening Debug\" comes from the clock and the session\ntype, not from what you did. Rename it first:\n\n```bash\nnode scripts/card.mjs <session> --title \"Fixed the login loop\"\n```\n\nOr from chat: `snapshot` with `title`. `rename.mjs session <id> --reset` brings\nthe generated one back.\n\n**Then pick how much to show.** One setting per card, kept across re-logs like\nthe photo:\n\n| `--share` | what goes |\n|---|---|\n| `full` (default) | nothing — the card as measured |\n| `friends` | est. API cost, economy, the diff footer |\n| `anonymous` | all of that, plus the photo, avatar, name, your title, the subtitle and the project |\n\n```bash\nnode scripts/card.mjs <session> --share friends\nnode scripts/card.mjs <session> --share anonymous\nnode scripts/card.mjs <session> --share full      # everything back\n```\n\nOr `snapshot` with `share`. The route, the climb and the badges stay on every\nlevel: without them it is a stats table, not an activity.\n\nShare levels are for session cards. Weekly and Monthly Snaps have their own\nswitches — `--hide-projects` and `--title` — and no photo to drop.\n\nThe subtitle is your first prompt, and prompts name customers, vendors and\ninternal projects.\n\n```bash\nnode scripts/privacy.mjs              # list subtitles that look sensitive\nnode scripts/privacy.mjs --strip      # blank just those\nnode scripts/privacy.mjs --strip-all  # blank all, and stop recording them\nnode scripts/card.mjs <session> --no-summary\n```\n\nThe detector flags company suffixes and capitalised proper names; **it will not\ncatch everything** — \"fix the checkout bug for acme\" reads as clean. Read the\nsubtitle before you post one, or turn summaries off entirely with\n`{\"summaries\": \"off\"}` in `~/.agentrava/config.json`.\n\n## Cost\n\nClaude Code records four token classes per message, so a session is priced per\nmessage at whatever model produced it. Codex is priced per request — see above.\nRates are Anthropic and OpenAI list prices (`src/pricing.js`, read from their\ndocs on 18 September 2026); both vendors bill cache writes at 1.25× input and\ncache reads at 0.1×, so one table shape covers both.\n\nA model with no published rate is left unpriced rather than guessed at, and a\ncard with no price shows `—` rather than `$0.00`. Snaps say how many of their\nsessions were priced, so a gap is visible instead of silent.\n\n**This is not a bill.** A subscription does not charge per token. The figure is\nwhat the session *would* have cost on the API — useful for comparing sessions,\nuseless as an invoice. Cards set to `friends` or `anonymous` leave it off, so the\nestimate does not end up in a post by accident.\n\nThe split is the interesting part. Across 136 priced sessions: 0.7M input, 48.5M\noutput, 373M cache writes and 17.2 **billion** cache reads. Cache reads are 98% of\nall tokens, which is why they dominate cost even at a tenth of the input rate.\n\n## Data\n\nActivities live in `~/.agentrava/activities.json`, cards in `~/.agentrava/cards/`\nas PNG and SVG. Override with `AGENTRAVA_HOME`. **Nothing leaves the machine** —\nthere is no network call anywhere in this server.\n\nWrites are serialised with a `mkdir`-based cross-process lock: every session's hook\nwrites the same file, and without it a 20-way concurrent test lost 19 writes.\n\nActivity ids are derived from the client and session id, so a forced rebuild\nproduces the same ids, the same routes and the same card files — it used to mint\nnew ones and orphan hundreds of cards each time.\n\nProjects are resolved through agent worktrees: a session in\n`<repo>/.claude/worktrees/<name>` belongs to `<repo>`, read straight off the path\nso it still works after the worktree is deleted. Without this, one week counted\n18 projects that were really 2.\n\n## Development\n\n```bash\nnpm run demo                      # render sample cards from a synthetic season\nnode scripts/e2e.js               # drive the server over real MCP stdio\nnode scripts/rerender.js --prune  # redraw stored cards, delete orphans\nnode scripts/recap.js 2026-08-01 2026-08-31\n```\n\n## License\n\nMIT — see [LICENSE](LICENSE).\n"}