@molecule/api-agent-transcript-autodetect 1.0.0 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -3,7 +3,7 @@ AUTO-GENERATED — DO NOT EDIT THIS FILE.
3
3
  Generated by `mlcl sync-docs` from the package's src/index.ts JSDoc + mlcl/registry.json.
4
4
  Edits here are overwritten on the next commit (molecule's pre-commit hook regenerates).
5
5
  To change this document, edit the module-level JSDoc in src/index.ts.
6
- Generated: 2026-09-24T06:06:46.371Z
6
+ Generated: 2026-09-29T16:49:56.713Z
7
7
  -->
8
8
 
9
9
  # @molecule/api-agent-transcript-autodetect
@@ -15,25 +15,119 @@ Generated: 2026-09-24T06:06:46.371Z
15
15
  The transcript reader to bond when files can come from more than one harness.
16
16
 
17
17
  Implements `@molecule/api-agent-transcript` by trying each bundled reader's
18
- `detect()` and delegating to the first that recognizes the file: Claude Code
19
- (`/export` text, session `.jsonl`), Codex CLI (Markdown export, rollout
20
- `.jsonl`) and the Molecule IDE (stored conversation JSON). `createReader()`
21
- composes any other list, including readers of your own.
18
+ `detect()` and delegating to the first that recognizes the file:
19
+
20
+ | Harness | Files |
21
+ | ------------------- | ---------------------------------------------------------------------------------- |
22
+ | Claude Code | `/export` text, session `.jsonl` |
23
+ | Codex CLI | Markdown export, rollout `.jsonl` |
24
+ | Molecule IDE | stored conversation JSON |
25
+ | Gemini CLI | session `.jsonl` / `.json`, `/chat save` checkpoint |
26
+ | Cline / Roo Code | a task's `ui_messages.json` |
27
+ | OpenCode | `opencode export` JSON |
28
+ | GitHub Copilot Chat | VS Code's "Export Chat…" `chat.json` |
29
+ | Cursor | "Export Chat" Markdown |
30
+ | Aider | `.aider.chat.history.md` |
31
+ | anything else | a Markdown / text chat with `## User`, `**User:**` or `User:` markers (tried last) |
32
+
33
+ `createReader()` composes any other list, including readers of your own;
34
+ `harnessReaders` is the list without the generic Markdown chat reader. The example is the
35
+ usual job: read a post's folder of transcript exports at build time and
36
+ attribute its paragraphs with `@molecule/api-text-provenance`, writing
37
+ `provenance.json`.
22
38
 
23
39
  ## Quick Start
24
40
 
25
41
  ```typescript
26
- import { readdirSync, readFileSync } from 'node:fs'
27
- import { join } from 'node:path'
28
- import { readTranscript, setProvider } from '@molecule/api-agent-transcript'
29
- import { provider } from '@molecule/api-agent-transcript-autodetect'
42
+ // provenance.ts — runs in Node at build time (a build script or a Vite plugin), never in the page.
43
+ import { existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync } from 'node:fs'
44
+ import { dirname, join } from 'node:path'
45
+
46
+ import {
47
+ canReadTranscript,
48
+ readTranscript,
49
+ setProvider as setTranscriptReader,
50
+ } from '@molecule/api-agent-transcript'
51
+ import { provider as anyTranscript } from '@molecule/api-agent-transcript-autodetect'
52
+ import { attributeText, setProvider as setAttribution } from '@molecule/api-text-provenance'
53
+ import { provider as wordOverlap } from '@molecule/api-text-provenance-overlap'
54
+
55
+ setTranscriptReader(anyTranscript) // reads every supported harness, plus plain Markdown chats
56
+ setAttribution(wordOverlap)
57
+
58
+ // One block of the post, in page order. `prompt` and `model` are on every ai span.
59
+ export interface ProvenanceSpan {
60
+ text: string // the block's markdown: a paragraph, heading, list or code block
61
+ origin: 'human' | 'ai'
62
+ prompt?: string // the person's message the AI was answering, as typed
63
+ model?: string // the model that wrote it, as the transcript names it
64
+ }
65
+
66
+ // What /<slug>/provenance.json holds.
67
+ export interface Provenance {
68
+ aiShare: number // 0..1, the share of the post's words the AI wrote
69
+ words: number
70
+ aiWords: number
71
+ prompts: string[] // the distinct prompts behind the ai spans, in page order
72
+ spans: ProvenanceSpan[]
73
+ }
30
74
 
31
- setProvider(provider)
75
+ // The post's top-level blocks: front matter dropped, split on blank lines, never inside a code fence.
76
+ export function markdownBlocks(markdown: string): string[] {
77
+ const body = markdown.replace(/^---\r?\n[\s\S]*?\r?\n---\r?\n/, '')
78
+ const blocks: string[] = []
79
+ let lines: string[] = []
80
+ let inFence = false
81
+ for (const line of body.split(/\r?\n/)) {
82
+ if (/^\s*(`{3}|~{3})/.test(line)) inFence = !inFence
83
+ if (!inFence && line.trim() === '') {
84
+ if (lines.length > 0) blocks.push(lines.join('\n'))
85
+ lines = []
86
+ } else {
87
+ lines.push(line)
88
+ }
89
+ }
90
+ if (lines.length > 0) blocks.push(lines.join('\n'))
91
+ return blocks
92
+ }
93
+
94
+ // Attribute one post from its markdown file and the folder holding its transcript exports.
95
+ // A missing or empty folder is a 100% human post; files that are not transcripts are skipped.
96
+ export function postProvenance(markdownFile: string, transcriptDir: string): Provenance {
97
+ const blocks = markdownBlocks(readFileSync(markdownFile, 'utf8'))
98
+ const files = existsSync(transcriptDir)
99
+ ? readdirSync(transcriptDir, { withFileTypes: true })
100
+ .filter((entry) => entry.isFile())
101
+ .map((entry) => entry.name)
102
+ .sort()
103
+ : []
104
+ const sessions = files
105
+ .map((name) => ({ text: readFileSync(join(transcriptDir, name), 'utf8'), fileName: name }))
106
+ .filter((input) => canReadTranscript(input))
107
+ .map((input) => readTranscript(input))
108
+ const result = attributeText({ paragraphs: blocks, sessions })
109
+ return {
110
+ aiShare: result.aiShare,
111
+ words: result.words,
112
+ aiWords: result.aiWords,
113
+ prompts: result.prompts,
114
+ spans: result.paragraphs.map((p): ProvenanceSpan => {
115
+ const text = blocks[p.index]
116
+ return p.origin === 'ai'
117
+ ? { text, origin: 'ai', prompt: p.prompt, model: p.model }
118
+ : { text, origin: 'human' }
119
+ }),
120
+ }
121
+ }
32
122
 
