Skip to content

Repository files navigation

agent-transcripts

Swift code for reading what the coding agent CLIs leave on disk: the transcript of a conversation, and in Claude Code's case what it cost.

Pulled out of Nexion, where it backs the session picker and the usage panel. The formats it reads are nobody's public API, so the useful part of this package is the accumulated knowledge of their quirks rather than the code around it.

Reading a conversation

It reads Claude Code, Codex, opencode and Gemini CLI.

import AgentTranscripts

if let (source, how) = TranscriptReader.locateSource(
    agentName: "claude", sessionID: nil, cwd: "/path/to/project"
) {
    let turns = TranscriptReader.turns(source, maxBytes: 512 * 1024)
    print(TranscriptReader.render(turns, limit: 20))
    print("found \(how)")   // "by-id" is exact, "by-folder" is a guess
}

Past the obvious, most of what this knows is not obvious:

  • Claude Code and Codex write JSONL, one event per line; Gemini's older files are a single JSON object holding the whole conversation and its newer ones rewrite the entire array on every turn; opencode keeps a SQLite database instead of files.
  • Thinking blocks, tool calls and tool results are dropped. The question is what the agent said, and a tool result is what the machine said back.
  • The last line of a live transcript is usually half-written. It fails to parse and is skipped, then arrives whole on the next read.
  • Claude mangles the working directory into its folder names in a way you cannot reverse, so cwd is read out of the file rather than inferred from the path.

AgentSessionCatalog is the other half: recent conversations for a project, with the title the agent kept and whether another process still holds the transcript open, which for Codex means codex resume will refuse it.

let sessions = AgentSessionCatalog.recentSessions(
    agentID: "claude", projectPath: "/path/to/project", limit: 8
)

Blocking, so call it off the main thread.

What it cost

ClaudeUsageLog answers where the tokens went, which the quota endpoint does not: by day, by model, by project. Every assistant turn Claude Code writes carries the usage the API billed, so the source is local files and there is nothing to authenticate against.

let usage = ClaudeUsageLog.breakdown(days: 30)
print(usage.total, usage.today, usage.byModel.first?.label ?? "")

Those files get large. On a month of real transcripts, 510 MB, the scan reads them in 0.9s inside 8 MB of memory, against 4.7s and 154 MB for the version that decodes each line into objects. It gets there with a byte-level pre-filter that drops any line that cannot be a billed turn, reading the six fields that matter straight out of the bytes, and remembering each file by size and mtime so one that has only grown is re-read from where the last scan stopped.

Installing

.package(url: "https://github.com/nexion-one/agent-transcripts", from: "1.0.0")

One module, AgentTranscripts. macOS 13 or newer, Foundation and CryptoKit, nothing else.

What will break

These formats are private and change without notice. Everything here is read-only and best effort: a format that moves produces fewer turns or an empty list, never a crash and never a write. Do not build anything load-bearing on the assumption that a given field is still there next month.

Tested against the shapes these tools were writing in August 2026.

Contributions

Issues about a format that has moved are the most useful thing you can send, especially with a redacted sample line. Pull requests are read, but this package follows the requirements of the Nexion application, so not every change can be accepted.

License

MIT. See LICENSE.

About

Swift package for reading what the coding agent CLIs leave on disk: Claude Code, Codex, opencode and Gemini transcripts, and what Claude Code cost

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages