txcript
txcript lets you swap the agent you're using in the middle of a session.
It converts session transcripts between Claude Code, Codex, OpenCode, pi, Campfire, and Cursor. Your messages, the agent's reasoning, tool calls, and images all come across, so the new agent picks up the conversation where the old one left off.
Each harness has its own native transcript shape. This crate maps those shapes through a typed common model, then re-emits them for another harness. Native load/save stays byte-lossless; cross-harness transformation preserves the semantic conversation: messages, reasoning, tool calls, tool results, images, metadata, and usage where available.
Supported harnesses (string ids in parentheses, used by the CLI and WASM):
- Claude Code (
claude_code) - Codex (
codex) - OpenCode (
opencode) - pi (
pi) - Campfire (
campfire) - Cursor (
cursor)
It ships three ways: a Rust library, a CLI, and a WASM module for Bun / Node / the browser.
Use as a library
[]
= "0.1"
# Drops the OpenCode SQLite store (rusqlite); the OpenCode codec stays available.
# txcript = { version = "0.1", default-features = false }
Three layers, smallest to largest:
Codec—to_common/from_commonper harness;convert::<A, B>chains them through the canonical model.TextCodec—from_text/to_text: parse/render a harness's native session text, no I/O.Store— discover/load/save against a real backend (session directories, or SQLite DBs for OpenCode and Cursor).
Convert in memory (no filesystem):
use ;
let claude = from_text?; // Transcript<ClaudeCode>
let codex = ?; // Transcript<Codex>
let codex_text = to_text?; // native rollout JSONL
Or go through disk with a Store:
use ;
let store = default_root.expect;
let found = store.discover?; // cheap metadata scan
let claude = store.load?; // Transcript<ClaudeCode>
let codex = ?;
default_root.expect.save?; // resumable on disk
The canonical model is Transcript<Common> — Meta + Vec<Message>, where a
Message holds typed Blocks (Text, Thinking, ToolUse, ToolResult,
Image) and a typed Tool enum.
Use as a CLI
It discovers local sessions and continues one in any harness — the offline half
of replay's continue --local:
continue hands the terminal to the harness when done (on Unix it execs).
Same-harness continues resume the original in place; --with re-synthesizes
into another harness's native format first. Override the launch command per
harness with TRANSCRIPT_<HARNESS>_RESUME_CMD (a {id} template), e.g.
TRANSCRIPT_CODEX_RESUME_CMD="codex resume {id}".
Use as a WASM module (Bun / Node)
The pure codec compiles to WebAssembly; the JS host owns all I/O and calls in
for the transformation. The Store layer (filesystem, SQLite, subprocess) stays
native and is excluded from the WASM build.
Install from git
prepare builds the wasm on install, so the machine needs the Rust toolchain.
Run the one-time toolchain setup, then it builds automatically:
# once per machine: wasm32 target + matching wasm-bindgen-cli
(Bun may ask you to trust the dependency before it runs prepare; add
"txcript" to trustedDependencies in your package.json.)
Or build from a local checkout
Then import by path (e.g. as a sibling of your project), and wire it as a prebuild step:
// your project's package.json
{
"scripts": {
"build:txcript": "cd ../txcript && bun run build",
"prebuild": "bun run build:txcript"
}
}
API
import { convert, toCommon, fromCommon, harnesses } from "txcript";
// (or "../txcript/pkg/txcript.js" for a local checkout)
import { readFileSync, writeFileSync } from "node:fs";
const input = readFileSync("rollout.jsonl", "utf8");
// native -> native (e.g. a Codex rollout into Claude Code's JSONL)
writeFileSync("session.jsonl", convert(input, "codex", "claude_code"));
// canonical view, and back
const common = JSON.parse(toCommon(input, "codex")); // { meta, messages }
const pi = fromCommon(JSON.stringify(common), "pi");
harnesses(); // ["claude_code","codex","opencode","pi","campfire","cursor"]
Text-in / text-out: input is a harness's native session text (JSONL for
claude_code/codex/pi/campfire, the opencode export JSON for opencode, and a
JSON export of Cursor's store.db for cursor); the result is the target's
native text. Invalid harness names or unparseable input throw a JS Error.
Development
&&