33
- const dir = 'transcripts/my-post'
34
- const sessions = readdirSync(dir).map((f) =>
35
- readTranscript({ text: readFileSync(join(dir, f), 'utf8'), fileName: f }),
36
- )
123
+ // Write provenance.json, creating its folder.
124
+ export function writeProvenance(outFile: string, provenance: Provenance): void {
125
+ mkdirSync(dirname(outFile), { recursive: true })
126
+ writeFileSync(outFile, `${JSON.stringify(provenance, null, 2)}\n`)
127
+ }
128
+
129
+ // In the build, for each PUBLISHED post (skip drafts), after the site's own build has written dist/:
130
+ // writeProvenance('dist/my-post/provenance.json', postProvenance('posts/my-post.md', 'transcripts/my-post'))
37
131
  ```
38
132
 
39
133
  ## Type
@@ -43,7 +137,7 @@ const sessions = readdirSync(dir).map((f) =>
43
137
  ## Installation
44
138
 
45
139
  ```bash
46
- npm install @molecule/api-agent-transcript-autodetect @molecule/api-agent-transcript @molecule/api-agent-transcript-claude-code @molecule/api-agent-transcript-codex @molecule/api-agent-transcript-molecule-ide
140
+ npm install @molecule/api-agent-transcript-autodetect @molecule/api-agent-transcript @molecule/api-agent-transcript-aider @molecule/api-agent-transcript-claude-code @molecule/api-agent-transcript-cline @molecule/api-agent-transcript-codex @molecule/api-agent-transcript-copilot-chat @molecule/api-agent-transcript-cursor @molecule/api-agent-transcript-gemini-cli @molecule/api-agent-transcript-markdown-chat @molecule/api-agent-transcript-molecule-ide @molecule/api-agent-transcript-opencode
47
141
  ```
48
142
 
49
143
  ## API
@@ -64,9 +158,24 @@ function createReader(readers: readonly AgentTranscriptReader[]): AgentTranscrip
64
158
 
65
159
  ### Constants
66
160
 
161
+ #### `harnessReaders`
162
+
163
+ Every harness-specific reader, in the order they are tried. Each accepts
164
+ only its own harness's files, so the order only matters for the generic
165
+ Markdown chat reader, which is not in this list. Compose
166
+ `createReader(harnessReaders)` when only a real harness's own file should
167
+ be read.
168
+
169
+ ```typescript
170
+ const harnessReaders: readonly AgentTranscriptReader[]
171
+ ```
172
+
67
173
  #### `provider`
68
174
 
69
- Reads any Claude Code, Codex CLI or Molecule IDE transcript.
175
+ Reads a transcript from any supported harness — Claude Code, Codex CLI,
176
+ the Molecule IDE, Gemini CLI, Cline / Roo Code, OpenCode, GitHub Copilot
177
+ Chat, Cursor and Aider — and, last, any plain Markdown / text chat with
178
+ User / Assistant speaker markers.
70
179
 
71
180
  ```typescript
