npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@molecule/api-agent-transcript

v1.0.1

Published

Reads a coding agent's session export (Claude Code, Codex, Molecule IDE) into one normalized session: turns, models and the files the agent wrote

Readme

@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.