Export, normalize and convert coding-agent session transcripts. sessionport
is a small, zero-dependency CLI that reads a session from one coding agent and
writes it out in another format — normalizing everything through a single
canonical schema so you can archive, diff, share, or move conversations between
tools.
Status: early. It speaks the three major LLM API message formats (OpenAI, Anthropic, Gemini) plus specific agent CLIs (Claude Code, Codex) and the ChatGPT data export. Vendor exporters are best-effort for text content and are not guaranteed to be replayable by the original app — treat them as archival/handoff output, not a perfect restore. See Validation status below for how thoroughly each format has been checked.
People routinely switch between agent CLIs (Claude Code, Codex, and others), but
each stores its session transcript in its own JSONL shape. There's no common way
to read one, hand it to another, or export a clean copy for review. sessionport
gives you a single convert command and a documented canonical format.
Published as @williamsuchun/sessionport
(the unscoped name was taken).
# Run without installing
npx @williamsuchun/sessionport formats
# Or install globally (exposes the `sessionport` command)
npm install -g @williamsuchun/sessionportFrom source
git clone https://github.com/williamsuchun/sessionport && cd sessionport
npm install && npm run build
npm link # exposes `sessionport` on your PATHRequires Node.js >= 20.
# Auto-detect input, convert to a portable canonical JSON
sessionport convert session.jsonl --to portable -o session.portable.json
# Export a Codex session to readable Markdown
sessionport convert ~/.codex/sessions/2026/.../rollout-*.jsonl --to markdown -o out.md
# Move a Claude Code transcript into the OpenAI chat shape
sessionport convert ~/.claude/projects/<proj>/<id>.jsonl --from claude-code --to openai-chat
# Summarize a session without converting it
sessionport inspect session.jsonl
# List supported formats
sessionport formatsinspect prints only metadata and per-role message counts (no message bodies):
source: codex
sessionId: 019f3b85-…
cwd: /path/to/project
messages: 411
by role: system=4 user=6 assistant=67 tool=334
metadata: {"cliVersion":"0.142.5","modelProvider":"azure"}
sessionport speaks the three major LLM API message shapes as a generic substrate, plus specific agent CLIs and export files:
| Format | Import | Export | Kind | Notes |
|---|---|---|---|---|
portable |
✓ | ✓ | canonical | sessionport's own schema (sessionport/v1); lossless round-trip |
anthropic-messages |
✓ | ✓ | API standard | Anthropic Messages API ([{role,content-blocks}]); also Cline / Roo Code api_conversation_history.json |
openai-chat |
✓ | ✓ | API standard | OpenAI Chat Completions ({messages:[{role,content,tool_calls}]}) |
gemini |
✓ | ✓ | API standard | Google Gemini Content ({role,parts}); Gemini API / gemini-cli |
claude-code |
✓ | ✓ | agent CLI | Claude Code transcript JSONL (~/.claude/projects/**/<id>.jsonl) |
codex |
✓ | ✓ | agent CLI | Codex CLI rollout JSONL (~/.codex/sessions/**/rollout-*.jsonl) |
chatgpt-export |
✓ | export | OpenAI ChatGPT data export conversations.json (tree, linearized) |
|
markdown |
✓ | human | Human-readable transcript |
Input format is auto-detected by default; override with --from.
Formats differ in how thoroughly they've been checked against real data:
- Validated against real local files:
claude-code,codex,anthropic-messages(verified against a real Clineapi_conversation_history.json). - Implemented from documented API shapes:
openai-chat,gemini— the message structure follows each provider's public API; the exact on-disk wrapper used by a given CLI may vary, so pass--fromif detection is unsure. - Implemented from the widely-documented export shape:
chatgpt-export— not yet checked against a fresh export.
If an importer misreads a file from a client you use, that's a bug worth an issue — please attach the (redacted) structure.
There is no industry standard for the session envelope (session id, working
directory, timestamps), so sessionport keeps a small custom canonical model
(sessionport/v1). At the message level, though, almost every agent reduces
to one of three provider API shapes — OpenAI, Anthropic, Gemini —
so those are first-class adapters. Many clients persist raw API messages
(e.g. Cline/Roo store Anthropic messages verbatim), so one API adapter often
covers several clients. Telemetry schemas (OpenTelemetry GenAI) and protocols
(MCP) were considered and deliberately not used as the canonical model — they
describe spans/RPC, not stored transcripts.
Roles are normalized to user | assistant | system | tool.
These mappings are derived from actual local session files:
- Claude Code — each JSONL line is an event;
user/assistantevents carry amessagewhosecontentis either a string or an array of blocks. Text blocks become message text;tool_use/tool_resultblocks become separatetoolmessages. Content blocks that aren't text (e.g. images) become compact placeholders like[image]rather than being dropped or dumped as base64. Non-conversation events (queue operations, file-history snapshots) are skipped. - Codex — the conversation is taken from
response_itementries ofpayload.type === "message"/agent_message(content blocksinput_text/output_text);function_call,function_call_output, and custom tool calls becometoolmessages, with list-form outputs flattened the same way. UIevent_msgentries and encryptedreasoningitems are skipped.
- Importers preserve text; non-text blocks (images, references) are reduced to placeholders, and vendor-specific fields (reasoning, encrypted content, token accounting) are not carried into the canonical model.
- The
openai-chatexporter emits an API-valid shape (tool calls asassistant.tool_calls, results asrole:"tool"); when the source lacked call ids they are synthesized best-effort, so call/result pairing may be imperfect. - Vendor exporters (
claude-code,codex) are best-effort for text and are not guaranteed to be replayable by the original app.portableis the only lossless round-trip format. - Auto-detection is heuristic; pass
--fromif a file is ambiguous. - On-disk formats can change between agent versions; importers target the shapes observed at time of writing and may need updates.
npm run build # tsc -> dist/
npm test # build + vitest
npm run test:watchMIT
{ "schema": "sessionport/v1", "source": "claude-code", "sessionId": "…", "cwd": "/path/to/project", "createdAt": "2026-01-01T00:00:00.000Z", "metadata": { "version": "2.1.0" }, "messages": [ { "role": "user", "text": "…", "timestamp": "…" }, { "role": "assistant", "text": "…" }, { "role": "tool", "toolName": "read_file", "toolCallId": "tu_1", "text": "…", "meta": { "kind": "tool_use" } } ] }