72
181
  const provider: AgentTranscriptReader
@@ -100,12 +209,32 @@ Peer dependencies:
100
209
  ### Runtime Dependencies
101
210
 
102
211
  - `@molecule/api-agent-transcript`
212
+ - `@molecule/api-agent-transcript-aider`
103
213
  - `@molecule/api-agent-transcript-claude-code`
214
+ - `@molecule/api-agent-transcript-cline`
104
215
  - `@molecule/api-agent-transcript-codex`
216
+ - `@molecule/api-agent-transcript-copilot-chat`
217
+ - `@molecule/api-agent-transcript-cursor`
218
+ - `@molecule/api-agent-transcript-gemini-cli`
219
+ - `@molecule/api-agent-transcript-markdown-chat`
105
220
  - `@molecule/api-agent-transcript-molecule-ide`
106
-
107
- - A file no reader recognizes throws, naming the file and the readers
108
- tried — it is never read as an empty session. Check `canReadTranscript()`
109
- first to skip non-transcript files (a README beside the exports).
110
- - Order matters only for a file two readers would both accept; the bundled
111
- readers accept disjoint formats.
221
+ - `@molecule/api-agent-transcript-opencode`
222
+
223
+ - **Do NOT call `readTranscript` on every file in a folder.** A file no
224
+ reader recognizes throws, naming the file and the readers tried — it is
225
+ never read as an empty session. Filter with `canReadTranscript()` first,
226
+ as the example does.
227
+ - **Do NOT parse an export yourself or bond a single-format reader "to be
228
+ safe".** This bond reads every format above; a user turn is already only
229
+ what the person typed.
230
+ - **A plain chat with speaker markers is read too — as `markdown-chat`,
231
+ never as a harness.** When only a real harness's own file may count
232
+ (attributing text to a model, say), check `session.format !==
233
+ 'markdown-chat'`, or bond `createReader(harnessReaders)` instead.
234
+ - **Do NOT import this from page or client code** — it is server-only and
235
+ throws in a browser bundle.
236
+ - Install all four packages the example uses:
237
+ `npm install @molecule/api-agent-transcript @molecule/api-agent-transcript-autodetect @molecule/api-text-provenance @molecule/api-text-provenance-overlap`.
238
+ - Order matters only for a file two readers would both accept. The harness
239
+ readers accept disjoint formats; the generic Markdown chat reader is
240
+ tried last, so it never claims a harness's own file.
package/dist/index.d.ts CHANGED
@@ -2,32 +2,139 @@
2
2
  * The transcript reader to bond when files can come from more than one harness.
3
3
  *
4
4
  * Implements `@molecule/api-agent-transcript` by trying each bundled reader's
5
- * `detect()` and delegating to the first that recognizes the file: Claude Code
6
- * (`/export` text, session `.jsonl`), Codex CLI (Markdown export, rollout
7
- * `.jsonl`) and the Molecule IDE (stored conversation JSON). `createReader()`
8
- * composes any other list, including readers of your own.
5
+ * `detect()` and delegating to the first that recognizes the file:
6
+ *
7
+ * | Harness | Files |
8
+ * |---|---|
9
+ * | Claude Code | `/export` text, session `.jsonl` |
10
+ * | Codex CLI | Markdown export, rollout `.jsonl` |
11
+ * | Molecule IDE | stored conversation JSON |
12
+ * | Gemini CLI | session `.jsonl` / `.json`, `/chat save` checkpoint |
13
+ * | Cline / Roo Code | a task's `ui_messages.json` |
14
+ * | OpenCode | `opencode export` JSON |
15
+ * | GitHub Copilot Chat | VS Code's "Export Chat…" `chat.json` |
16
+ * | Cursor | "Export Chat" Markdown |
17
+ * | Aider | `.aider.chat.history.md` |
18
+ * | anything else | a Markdown / text chat with `## User`, `**User:**` or `User:` markers (tried last) |
19
+ *
20
+ * `createReader()` composes any other list, including readers of your own;
21
+ * `harnessReaders` is the list without the generic Markdown chat reader. The example is the
22
+ * usual job: read a post's folder of transcript exports at build time and
23
+ * attribute its paragraphs with `@molecule/api-text-provenance`, writing
24
+ * `provenance.json`.
9
25
  *
