txcript 0.6.0

Convert coding-agent session transcripts between harness formats.
Documentation
# txcript

Convert coding-agent session transcripts between Claude Code, Codex, OpenCode,
pi, Campfire, Cursor (CLI and desktop), Grok CLI, Amp, and Antigravity.

<p align="center">
  <a href="https://claude.com/claude-code"><img src="https://github.com/anthropics.png?size=160" alt="Claude Code" height="48" width="48"></a>
  &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;
  <a href="https://github.com/openai/codex"><img src="https://github.com/openai.png?size=160" alt="Codex" height="48" width="48"></a>
  &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;
  <a href="https://opencode.ai"><img src="https://opencode.ai/apple-touch-icon-v3.png" alt="OpenCode" height="48" width="48"></a>
  &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;
  <a href="https://pi.dev"><img src="https://pi.dev/logo-auto.svg" alt="pi" height="48" width="48"></a>
  &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;
  <a href="https://cursor.com"><img src="https://github.com/cursor.png?size=160" alt="Cursor" height="48" width="48"></a>
</p>

txcript maps each native transcript format through a typed common model. Native
load/save is byte-lossless; cross-harness conversion preserves 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 CLI (`cursor`)
- Cursor desktop (`cursor_desktop`) — the IDE app's Agents sessions, read from
  and written to its `state.vscdb` database
- Grok CLI (`grok`)
- Amp (`amp`) — convert *from* amp only: threads are server-side and the CLI
  has no import, so sessions can't be continued into amp
- Antigravity (`antigravity`)

Available as a **Rust library**, **CLI**, and **WASM module** for Bun, Node, and
browsers.

## Use as a library

```toml
[dependencies]
txcript = "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_common` per 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 both Cursors).

Convert in memory (no filesystem):

```rust
use txcript::harness::{claude_code, codex};
use txcript::{Codec, TextCodec, convert};

let claude = claude_code::ClaudeCode::from_text(jsonl_text)?;          // Transcript<ClaudeCode>
let codex = convert::<claude_code::ClaudeCode, codex::Codex>(&claude)?; // Transcript<Codex>
let codex_text = codex::Codex::to_text(&codex)?;                       // native rollout JSONL
```

Or go through disk with a `Store`:

```rust
use txcript::harness::{claude_code, codex};
use txcript::{Store, convert};

let store = claude_code::ClaudeStore::default_root().expect("home dir");
let found = store.discover()?;                       // cheap metadata scan
let claude = store.load(&found[0].reference)?;       // Transcript<ClaudeCode>

let codex = convert::<_, codex::Codex>(&claude)?;
codex::CodexStore::default_root().expect("home dir").save(&codex)?;  // resumable on disk
```

The canonical model is `Transcript<Common>` — `Meta` + `Vec<Message>`, where a
`Message` holds typed `Block`s (`Text`, `Thinking`, `ToolUse`, `ToolResult`,
`Image`) and a typed `Tool` enum.

