@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 +151 -22
- package/dist/index.d.ts +125 -18
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +125 -18
- package/dist/provider.d.ts +12 -1
- package/dist/provider.d.ts.map +1 -1
- package/dist/provider.js +30 -2
- package/package.json +18 -5
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-
|
|
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:
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
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
|
-
|
|
27
|
-
import {
|
|
28
|
-
import {
|
|
29
|
-
|
|
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
|
-
|
|
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
|
-
|
|
34
|
-
|
|
35
|
-
|
|
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
|
|
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
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
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:
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
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
|
-
*
|
|
13
|
-
* import {
|
|
14
|
-
* import {
|
|
15
|
-
*
|
|
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
|
-
*
|
|
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
|
-
*
|
|
20
|
-
*
|
|
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
|
-
* -
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
*
|
|
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
|
*/
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA
|
|
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:
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
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
|
-
*
|
|
13
|
-
* import {
|
|
14
|
-
* import {
|
|
15
|
-
*
|
|
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
|
-
*
|
|
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
|
-
*
|
|
20
|
-
*
|
|
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
|
-
* -
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
*
|
|
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
|
*/
|
package/dist/provider.d.ts
CHANGED
|
@@ -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
|
-
*
|
|
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
|
package/dist/provider.d.ts.map
CHANGED
|
@@ -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;
|
|
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
|
-
*
|
|
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
|
|
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.
|
|
4
|
-
"description": "Picks the reader that recognizes a transcript and delegates to it; bundles
|
|
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-
|
|
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.
|
|
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"
|