10
26
  * @example
11
27
  * ```typescript
12
- * import { readdirSync, readFileSync } from 'node:fs'
13
- * import { join } from 'node:path'
14
- * import { readTranscript, setProvider } from '@molecule/api-agent-transcript'
15
- * import { provider } from '@molecule/api-agent-transcript-autodetect'
28
+ * // provenance.ts — runs in Node at build time (a build script or a Vite plugin), never in the page.
29
+ * import { existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync } from 'node:fs'
30
+ * import { dirname, join } from 'node:path'
31
+ *
32
+ * import {
33
+ * canReadTranscript,
34
+ * readTranscript,
35
+ * setProvider as setTranscriptReader,
36
+ * } from '@molecule/api-agent-transcript'
37
+ * import { provider as anyTranscript } from '@molecule/api-agent-transcript-autodetect'
38
+ * import { attributeText, setProvider as setAttribution } from '@molecule/api-text-provenance'
39
+ * import { provider as wordOverlap } from '@molecule/api-text-provenance-overlap'
40
+ *
41
+ * setTranscriptReader(anyTranscript) // reads every supported harness, plus plain Markdown chats
42
+ * setAttribution(wordOverlap)
43
+ *
44
+ * // One block of the post, in page order. `prompt` and `model` are on every ai span.
45
+ * export interface ProvenanceSpan {
46
+ * text: string // the block's markdown: a paragraph, heading, list or code block
47
+ * origin: 'human' | 'ai'
48
+ * prompt?: string // the person's message the AI was answering, as typed
49
+ * model?: string // the model that wrote it, as the transcript names it
50
+ * }
51
+ *
52
+ * // What /<slug>/provenance.json holds.
53
+ * export interface Provenance {
54
+ * aiShare: number // 0..1, the share of the post's words the AI wrote
55
+ * words: number
56
+ * aiWords: number
57
+ * prompts: string[] // the distinct prompts behind the ai spans, in page order
58
+ * spans: ProvenanceSpan[]
59
+ * }
60
+ *
61
+ * // The post's top-level blocks: front matter dropped, split on blank lines, never inside a code fence.
62
+ * export function markdownBlocks(markdown: string): string[] {
63
+ * const body = markdown.replace(/^---\r?\n[\s\S]*?\r?\n---\r?\n/, '')
64
+ * const blocks: string[] = []
65
+ * let lines: string[] = []
66
+ * let inFence = false
67
+ * for (const line of body.split(/\r?\n/)) {
68
+ * if (/^\s*(`{3}|~{3})/.test(line)) inFence = !inFence
69
+ * if (!inFence && line.trim() === '') {
70
+ * if (lines.length > 0) blocks.push(lines.join('\n'))
71
+ * lines = []
72
+ * } else {
73
+ * lines.push(line)
74
+ * }
75
+ * }
76
+ * if (lines.length > 0) blocks.push(lines.join('\n'))
77
+ * return blocks
78
+ * }
79
+ *
80
+ * // Attribute one post from its markdown file and the folder holding its transcript exports.
81
+ * // A missing or empty folder is a 100% human post; files that are not transcripts are skipped.
82
+ * export function postProvenance(markdownFile: string, transcriptDir: string): Provenance {
83
+ * const blocks = markdownBlocks(readFileSync(markdownFile, 'utf8'))
84
+ * const files = existsSync(transcriptDir)
85
+ * ? readdirSync(transcriptDir, { withFileTypes: true })
86
+ * .filter((entry) => entry.isFile())
87
+ * .map((entry) => entry.name)
88
+ * .sort()
89
+ * : []
90
+ * const sessions = files
91
+ * .map((name) => ({ text: readFileSync(join(transcriptDir, name), 'utf8'), fileName: name }))
92
+ * .filter((input) => canReadTranscript(input))
93
+ * .map((input) => readTranscript(input))
94
+ * const result = attributeText({ paragraphs: blocks, sessions })
95
+ * return {
96
+ * aiShare: result.aiShare,
97
+ * words: result.words,
98
+ * aiWords: result.aiWords,
99
+ * prompts: result.prompts,
100
+ * spans: result.paragraphs.map((p): ProvenanceSpan => {
101
+ * const text = blocks[p.index]
102
+ * return p.origin === 'ai'
103
+ * ? { text, origin: 'ai', prompt: p.prompt, model: p.model }
104
+ * : { text, origin: 'human' }
105
+ * }),
106
+ * }
107
+ * }
16
108
  *
17
- * setProvider(provider)
109
+ * // Write provenance.json, creating its folder.
110
+ * export function writeProvenance(outFile: string, provenance: Provenance): void {
111
+ * mkdirSync(dirname(outFile), { recursive: true })
112
+ * writeFileSync(outFile, `${JSON.stringify(provenance, null, 2)}\n`)
113
+ * }
18
114
  *