A slash command the user ran at the harness is a `Tool::Command` on a user
turn, with whatever the harness printed back as the paired `ToolResult` — so
`/release patch` reads as a call rather than as the markup the harness happens
to record it in. The leading `/` is what marks it canonically: no model-facing
tool name has one. Boilerplate the harness regenerates on its own (Claude
Code's local-command caveat) does not survive into the model.

### Search (feature `search`, on by default)

`txcript::search` supports fuzzy and substring search over transcripts via
[nucleo](https://github.com/helix-editor/nucleo). One-shot search:

```rust
use txcript::search::{Query, search};

let hits = search(&common, &Query::fuzzy("relay bug"));   // fzf syntax: 'exact ^prefix !not
for hit in hits {
    // hit.origin: User | Assistant | Thinking | ToolUse | ToolResult | Meta
    // hit.span addresses the message; hit.highlights are char ranges into hit.line
    let messages = common.fragment(&hit.span);            // zero-copy: Option<&[Message]>
}
```

For picker-style search, build an `Index` once and query it per keystroke:

```rust
use txcript::search::{DocKey, Index, Query};

let mut index = Index::new();
index.insert(DocKey { harness, id }, &common);   // re-insert replaces; caller owns refresh
let matches = index.query(&Query::fuzzy("srch")); // ranked docs, best lines as hits
```

An empty pattern returns documents newest-first. Tool outputs are excluded by
default; use `Origin::ALL` to include them. `Query.harnesses`, `Query.limit`,
and `Query.hits_per_doc` narrow results.

## Use as a CLI

The binary lives in its own workspace crate (`cli/`, package `txcript-cli`)
so its dependencies (clap) never touch library consumers:

```sh
cargo install --git https://github.com/skillsynchq/txcript txcript-cli   # installs `txcript`
# or from a checkout: cargo install --path cli
```

It discovers local sessions and continues one in any harness — the offline half
of replay's `continue --local`:

```sh
txcript list                             # local sessions across every harness
txcript continue <id>[#range]            # continue <id>, then launch its harness
    [--with <harness>]                    #   ...continuing in <harness> instead
    [--from <harness>]                    #   scope the id lookup to one harness
    [--out <dir>]                         #   write under <dir>; implies --no-resume
    [--no-resume]                         #   write the session but don't launch
txcript view <id>[#range]                # print a session as compact text
    [--from <harness>]                    #   scope the id lookup to one harness
```

`continue` hands the terminal to the harness when done (on Unix it `exec`s).
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}"`.

A cross-harness continue leaves the original session where it was — what is
written is always a copy; the source is never modified or removed.

`view` prints the same token-conscious text projection the MCP server serves,
with a `── #N ──` rule numbering each message. `#range` names a 1-based,
inclusive message range — `abc#7` is message 7, `abc#5-12`, `abc#5-` (from 5
on), `abc#-10` (through 10) — and the printed ordinals are the ones ranges
use, so what you see is what you reference. `continue` accepts the same
suffix and continues just those messages as a new session; ranges that cut a
tool call away from its result are refused, with the nearest valid range
suggested.

Search across every session on the machine:

```sh
txcript query 'relay bug'                # one-shot: ranked hits, highlighted
txcript query                            # fzf-style picker; Enter continues
    [--from <harness>]                   #   search only <harness> (default: all)
    [--with <harness>]                   #   continue the pick in <harness>
```

The picker is dependency-free (raw-mode ANSI): type to filter with fzf-style
fuzzy syntax, arrows / ctrl-p/n to move, Enter to continue the selection in
its own harness (or `--with`), Esc to cancel. Every row shows which kind of
content matched — user text, assistant text, thinking, tool use, tool output,
or session metadata.

Shell completions (bash, zsh, fish, elvish, powershell) come from the binary
itself:

```sh
txcript completion zsh > ~/.zfunc/_txcript      # or wherever your fpath looks
source <(txcript completion bash)               # bash, ad hoc
txcript completion fish > ~/.config/fish/completions/txcript.fish
```

Start the MCP server over the standard stdin/stdout transport:

```sh
txcript mcp
```

It exposes exactly three read-only tools. Their optional filters match the CLI:

- `list_sessions(from?, cwd?)`
- `search_sessions(pattern, from?, cwd?)`
- `read_session(id, from?)`

Omitting `from` includes every harness. Omitting `cwd` applies no directory
filter, including sessions without a recorded working directory; when `cwd` is
present, those sessions do not match.

The library also exposes `txcript::text::to_text(&common)`, a one-way,
token-conscious projection of `Transcript<Common>` for use as LLM context. It
keeps messages, reasoning text, and compact tool calls/results while omitting
replay-only payloads such as encrypted reasoning, usage accounting, and inline
image bytes. `to_text_fragment(&common, &span)` renders a `Span` of the body
in the same format with `── #N ──` rules carrying each message's 1-based
ordinal in the full session — the numbering `txcript view` prints.

## 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

```sh
bun add git+ssh://git@github.com/skillsynchq/txcript.git
```

`prepare` builds the wasm on install, so the machine needs the Rust toolchain.
Run the one-time toolchain setup, then it builds automatically:

```sh
# once per machine: wasm32 target + matching wasm-bindgen-cli
bun --cwd node_modules/txcript run setup
```

(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

```sh
git clone https://github.com/skillsynchq/txcript.git
cd txcript
bun run setup        # once: wasm target + wasm-bindgen-cli
bun run build        # produces ./pkg
```

Then import by path (e.g. as a sibling of your project), and wire it as a
prebuild step:

```jsonc
// your project's package.json
{
  "scripts": {
    "build:txcript": "cd ../txcript && bun run build",
    "prebuild": "bun run build:txcript"
  }
}
```

### API

```ts
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","cursor_desktop","grok","amp","antigravity"]
```

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, a
JSON export of Cursor's `store.db` for cursor, a JSON dump of the session's
`state.vscdb` rows for cursor_desktop, a JSON bundle of the
session directory's files for grok, and the thread JSON document — the
`amp threads export` shape — for amp, and a JSON dump of the conversation
database — hex-encoded protobuf step blobs — for antigravity); the result is
the target's native text.
Invalid harness names or unparseable input throw a JS `Error`.

## Development

```sh
cargo test                                          # native suite
cargo test --no-default-features                    # without the SQLite store
bun run build && bun examples/convert.ts <file> <from> <to>
```