npm.io
1.0.1 • Published 5h ago

@molecule/api-agent-transcript

Licence
Apache-2.0
Version
1.0.1
Deps
0
Size
42 kB
Vulns
0
Weekly
0
Stars
44

@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.ts JSDoc, 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-i18n

  • Do 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 readTranscript on every file in a folder. A file no reader recognizes throws. Filter with canReadTranscript() 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: false says so. Claude Code's /export renders markdown (headings lose their #, bold loses its **) and collapses some edits; prefer its session .jsonl when you have it.

  • Parsing is pure and synchronous; nothing is fetched or written.

Keywords