19
- * const dir = 'transcripts/my-post'
20
- * const sessions = readdirSync(dir).map((f) =>
21
- * readTranscript({ text: readFileSync(join(dir, f), 'utf8'), fileName: f }),
22
- * )
115
+ * // In the build, for each PUBLISHED post (skip drafts), after the site's own build has written dist/:
116
+ * // writeProvenance('dist/my-post/provenance.json', postProvenance('posts/my-post.md', 'transcripts/my-post'))
23
117
  * ```
24
118
  *
25
119
  * @remarks
26
- * - A file no reader recognizes throws, naming the file and the readers
27
- * tried — it is never read as an empty session. Check `canReadTranscript()`
28
- * first to skip non-transcript files (a README beside the exports).
29
- * - Order matters only for a file two readers would both accept; the bundled
30
- * readers accept disjoint formats.
120
+ * - **Do NOT call `readTranscript` on every file in a folder.** A file no
121
+ * reader recognizes throws, naming the file and the readers tried — it is
122
+ * never read as an empty session. Filter with `canReadTranscript()` first,
123
+ * as the example does.
124
+ * - **Do NOT parse an export yourself or bond a single-format reader "to be
125
+ * safe".** This bond reads every format above; a user turn is already only
126
+ * what the person typed.
127
+ * - **A plain chat with speaker markers is read too — as `markdown-chat`,
128
+ * never as a harness.** When only a real harness's own file may count
129
+ * (attributing text to a model, say), check `session.format !==
130
+ * 'markdown-chat'`, or bond `createReader(harnessReaders)` instead.
131
+ * - **Do NOT import this from page or client code** — it is server-only and
132
+ * throws in a browser bundle.
133
+ * - Install all four packages the example uses:
134
+ * `npm install @molecule/api-agent-transcript @molecule/api-agent-transcript-autodetect @molecule/api-text-provenance @molecule/api-text-provenance-overlap`.
135
+ * - Order matters only for a file two readers would both accept. The harness
136
+ * readers accept disjoint formats; the generic Markdown chat reader is
137
+ * tried last, so it never claims a harness's own file.
31
138
  *
32
139
  * @module
33
140
  */
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgCG;AAEH,cAAc,oBAAoB,CAAA;AAClC,cAAc,eAAe,CAAA"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2IG;AAEH,cAAc,oBAAoB,CAAA;AAClC,cAAc,eAAe,CAAA"}
package/dist/index.js CHANGED
@@ -2,32 +2,139 @@
2
2
  * The transcript reader to bond when files can come from more than one harness.
3
3
  *
4
4
  * Implements `@molecule/api-agent-transcript` by trying each bundled reader's
5
- * `detect()` and delegating to the first that recognizes the file: Claude Code
6
- * (`/export` text, session `.jsonl`), Codex CLI (Markdown export, rollout
7
- * `.jsonl`) and the Molecule IDE (stored conversation JSON). `createReader()`
8
- * composes any other list, including readers of your own.
5
+ * `detect()` and delegating to the first that recognizes the file:
6
+ *
7
+ * | Harness | Files |
8
+ * |---|---|
9
+ * | Claude Code | `/export` text, session `.jsonl` |
10
+ * | Codex CLI | Markdown export, rollout `.jsonl` |
11
+ * | Molecule IDE | stored conversation JSON |
12
+ * | Gemini CLI | session `.jsonl` / `.json`, `/chat save` checkpoint |
13
+ * | Cline / Roo Code | a task's `ui_messages.json` |
14
+ * | OpenCode | `opencode export` JSON |
15
+ * | GitHub Copilot Chat | VS Code's "Export Chat…" `chat.json` |
16
+ * | Cursor | "Export Chat" Markdown |
17
+ * | Aider | `.aider.chat.history.md` |
18
+ * | anything else | a Markdown / text chat with `## User`, `**User:**` or `User:` markers (tried last) |
19
+ *
20
+ * `createReader()` composes any other list, including readers of your own;
21
+ * `harnessReaders` is the list without the generic Markdown chat reader. The example is the
22
+ * usual job: read a post's folder of transcript exports at build time and
23
+ * attribute its paragraphs with `@molecule/api-text-provenance`, writing
24
+ * `provenance.json`.
9
25
  *
10
26
  * @example
