Export coding-agent conversations into readable markdown, then turn them into an interactive HTML viewer for analysis. Three agents are supported, and all land in the same format so a single index can list them side by side:
- Claude Code — the jsonl transcripts under
~/.claude/projects/ - OpenCode — the SQLite database at
$XDG_DATA_HOME/opencode/opencode.db - Cursor — the SQLite database at
Cursor/User/globalStorage/state.vscdb(see the Cursor caveat)
Two steps, both run through the cca CLI (see Install):
cca export— dumps conversations to markdown (plus a structured JSON sidecar), organized by git branch.cca generate-html— converts a markdown export into three reports: a three-column interactive discussion viewer, a metrics dashboard, and a token/cost/time simulation page.
Under the hood these are standalone, self-contained TypeScript scripts using only
Node built-ins, run with tsx — no build step.
curl -fsSL https://raw.githubusercontent.com/theodo-group/coding-conversation-analyzer/main/install.sh | bashThis clones the repo to ~/.coding-conversation-analyzer, installs dependencies, and
puts the cca command on your PATH with three subcommands:
cca export <output-dir> # export conversations to markdown
cca generate-html <input> [output] # render a markdown export to HTML
cca update # update to the latest version
cca --version # print the installed versionThe standalone aliases cca-export and cca-generate-html are also installed for
backward compatibility. Re-run the one-liner — or cca update — any time to update;
it's a no-op when you're already on the latest version.
Override the defaults with env vars if needed:
INSTALL_DIR=~/tools/cca BIN_DIR=~/bin \
bash -c "$(curl -fsSL https://raw.githubusercontent.com/theodo-group/coding-conversation-analyzer/main/install.sh)"Requires Node.js 18+. If ~/.local/bin isn't on your PATH, the installer prints the line
to add to your shell profile.
git clone https://github.com/theodo-group/coding-conversation-analyzer.git
cd coding-conversation-analyzer
npm installThen run the scripts with npm run export / npm run view, or a global tsx (npm i -g tsx).
cca export <output-dir> # if installed via the one-liner
# or, from a clone:
npm run export -- <output-dir>
# or: tsx src/export-history.ts <output-dir>Exports conversations including tool results, thinking blocks, subagent conversations, actual Edit diffs, and YAML frontmatter. Incremental — re-running only exports new or changed conversations.
Coding agents key their history by working directory, but one project is not always one directory. Running from the main clone, the exporter also picks up sessions from:
- the repo's git worktrees (
git worktree list) — including the isolated worktrees Claude Code itself creates for agents; - Conductor workspaces of the same repo. Conductor runs
every workspace as a worktree at
~/conductor/workspaces/<repo>/<name>; sessions from archived workspaces — whose directory and worktree registration are already gone, but whose transcripts remain — are recovered too, by matching Claude Code's transcript folder names.
Everything lands flat in the same output tree and the same index as the main clone's sessions, grouped by branch as usual — one project, one report, regardless of how many workspaces did the work.
By default (--source auto) every agent that has sessions for the current git root is
exported, so a task done twice — once in Claude Code, once in Cursor — shows up as two
rows in the same index, directly comparable. Restrict it with --source:
cca export <output-dir> --source claude # Claude Code only
cca export <output-dir> --source opencode # OpenCode only
cca export <output-dir> --source cursor # Cursor onlyThe OpenCode and Cursor databases are read read-only through Node's built-in
node:sqlite, which is safe while either app is running — Cursor's write-ahead log is
replayed, so a session written seconds ago exports in full. Filenames are unambiguous per
source: Claude sessions use their 8-character uuid prefix, OpenCode sessions an
oc-tagged session id (…-ocf850f7be.md), Cursor sessions a cu-tagged one
(…-cucdd5e6fe.md).
A few things differ because the sources differ, and the reports say so rather than pretending otherwise:
| Claude Code | OpenCode | Cursor | |
|---|---|---|---|
| Token usage & cost | recorded per message | recorded per API step | not recorded at all — see below |
| Branch | recorded per message | not recorded — read from the working tree now, and stamped branchSource: "live-git" |
recorded at session time (branchSource: "snapshot") |
| Timeline band | permission mode (Normal / Plan / Auto-accept / Bypass) | active agent / mode (build, plan, explore, …) — a different thing, labelled differently | session mode (agent / chat / edit), one flat segment — Cursor records no transitions |
| Diffs | approximated from the edit's before/after strings | real unified diffs with exact add/delete counts | real line-level diffs, exact — Cursor precomputes them |
| Subagent links | scraped from the spawn's result text | stated outright, and nested arbitrarily deep | stated outright, with retried spawns excluded |
| Task notifications / workflows | present | no analog — simply never emitted | no analog — simply never emitted |
| Compaction | — | marked on the transcript and the timeline (🗜️) | not observable in what Cursor stores |
Cursor meters usage server-side and writes {"inputTokens": 0, "outputTokens": 0} for
every message on disk. There is nothing to price, so a Cursor export omits cost rather
than reporting $0.00 — a zero would be a claim about the session, and the wrong one.
Concretely:
- the markdown carries
tokens: unavailablein its frontmatter and a one-paragraph note above the transcript; - the dashboard shows a banner, an
n/acost tile, and — in place of the cost/context chart — Cursor's own estimated context breakdown (system prompt, tools, rules, skills, MCP, conversation), which neither other source provides; - the simulation page is not generated, since it is built entirely on token usage;
- in a mixed-source index the row reads
n/aand is left out of the selected cost total.
Everything else — messages, thinking, tool calls and results, timings, diffs, subagents — is complete, and in places more exact than the other sources.
Long tool results are truncated by default. Pass --full to export them in full:
cca export <output-dir> --fullBy default it reads Claude Code's history from ~/.claude, OpenCode's from
$XDG_DATA_HOME/opencode (i.e. ~/.local/share/opencode), and Cursor's from the platform
application-support directory (~/Library/Application Support/Cursor on macOS,
~/.config/Cursor on Linux, %APPDATA%/Cursor on Windows). Pass --claude-dir <path>,
--opencode-dir <path> or --cursor-dir <path> (=<path> also works, ~ is expanded) to
read from a different location — useful for a non-standard CLAUDE_CONFIG_DIR, a backup,
or another machine's history:
cca export <output-dir> --claude-dir /path/to/.claude
cca export <output-dir> --opencode-dir /path/to/opencode
cca export <output-dir> --cursor-dir "/path/to/Cursor"Output structure:
<output-dir>/
<git-user>/
<branch>/
2026-03-01-12-58-08-479c0b78.md
2026-03-01-12-58-08-479c0b78.json # sidecar: usage, cost inputs, timeline, diffs, setup
2026-03-01-12-58-08-479c0b78-subagents/
agent-abc123.md
This structured data — per-message token usage, model, timestamps, permission-mode
timeline, edit diffs, subagent token totals, and the active agents/skills config — is
what the markdown body drops. It is embedded directly in the .md as a trailing hidden
HTML comment (<!-- cca:data … -->, invisible in any rendered markdown), so a single
.md is self-contained: it renders both the discussion and the dashboard on its own.
The same data is also written as a sibling .json sidecar for backward compatibility
and for tooling that wants the raw metrics without parsing the markdown.
cca generate-html <input.md | input-dir> [output.html | output-dir] # if installed via the one-liner
# or, from a clone:
npm run view -- <input.md | input-dir> [output.html | output-dir]
# or: tsx src/generate-html.ts <input.md | input-dir> [output.html | output-dir]Each markdown input produces up to three files, side by side:
<name>-discussion.html— the three-column interactive viewer (always written)<name>-dashboard.html— the metrics dashboard (written whenever the source.mdcarries an embeddedcca:datablock, or a<name>.jsonsidecar sits next to it)<name>-simulation.html— the token/cost/time simulator (written under the same condition as the dashboard; see below)
If the input is a single .md file and the output argument is omitted, the files
default to <input_basename>-discussion.html, -dashboard.html and -simulation.html.
The simulator is a learning tool: a linear transcript of the conversation with a checkbox on every tool call. Unchecking a tool simulates never having run it — its result stops riding along in every later prompt — and a sticky side panel recomputes the session's cost, peak context and duration live in the browser.
It is an accounting model over the real token usage, not a counterfactual: it assumes
the same conversation trajectory, only with cheaper context, and offers no advice.
A tool's context weight is derived by differencing the context size of consecutive API
calls (input + cache_creation + cache_read) and subtracting the known output tokens;
that weight is then removed from every later call — split across each call's
cache-write / cache-read in proportion to its actual cw:cr, so a cache-expiry re-write
credits the full 1.25× write, a normal cached read credits 0.1×. Cost is priced
identically to the dashboard.
A lone tool in a turn takes that turn's exact differenced weight. Tools that share a
turn are each sized from their own result length (via a chars→tokens ratio calibrated
from the session's single-tool turns), marked with a *. When a turn grows by more than
its tool results carry — a Skill loading its body, a Task/Agent subagent
spawn, an MCP call returning a large resource — that unexplained residual is
attributed to the injector call, so unchecking it removes the context it actually caused
(e.g. a /graphify skill load that added ~200k tokens). Growth with no identifiable
injector (a pasted message, say) is left unattributed rather than guessed.
If the input is a directory, every .md/.markdown file inside is converted
recursively, writing both files next to each source — or mirroring the directory tree
under output-dir if a second argument is given. An index.html is also written at
the output root (see below).
Converting a directory writes an index.html at the output root listing every
conversation in one table — title, cost, max context, duration, and change
(lines added/removed) — with links to each conversation's discussion, dashboard and
simulation reports.
Each row has a checkbox (ticked by default). A sticky totals bar live-sums the selection so several sessions on one feature can be analyzed as a group: cost, duration, and change are summed, while max context shows the peak reached across the selection. "Select all" / "Clear" toggle the whole list.
Metrics come from each conversation's .json sidecar. A markdown file with no
sidecar next to it is still listed (with its discussion link) but shows — and no
checkbox, since it has no metrics or dashboard.
- Three-column grid layout: Input | Assistant | Tools
- Input: your prompts (🧑); teammate/inter-agent messages (🤝, accent-colored per
teammate with an id label, JSON payloads rendered as a key/value grid and a
summarychip); local commands (⌨️, e.g./compact); and subagent task notifications (🔔) - Assistant: replies (🤖) and thinking (🧠)
- Tools: calls (⚪️), results (🟢), errors (🔴), and skill prompts (📜)
- Context compaction, where the source records it, is marked in the Input column (🗜️)
- Input: your prompts (🧑); teammate/inter-agent messages (🤝, accent-colored per
teammate with an id label, JSON payloads rendered as a key/value grid and a
- Per-subtype counters in each column header (e.g.
14 🧑 · 14 🤝 · 4 ⌨️ · 3 🔔) - Collapsible cards with turn-based grouping
- Navigation buttons to jump between messages of the same subtype
- Tool results, errors, and skill prompts collapsed by default
- Dark theme with color-coded message types
Same dark theme, a single-page metrics report generated from the JSON sidecar:
- Summary tiles: duration, human turns, lines added/removed, tool-call breakdown
- Cost & context chart — spend and context-window usage over the conversation,
per model. Cost is computed from token usage, since it isn't stored in the
transcript: prices and context limits come from a models.dev-shaped
catalog (
src/models.ts) with each model's own explicit cache-read/cache-write prices. Models from providers the built-in catalog doesn't cover ride along in the export's sidecar, so an OpenCode session on any provider still prices correctly — including free models, which show an honest$0. The context-window axis is the largest limit across the models the session actually used - Message timeline with a thinking-blocks toggle and a band showing the permission mode (Claude Code), the active agent/mode (OpenCode) or the session mode (Cursor)
- Spawned subagents, with per-model token totals
- Generated diffs from
Write/Edittool calls - Setup panel — the configuration active for the run, grouped by the kinds the
source actually has: agents and skills for Claude Code (
.claude/,~/.claude/); agents, commands, plugins and skills for OpenCode (.opencode/,~/.config/opencode/, plusopencode.json(c)agents,AGENTS.md, and the~/.claude/skillsit reaches throughexternal_directoryrules); agents, skills, commands and always-onrules/*.mdcfor Cursor (.cursor/,~/.cursor/, including Cursor's built-in skills). Read from the current config, so it reflects config now, not necessarily at run time
Refresh the price catalog from models.dev with npm run sync-models — it prints entries
for review; the checked-in catalog stays authoritative so exports are reproducible.
- Node.js 18+
tsx(installed vianpm install, or globally)
MIT