@molecule/api-agent-transcript
Auto-generated, AI-first package reference for the molecule.dev ecosystem. It is written to be read by coding agents as much as by people, and is generated from this package's source — edit
src/index.tsJSDoc, not this file.
One normalized shape for a coding agent's session, whatever harness recorded it.
Defines AgentSession — the turns of a conversation (the user's messages as
typed, the assistant's prose replies, and the files each assistant turn
wrote) — and the AgentTranscriptReader contract that format bonds
implement. Readers, one per real export format: @molecule/api-agent-transcript-claude-code
(the /export text and the session .jsonl), -codex (the Markdown export
and the rollout .jsonl), -molecule-ide (a Molecule IDE conversation's
JSON). @molecule/api-agent-transcript-autodetect bundles all three and
picks the one that recognizes each file — bond that.
The example is what most apps want transcripts for: read a post's folder of
transcript exports at build time and attribute the post's paragraphs with
@molecule/api-text-provenance, writing provenance.json. To read one
file on its own: readTranscript({ text, fileName }) returns the session.
Quick Start
// provenance.ts — runs in Node at build time (a build script or a Vite plugin), never in the page.
import { existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync } from 'node:fs'
import { dirname, join } from 'node:path'
import {
canReadTranscript,
readTranscript,
setProvider as setTranscriptReader,
} from '@molecule/api-agent-transcript'
import { provider as anyTranscript } from '@molecule/api-agent-transcript-autodetect'
import { attributeText, setProvider as setAttribution } from '@molecule/api-text-provenance'
import { provider as wordOverlap } from '@molecule/api-text-provenance-overlap'
setTranscriptReader(anyTranscript) // reads Claude Code, Codex and Molecule IDE exports
setAttribution(wordOverlap)
// One block of the post, in page order. `prompt` and `model` are on every ai span.
export interface ProvenanceSpan {
text: string // the block's markdown: a paragraph, heading, list or code block
origin: 'human' | 'ai'
prompt?: string // the person's message the AI was answering, as typed
model?: string // the model that wrote it, as the transcript names it
}
// What /<slug>/provenance.json holds.
export interface Provenance {
aiShare: number // 0..1, the share of the post's words the AI wrote
words: number
aiWords: number
prompts: string[] // the distinct prompts behind the ai spans, in page order
spans: ProvenanceSpan[]
}
// The post's top-level blocks: front matter dropped, split on blank lines, never inside a code fence.
export function markdownBlocks(markdown: string): string[] {
const body = markdown.replace(/^---\r?\n[\s\S]*?\r?\n---\r?\n/, '')
const blocks: string[] = []
let lines: string[] = []
let inFence = false
for (const line of body.split(/\r?\n/)) {
if (/^\s*(`{3}|~{3})/.test(line)) inFence = !inFence
if (!inFence && line.trim() === '') {
if (lines.length > 0) blocks.push(lines.join('\n'))
lines = []
} else {
lines.push(line)
}
}
if (lines.length > 0) blocks.push(lines.join('\n'))
return blocks
}
// Attribute one post from its markdown file and the folder holding its transcript exports.
// A missing or empty folder is a 100% human post; files that are not transcripts are skipped.
export function postProvenance(markdownFile: string, transcriptDir: string): Provenance {
const blocks = markdownBlocks(readFileSync(markdownFile, 'utf8'))
const files = existsSync(transcriptDir)
? readdirSync(transcriptDir, { withFileTypes: true })
.filter((entry) => entry.isFile())
.map((entry) => entry.name)
.sort()
: []
const sessions = files
.map((name) => ({ text: readFileSync(join(transcriptDir, name), 'utf8'), fileName: name }))
.filter((input) => canReadTranscript(input))
.map((input) => readTranscript(input))
const result = attributeText({ paragraphs: blocks, sessions })
return {
aiShare: result.aiShare,
words: result.words,
aiWords: result.aiWords,
prompts: result.prompts,
spans: result.paragraphs.map((p): ProvenanceSpan => {
const text = blocks[p.index]
return p.origin === 'ai'
? { text, origin: 'ai', prompt: p.prompt, model: p.model }
: { text, origin: 'human' }
}),
}
}
// Write provenance.json, creating its folder.
export function writeProvenance(outFile: string, provenance: Provenance): void {
mkdirSync(dirname(outFile), { recursive: true })
writeFileSync(outFile, `${JSON.stringify(provenance, null, 2)}\n`)
}
// In the build, for each PUBLISHED post (skip drafts), after the site's own build has written dist/:
// writeProvenance('dist/my-post/provenance.json', postProvenance('posts/my-post.md', 'transcripts/my-post'))
Type
core
Installation
npm install @molecule/api-agent-transcript @molecule/api-bond @molecule/api-i18n
API
Interfaces
AgentFileWrite
A file the assistant wrote or edited during a turn.
interface AgentFileWrite {
/** The path as the session recorded it (absolute or relative to the session's working directory). */
path: string
/** `create` = the whole file was written; `edit` = part of an existing file was replaced. */
kind: 'create' | 'edit'
/**
* The text the assistant put into the file: the whole file for `create`, the
* inserted/replacement text for `edit`. Empty when the export omits it.
*/
text: string
/** False when the export shows only part of the text (collapsed, truncated or elided). */
complete: boolean
}
AgentSession
A whole session, normalized.
interface AgentSession {
/** The reader that produced it, e.g. `claude-code`, `codex`, `molecule-ide`. */
format: string
/** The harness's own name for itself, e.g. `Claude Code`. */
harness: string
/** The harness version, when the export records it. */
harnessVersion?: string
/** The session's model when a single one is named for the whole session. Per-turn models are on each turn. */
model?: string
/** ISO 8601 start time, when recorded. */
startedAt?: string
/** The turns, in order. */
turns: AgentTurn[]
}
AgentTranscriptReader
The contract every transcript reader bond implements.
A reader recognizes its own format with detect() and never guesses: it
returns false for anything it does not positively recognize, so a composing
reader (@molecule/api-agent-transcript-autodetect) can try each in turn.
interface AgentTranscriptReader {
/** A stable id for the format family, e.g. `claude-code`. */
readonly format: string
/** A human label, e.g. `Claude Code`. */
readonly label: string
/**
* Whether this reader recognizes the input.
*
* @param input - The transcript.
* @returns True only when the input is positively this reader's format.
*/
detect(input: TranscriptInput): boolean
/**
* Read the transcript into a normalized session.
*
* @param input - The transcript.
* @returns The session.
* @throws {Error} When the input is not this reader's format.
*/
read(input: TranscriptInput): AgentSession
}
AgentTurn
One turn of the conversation. Consecutive assistant messages before the next user message form one turn.
interface AgentTurn {
/** Who spoke. */
role: AgentTurnRole
/**
* What was said. For `user`: the message as typed (the harness's injected
* context is never included). For `assistant`: its prose replies, joined by a
* blank line — tool calls are not prose; the files they wrote are in `files`.
*/
text: string
/** ISO 8601 time of the turn's first message, when the export records it. */
timestamp?: string
/** The model that produced an assistant turn, when the export records it. */
model?: string
/** Files an assistant turn wrote. Always empty for user turns. */
files: AgentFileWrite[]
}
TranscriptInput
A transcript to read: its text, and its file name when known (some readers use the extension as a hint).
interface TranscriptInput {
/** The file's full text. */
text: string
/** The file name or path, e.g. `session.jsonl`, `codex-session.md`. */
fileName?: string
}
Types
AgentTurnRole
Who said a turn. Tool calls and their results are folded into the assistant turn that made them.
type AgentTurnRole = 'user' | 'assistant'
Functions
canReadTranscript(input)
Whether the bonded reader recognizes a transcript.
function canReadTranscript(input: TranscriptInput): boolean
input— The transcript text and, when known, its file name.
Returns: True when the bonded reader can read it.
getProvider()
Retrieves the bonded transcript reader, throwing if none is configured.
function getProvider(): AgentTranscriptReader
Returns: The bonded reader.
hasProvider()
Checks whether a transcript reader is bonded.
function hasProvider(): boolean
Returns: true if a reader is bonded.
readTranscript(input)
Read a transcript into a normalized session with the bonded reader.
function readTranscript(input: TranscriptInput): AgentSession
input— The transcript text and, when known, its file name.
Returns: The normalized session.
setProvider(provider)
Registers a transcript reader as the active one. Called during application startup.
function setProvider(provider: AgentTranscriptReader): void
provider— The reader to bond.
Available Providers
| Provider | Package |
|---|---|
| Agent transcript autodetect | @molecule/api-agent-transcript-autodetect |
| Claude Code transcript reader | @molecule/api-agent-transcript-claude-code |
| Codex CLI transcript reader | @molecule/api-agent-transcript-codex |
| Molecule IDE transcript reader | @molecule/api-agent-transcript-molecule-ide |
Injection Notes
Requirements
Peer dependencies:
@molecule/api-bond^1.0.1@molecule/api-i18n^1.0.1
Runtime Dependencies
@molecule/api-bond@molecule/api-i18nDo NOT parse an export yourself (splitting on
---,❯,## User, role labels). The readers already drop harness noise: a user turn is only what the person typed — no environment blocks, slash-command echoes, auto-continue messages or tool results — so it is safe to show as "the prompt".Do NOT import this from page or client code. It is server-only and throws in a browser bundle; read transcripts at build time or in your API.
Do NOT call
readTranscripton every file in a folder. A file no reader recognizes throws. Filter withcanReadTranscript()first, as the example does, so a README beside the exports is skipped.Keep each transcript's original file name and pass it as
fileName— it is a detection hint.Assistant text is prose only. Tool calls are not text; the files they wrote are in
turn.files(create= whole file,edit= the replacement text), and content an agent put in a file counts as the agent's writing.Exports lose detail, and
complete: falsesays so. Claude Code's/exportrenders markdown (headings lose their#, bold loses its**) and collapses some edits; prefer its session.jsonlwhen you have it.Parsing is pure and synchronous; nothing is fetched or written.