11
27
  * ```typescript
12
- * import { readdirSync, readFileSync } from 'node:fs'
13
- * import { join } from 'node:path'
14
- * import { readTranscript, setProvider } from '@molecule/api-agent-transcript'
15
- * import { provider } from '@molecule/api-agent-transcript-autodetect'
28
+ * // provenance.ts — runs in Node at build time (a build script or a Vite plugin), never in the page.
29
+ * import { existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync } from 'node:fs'
30
+ * import { dirname, join } from 'node:path'
31
+ *
32
+ * import {
33
+ * canReadTranscript,
34
+ * readTranscript,
35
+ * setProvider as setTranscriptReader,
36
+ * } from '@molecule/api-agent-transcript'
37
+ * import { provider as anyTranscript } from '@molecule/api-agent-transcript-autodetect'
38
+ * import { attributeText, setProvider as setAttribution } from '@molecule/api-text-provenance'
39
+ * import { provider as wordOverlap } from '@molecule/api-text-provenance-overlap'
40
+ *
41
+ * setTranscriptReader(anyTranscript) // reads every supported harness, plus plain Markdown chats
42
+ * setAttribution(wordOverlap)
43
+ *
44
+ * // One block of the post, in page order. `prompt` and `model` are on every ai span.
45
+ * export interface ProvenanceSpan {
46
+ * text: string // the block's markdown: a paragraph, heading, list or code block
47
+ * origin: 'human' | 'ai'
48
+ * prompt?: string // the person's message the AI was answering, as typed
49
+ * model?: string // the model that wrote it, as the transcript names it
50
+ * }
51
+ *
52
+ * // What /<slug>/provenance.json holds.
53
+ * export interface Provenance {
54
+ * aiShare: number // 0..1, the share of the post's words the AI wrote
55
+ * words: number
56
+ * aiWords: number
57
+ * prompts: string[] // the distinct prompts behind the ai spans, in page order
58
+ * spans: ProvenanceSpan[]
59
+ * }
60
+ *
61
+ * // The post's top-level blocks: front matter dropped, split on blank lines, never inside a code fence.
62
+ * export function markdownBlocks(markdown: string): string[] {
63
+ * const body = markdown.replace(/^---\r?\n[\s\S]*?\r?\n---\r?\n/, '')
64
+ * const blocks: string[] = []
65
+ * let lines: string[] = []
66
+ * let inFence = false
67
+ * for (const line of body.split(/\r?\n/)) {
68
+ * if (/^\s*(`{3}|~{3})/.test(line)) inFence = !inFence
69
+ * if (!inFence && line.trim() === '') {
70
+ * if (lines.length > 0) blocks.push(lines.join('\n'))
71
+ * lines = []
72
+ * } else {
73
+ * lines.push(line)
74
+ * }
75
+ * }
76
+ * if (lines.length > 0) blocks.push(lines.join('\n'))
77
+ * return blocks
78
+ * }
79
+ *
80
+ * // Attribute one post from its markdown file and the folder holding its transcript exports.
81
+ * // A missing or empty folder is a 100% human post; files that are not transcripts are skipped.
82
+ * export function postProvenance(markdownFile: string, transcriptDir: string): Provenance {
83
+ * const blocks = markdownBlocks(readFileSync(markdownFile, 'utf8'))
84
+ * const files = existsSync(transcriptDir)
85
+ * ? readdirSync(transcriptDir, { withFileTypes: true })
86
+ * .filter((entry) => entry.isFile())
87
+ * .map((entry) => entry.name)
88
+ * .sort()
89
+ * : []
90
+ * const sessions = files
91
+ * .map((name) => ({ text: readFileSync(join(transcriptDir, name), 'utf8'), fileName: name }))
92
+ * .filter((input) => canReadTranscript(input))
93
+ * .map((input) => readTranscript(input))
94
+ * const result = attributeText({ paragraphs: blocks, sessions })
95
+ * return {
96
+ * aiShare: result.aiShare,
97
+ * words: result.words,
98
+ * aiWords: result.aiWords,
99
+ * prompts: result.prompts,
100
+ * spans: result.paragraphs.map((p): ProvenanceSpan => {
101
+ * const text = blocks[p.index]
102
+ * return p.origin === 'ai'
103
+ * ? { text, origin: 'ai', prompt: p.prompt, model: p.model }
104
+ * : { text, origin: 'human' }
105
+ * }),
106
+ * }
107
+ * }
16
108
  *
17
- * setProvider(provider)
109
+ * // Write provenance.json, creating its folder.
110
+ * export function writeProvenance(outFile: string, provenance: Provenance): void {
111
+ * mkdirSync(dirname(outFile), { recursive: true })
112
+ * writeFileSync(outFile, `${JSON.stringify(provenance, null, 2)}\n`)
113
+ * }
18
114
  *
19
- * const dir = 'transcripts/my-post'
20
- * const sessions = readdirSync(dir).map((f) =>
21
- * readTranscript({ text: readFileSync(join(dir, f), 'utf8'), fileName: f }),
22
- * )
115
+ * // In the build, for each PUBLISHED post (skip drafts), after the site's own build has written dist/:
116
+ * // writeProvenance('dist/my-post/provenance.json', postProvenance('posts/my-post.md', 'transcripts/my-post'))
23
117
  * ```
24
118
  *
25
119
  * @remarks
26
- * - A file no reader recognizes throws, naming the file and the readers
27
- * tried — it is never read as an empty session. Check `canReadTranscript()`
28
- * first to skip non-transcript files (a README beside the exports).
29
- * - Order matters only for a file two readers would both accept; the bundled
30
- * readers accept disjoint formats.
120
+ * - **Do NOT call `readTranscript` on every file in a folder.** A file no
121
+ * reader recognizes throws, naming the file and the readers tried — it is
122
+ * never read as an empty session. Filter with `canReadTranscript()` first,
123
+ * as the example does.
124
+ * - **Do NOT parse an export yourself or bond a single-format reader "to be
125
+ * safe".** This bond reads every format above; a user turn is already only
126
+ * what the person typed.
127
+ * - **A plain chat with speaker markers is read too — as `markdown-chat`,
128
+ * never as a harness.** When only a real harness's own file may count
129
+ * (attributing text to a model, say), check `session.format !==
130
+ * 'markdown-chat'`, or bond `createReader(harnessReaders)` instead.
131
+ * - **Do NOT import this from page or client code** — it is server-only and
132
+ * throws in a browser bundle.
133
+ * - Install all four packages the example uses:
134
+ * `npm install @molecule/api-agent-transcript @molecule/api-agent-transcript-autodetect @molecule/api-text-provenance @molecule/api-text-provenance-overlap`.
135
+ * - Order matters only for a file two readers would both accept. The harness
136
+ * readers accept disjoint formats; the generic Markdown chat reader is
137
+ * tried last, so it never claims a harness's own file.
31
138
  *
32
139
  * @module
33
140
  */
@@ -12,7 +12,18 @@ import type { AgentTranscriptReader } from '@molecule/api-agent-transcript';
12
12
  */
13
13
  export declare function createReader(readers: readonly AgentTranscriptReader[]): AgentTranscriptReader;
14
14
  /**
15
- * Reads any Claude Code, Codex CLI or Molecule IDE transcript.
15
+ * Every harness-specific reader, in the order they are tried. Each accepts
16
+ * only its own harness's files, so the order only matters for the generic
17
+ * Markdown chat reader, which is not in this list. Compose
18
+ * `createReader(harnessReaders)` when only a real harness's own file should
19
+ * be read.
20
+ */
21
+ export declare const harnessReaders: readonly AgentTranscriptReader[];
22
+ /**
23
+ * Reads a transcript from any supported harness — Claude Code, Codex CLI,
24
+ * the Molecule IDE, Gemini CLI, Cline / Roo Code, OpenCode, GitHub Copilot
25
+ * Chat, Cursor and Aider — and, last, any plain Markdown / text chat with
26
+ * User / Assistant speaker markers.
16
27
  */
17
28
  export declare const provider: AgentTranscriptReader;
18
29
  //# sourceMappingURL=provider.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"provider.d.ts","sourceRoot":"","sources":["../src/provider.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,KAAK,EAEV,qBAAqB,EAEtB,MAAM,gCAAgC,CAAA;AAKvC;;;;;GAKG;AACH,wBAAgB,YAAY,CAAC,OAAO,EAAE,SAAS,qBAAqB,EAAE,GAAG,qBAAqB,CAmB7F;AAED;;GAEG;AACH,eAAO,MAAM,QAAQ,EAAE,qBAAsE,CAAA"}
1
+ {"version":3,"file":"provider.d.ts","sourceRoot":"","sources":["../src/provider.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,KAAK,EAEV,qBAAqB,EAEtB,MAAM,gCAAgC,CAAA;AAYvC;;;;;GAKG;AACH,wBAAgB,YAAY,CAAC,OAAO,EAAE,SAAS,qBAAqB,EAAE,GAAG,qBAAqB,CAmB7F;AAED;;;;;;GAMG;AACH,eAAO,MAAM,cAAc,EAAE,SAAS,qBAAqB,EAU1D,CAAA;AAED;;;;;GAKG;AACH,eAAO,MAAM,QAAQ,EAAE,qBAAuE,CAAA"}
package/dist/provider.js CHANGED
@@ -3,9 +3,16 @@
3
3
  *
4
4
  * @module
5
5
  */
6
+ import { provider as aider } from '@molecule/api-agent-transcript-aider';
6
7
  import { provider as claudeCode } from '@molecule/api-agent-transcript-claude-code';
8
+ import { provider as cline } from '@molecule/api-agent-transcript-cline';
7
9
  import { provider as codex } from '@molecule/api-agent-transcript-codex';
10
+ import { provider as copilotChat } from '@molecule/api-agent-transcript-copilot-chat';
11
+ import { provider as cursor } from '@molecule/api-agent-transcript-cursor';
12
+ import { provider as geminiCli } from '@molecule/api-agent-transcript-gemini-cli';
13
+ import { provider as markdownChat } from '@molecule/api-agent-transcript-markdown-chat';
8
14
  import { provider as moleculeIde } from '@molecule/api-agent-transcript-molecule-ide';
15
+ import { provider as opencode } from '@molecule/api-agent-transcript-opencode';
9
16
  /**
10
17
  * Compose readers: the first whose `detect()` accepts the input reads it.
11
18
  *
@@ -30,6 +37,27 @@ export function createReader(readers) {
30
37
  };
31
38
  }
32
39
  /**
33
- * Reads any Claude Code, Codex CLI or Molecule IDE transcript.
40
+ * Every harness-specific reader, in the order they are tried. Each accepts
41
+ * only its own harness's files, so the order only matters for the generic
42
+ * Markdown chat reader, which is not in this list. Compose
43
+ * `createReader(harnessReaders)` when only a real harness's own file should
44
+ * be read.
34
45
  */
35
- export const provider = createReader([claudeCode, codex, moleculeIde]);
46
+ export const harnessReaders = [
47
+ claudeCode,
48
+ codex,
49
+ moleculeIde,
50
+ geminiCli,
51
+ cline,
52
+ opencode,
53
+ copilotChat,
54
+ cursor,
55
+ aider,
56
+ ];
57
+ /**
58
+ * Reads a transcript from any supported harness — Claude Code, Codex CLI,
59
+ * the Molecule IDE, Gemini CLI, Cline / Roo Code, OpenCode, GitHub Copilot
60
+ * Chat, Cursor and Aider — and, last, any plain Markdown / text chat with
61
+ * User / Assistant speaker markers.
62
+ */
63
+ export const provider = createReader([...harnessReaders, markdownChat]);
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@molecule/api-agent-transcript-autodetect",
3
- "version": "1.0.0",
4
- "description": "Picks the reader that recognizes a transcript and delegates to it; bundles the Claude Code, Codex and Molecule IDE readers",
3
+ "version": "1.1.0",
4
+ "description": "Picks the reader that recognizes a transcript and delegates to it; bundles readers for Claude Code, Codex CLI, the Molecule IDE, Gemini CLI, Cline / Roo Code, OpenCode, GitHub Copilot Chat, Cursor and Aider, plus a generic Markdown chat reader",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
7
7
  "types": "dist/index.d.ts",
@@ -26,20 +26,33 @@
26
26
  "transcript",
27
27
  "detect",
28
28
  "claude-code",
29
- "codex"
29
+ "codex",
30
+ "gemini-cli",
31
+ "aider",
32
+ "cursor",
33
+ "cline",
34
+ "opencode",
35
+ "copilot"
30
36
  ],
31
37
  "license": "Apache-2.0",
32
38
  "author": "Molecule Dev, Inc. (https://molecule.dev)",
33
39
  "dependencies": {
40
+ "@molecule/api-agent-transcript-aider": "1.0.0",
34
41
  "@molecule/api-agent-transcript-claude-code": "1.0.0",
42
+ "@molecule/api-agent-transcript-cline": "1.0.0",
35
43
  "@molecule/api-agent-transcript-codex": "1.0.0",
36
- "@molecule/api-agent-transcript-molecule-ide": "1.0.0"
44
+ "@molecule/api-agent-transcript-copilot-chat": "1.0.0",
45
+ "@molecule/api-agent-transcript-cursor": "1.0.0",
46
+ "@molecule/api-agent-transcript-gemini-cli": "1.0.0",
47
+ "@molecule/api-agent-transcript-markdown-chat": "1.0.0",
48
+ "@molecule/api-agent-transcript-molecule-ide": "1.0.0",
49
+ "@molecule/api-agent-transcript-opencode": "1.0.0"
37
50
  },
38
51
  "peerDependencies": {
39
52
  "@molecule/api-agent-transcript": "^1.0.0"
40
53
  },
41
54
  "devDependencies": {
42
- "@molecule/api-agent-transcript": "1.0.0",
55
+ "@molecule/api-agent-transcript": "1.0.1",
43
56
  "@types/node": "26.1.2",
44
57
  "typescript": "6.0.3",
45
58
  "vitest": "4.1.11"