@molecule/api-agent-transcript-claude-code 1.0.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/LICENSE ADDED
@@ -0,0 +1,115 @@
1
+ Apache License
2
+ Version 2.0, January 2004
3
+ http://www.apache.org/licenses/
4
+
5
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
6
+
7
+ 1. Definitions.
8
+
9
+ "License" shall mean the terms and conditions for use, reproduction,
10
+ and distribution as defined by Sections 1 through 9 of this document.
11
+
12
+ "Licensor" shall mean the copyright owner or entity authorized by
13
+ the copyright owner that is granting the License.
14
+
15
+ "Legal Entity" shall mean the union of the acting entity and all
16
+ other entities that control, are controlled by, or are under common
17
+ control with that entity. For the purposes of this definition,
18
+ "control" means (i) the power, direct or indirect, to cause the
19
+ direction or management of such entity, whether by contract or
20
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
21
+ outstanding shares, or (iii) beneficial ownership of such entity.
22
+
23
+ "You" (or "Your") shall mean an individual or Legal Entity
24
+ exercising permissions granted by this License.
25
+
26
+ "Source" form shall mean the preferred form for making modifications,
27
+ including but not limited to software source code, documentation
28
+ source, and configuration files.
29
+
30
+ "Object" form shall mean any form resulting from mechanical
31
+ transformation or translation of a Source form, including but
32
+ not limited to compiled object code, generated documentation,
33
+ and conversions to other media types.
34
+
35
+ "Work" shall mean the work of authorship, whether in Source or
36
+ Object form, made available under the License, as indicated by a
37
+ copyright notice that is included in or attached to the work.
38
+
39
+ "Derivative Works" shall mean any work, whether in Source or Object
40
+ form, that is based on (or derived from) the Work and for which the
41
+ editorial revisions, annotations, elaborations, or other modifications
42
+ represent, as a whole, an original work of authorship.
43
+
44
+ "Contribution" shall mean any work of authorship, including the
45
+ original version of the Work and any modifications or additions
46
+ to that Work, that is intentionally submitted to the Licensor for
47
+ inclusion in the Work by the copyright owner or by an individual or
48
+ Legal Entity authorized to submit on behalf of the copyright owner.
49
+
50
+ "Contributor" shall mean Licensor and any individual or Legal Entity
51
+ on behalf of whom a Contribution has been received by the Licensor and
52
+ subsequently incorporated within the Work.
53
+
54
+ 2. Grant of Copyright License. Subject to the terms and conditions of
55
+ this License, each Contributor hereby grants to You a perpetual,
56
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
57
+ copyright license to reproduce, prepare Derivative Works of,
58
+ publicly display, publicly perform, sublicense, and distribute the
59
+ Work and such Derivative Works in Source or Object form.
60
+
61
+ 3. Grant of Patent License. Subject to the terms and conditions of
62
+ this License, each Contributor hereby grants to You a perpetual,
63
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
64
+ patent license to make, have made, use, offer to sell, sell, import,
65
+ and otherwise transfer the Work.
66
+
67
+ 4. Redistribution. You may reproduce and distribute copies of the
68
+ Work or Derivative Works thereof in any medium, with or without
69
+ modifications, and in Source or Object form, provided that You
70
+ meet the following conditions:
71
+
72
+ (a) You must give any other recipients of the Work or
73
+ Derivative Works a copy of this License; and
74
+
75
+ (b) You must cause any modified files to carry prominent notices
76
+ stating that You changed the files; and
77
+
78
+ (c) You must retain, in the Source form of any Derivative Works
79
+ that You distribute, all copyright, patent, trademark, and
80
+ attribution notices from the Source form of the Work,
81
+ excluding those notices that do not pertain to any part of
82
+ the Derivative Works; and
83
+
84
+ (d) If the Work includes a "NOTICE" text file as part of its
85
+ distribution, then any Derivative Works that You distribute must
86
+ include a readable copy of the attribution notices contained
87
+ within such NOTICE file.
88
+
89
+ 5. Submission of Contributions.
90
+
91
+ 6. Trademarks. This License does not grant permission to use the trade
92
+ names, trademarks, service marks, or product names of the Licensor.
93
+
94
+ 7. Disclaimer of Warranty. Unless required by applicable law or
95
+ agreed to in writing, Licensor provides the Work on an "AS IS" BASIS,
96
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND.
97
+
98
+ 8. Limitation of Liability. In no event and under no legal theory shall
99
+ any Contributor be liable to You for damages.
100
+
101
+ 9. Accepting Warranty or Additional Liability.
102
+
103
+ Copyright 2026 Molecule Dev, Inc.
104
+
105
+ Licensed under the Apache License, Version 2.0 (the "License");
106
+ you may not use this file except in compliance with the License.
107
+ You may obtain a copy of the License at
108
+
109
+ http://www.apache.org/licenses/LICENSE-2.0
110
+
111
+ Unless required by applicable law or agreed to in writing, software
112
+ distributed under the License is distributed on an "AS IS" BASIS,
113
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
114
+ See the License for the specific language governing permissions and
115
+ limitations under the License.
package/README.md ADDED
@@ -0,0 +1,152 @@
1
+ <!--
2
+ AUTO-GENERATED — DO NOT EDIT THIS FILE.
3
+ Generated by `mlcl sync-docs` from the package's src/index.ts JSDoc + mlcl/registry.json.
4
+ Edits here are overwritten on the next commit (molecule's pre-commit hook regenerates).
5
+ To change this document, edit the module-level JSDoc in src/index.ts.
6
+ Generated: 2026-09-24T06:06:46.914Z
7
+ -->
8
+
9
+ # @molecule/api-agent-transcript-claude-code
10
+
11
+ > **Auto-generated, AI-first package reference** for the [molecule.dev](https://molecule.dev) ecosystem.
12
+ > It is written to be read by coding agents as much as by people, and is generated from this
13
+ > package's source — edit `src/index.ts` JSDoc, not this file.
14
+
15
+ Claude Code transcript reader for `@molecule/api-agent-transcript`.
16
+
17
+ Reads both forms of a Claude Code session into a normalized `AgentSession`:
18
+ the session log Claude Code keeps at
19
+ `~/.claude/projects/<project>/<session-id>.jsonl` (full fidelity: the
20
+ assistant's markdown as written, per-message model and timestamps, and the
21
+ complete text of every `Write` / `Edit` / `MultiEdit`), and the plain text
22
+ `/export` writes (the terminal rendering: user `❯`, assistant `●`, tool
23
+ calls such as `Write(path)` with the lines they wrote).
24
+
25
+ ## Quick Start
26
+
27
+ ```typescript
28
+ import { readFileSync } from 'node:fs'
29
+ import { readTranscript, setProvider } from '@molecule/api-agent-transcript'
30
+ import { provider } from '@molecule/api-agent-transcript-claude-code'
31
+
32
+ setProvider(provider)
33
+ const session = readTranscript({
34
+ text: readFileSync('session.jsonl', 'utf8'),
35
+ fileName: 'session.jsonl',
36
+ })
37
+ // session.turns: [{ role: 'user', text: '…as typed…' }, { role: 'assistant', model: 'claude-…', text, files }]
38
+ ```
39
+
40
+ ## Type
41
+
42
+ `provider`
43
+
44
+ ## Installation
45
+
46
+ ```bash
47
+ npm install @molecule/api-agent-transcript-claude-code @molecule/api-agent-transcript
48
+ ```
49
+
50
+ ## API
51
+
52
+ ### Functions
53
+
54
+ #### `looksLikeExportText(text)`
55
+
56
+ Whether a text looks like a Claude Code `/export`.
57
+
58
+ ```typescript
59
+ function looksLikeExportText(text: string): boolean
60
+ ```
61
+
62
+ - `text` — The file's text.
63
+
64
+ **Returns:** True when it carries the export's header or both of its speaker markers.
65
+
66
+ #### `looksLikeSessionJsonl(text)`
67
+
68
+ Whether a text looks like a Claude Code session log.
69
+
70
+ ```typescript
71
+ function looksLikeSessionJsonl(text: string): boolean
72
+ ```
73
+
74
+ - `text` — The file's text.
75
+
76
+ **Returns:** True when the first parseable lines are Claude Code log records.
77
+
78
+ #### `readExportText(text)`
79
+
80
+ Read a Claude Code `/export` text.
81
+
82
+ ```typescript
83
+ function readExportText(text: string): AgentSession
84
+ ```
85
+
86
+ - `text` — The export's text.
87
+
88
+ **Returns:** The normalized session.
89
+
90
+ #### `readSessionJsonl(text)`
91
+
92
+ Read a Claude Code session log.
93
+
94
+ ```typescript
95
+ function readSessionJsonl(text: string): AgentSession
96
+ ```
97
+
98
+ - `text` — The `.jsonl` text.
99
+
100
+ **Returns:** The normalized session.
101
+
102
+ ### Constants
103
+
104
+ #### `provider`
105
+
106
+ Reads Claude Code sessions: the session log (`.jsonl`) and the `/export` text.
107
+
108
+ ```typescript
109
+ const provider: AgentTranscriptReader
110
+ ```
111
+
112
+ ## Core Interface
113
+
114
+ Implements `@molecule/api-agent-transcript` interface.
115
+
116
+ ## Bond Wiring
117
+
118
+ Setup function to register this provider with the core interface:
119
+
120
+ ```typescript
121
+ import { setProvider } from '@molecule/api-agent-transcript'
122
+ import { provider } from '@molecule/api-agent-transcript-claude-code'
123
+
124
+ export function setupAgentTranscriptClaudeCode(): void {
125
+ setProvider(provider)
126
+ }
127
+ ```
128
+
129
+ ## Injection Notes
130
+
131
+ ### Requirements
132
+
133
+ Peer dependencies:
134
+
135
+ - `@molecule/api-agent-transcript` ^1.0.0
136
+
137
+ ### Runtime Dependencies
138
+
139
+ - `@molecule/api-agent-transcript`
140
+
141
+ - **Prefer the `.jsonl` when you have it.** The `/export` text is a rendering:
142
+ markdown is already applied (a `## heading` arrives as `heading`, `**bold**`
143
+ as `bold`), paragraphs are re-joined from terminal wrapping, and the model
144
+ is only the display name in the header (`Haiku 4.5`). Edits the terminal
145
+ showed collapsed ("Made 1 edit") carry no text, and a truncated preview
146
+ (`… +N lines`) is marked `complete: false`.
147
+ - Dropped from user turns: slash-command echoes (`<command-name>` …), their
148
+ output, `<system-reminder>` text, tool results, and lines Claude Code marks
149
+ `isMeta`. A subagent's own messages (`isSidechain`) are not part of the
150
+ session.
151
+ - Verified against Claude Code 2.1.281's own files; the older `>` / `⏺`
152
+ markers are accepted too.
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=browser-guard.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"browser-guard.d.ts","sourceRoot":"","sources":["../src/browser-guard.ts"],"names":[],"mappings":"AAuBA,OAAO,EAAE,CAAA"}
@@ -0,0 +1,18 @@
1
+ /**
2
+ * Browser guard — `@molecule/api-agent-transcript-claude-code` is SERVER-ONLY.
3
+ *
4
+ * Generated by scripts/gen-browser-guards.mjs (workspace root) — edit THAT, not this.
5
+ * Evaluating a server package in a browser bundle is always an import-graph mistake
6
+ * (node APIs, secrets); without this guard it surfaces as a cryptic downstream crash
7
+ * ("Buffer is not defined") far from the culprit. Throwing here names the package and
8
+ * the fix at the exact moment the client bundle evaluates it. jsdom tests and SSR are
9
+ * unaffected: the throw requires browser globals AND the absence of a node runtime.
10
+ */
11
+ const g = globalThis;
12
+ if (g.window !== undefined && g.document !== undefined && !g.process?.versions?.node) {
13
+ throw new Error('@molecule/api-agent-transcript-claude-code is SERVER-ONLY: it was bundled into browser/client code. Import it only ' +
14
+ 'from server code (a server route/function or your API), or dynamic-import it inside ' +
15
+ 'the server handler — never from components or shared client modules, and never ' +
16
+ 'polyfill Buffer/process to silence this.');
17
+ }
18
+ export {};
@@ -0,0 +1,44 @@
1
+ /**
2
+ * Claude Code's `/export` file: the terminal rendering of the conversation as
3
+ * plain text (verified against Claude Code 2.1.281).
4
+ *
5
+ * ```text
6
+ * ▐▛███▛█ Claude Code v2.1.281
7
+ * ▝▜██████▀ Haiku 4.5 · Claude Max
8
+ *
9
+ * ❯ The user's message, wrapped at the terminal width and padded with spaces
10
+ * to it.
11
+ *
12
+ * ● The assistant's reply, markdown already rendered (a heading loses its
13
+ * `#`), wrapped at the terminal width.
14
+ *
15
+ * ● Write(notes.md)
16
+ * ⎿ Wrote 3 lines to notes.md
17
+ * 1 # Notes
18
+ * 2
19
+ * 3 A line that runs past the width is broken mid-wo
20
+ * rd.
21
+ *
22
+ * ✻ Cooked for 5s · done 8:43 AM
23
+ * ```
24
+ *
25
+ * Older versions used `>` for the user and `⏺` for the assistant; both are accepted.
26
+ *
27
+ * @module
28
+ */
29
+ import type { AgentSession } from '@molecule/api-agent-transcript';
30
+ /**
31
+ * Whether a text looks like a Claude Code `/export`.
32
+ *
33
+ * @param text - The file's text.
34
+ * @returns True when it carries the export's header or both of its speaker markers.
35
+ */
36
+ export declare function looksLikeExportText(text: string): boolean;
37
+ /**
38
+ * Read a Claude Code `/export` text.
39
+ *
40
+ * @param text - The export's text.
41
+ * @returns The normalized session.
42
+ */
43
+ export declare function readExportText(text: string): AgentSession;
44
+ //# sourceMappingURL=export-text.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"export-text.d.ts","sourceRoot":"","sources":["../src/export-text.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AAEH,OAAO,KAAK,EAAkB,YAAY,EAAa,MAAM,gCAAgC,CAAA;AAU7F;;;;;GAKG;AACH,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAIzD;AA0FD;;;;;GAKG;AACH,wBAAgB,cAAc,CAAC,IAAI,EAAE,MAAM,GAAG,YAAY,CAuEzD"}
@@ -0,0 +1,226 @@
1
+ /**
2
+ * Claude Code's `/export` file: the terminal rendering of the conversation as
3
+ * plain text (verified against Claude Code 2.1.281).
4
+ *
5
+ * ```text
6
+ * ▐▛███▛█ Claude Code v2.1.281
7
+ * ▝▜██████▀ Haiku 4.5 · Claude Max
8
+ *
9
+ * ❯ The user's message, wrapped at the terminal width and padded with spaces
10
+ * to it.
11
+ *
12
+ * ● The assistant's reply, markdown already rendered (a heading loses its
13
+ * `#`), wrapped at the terminal width.
14
+ *
15
+ * ● Write(notes.md)
16
+ * ⎿ Wrote 3 lines to notes.md
17
+ * 1 # Notes
18
+ * 2
19
+ * 3 A line that runs past the width is broken mid-wo
20
+ * rd.
21
+ *
22
+ * ✻ Cooked for 5s · done 8:43 AM
23
+ * ```
24
+ *
25
+ * Older versions used `>` for the user and `⏺` for the assistant; both are accepted.
26
+ *
27
+ * @module
28
+ */
29
+ const USER = /^(?:❯|>) ?(.*)$/;
30
+ const ASSISTANT = /^(?:●|⏺) ?(.*)$/;
31
+ const STATUS = /^✻ /;
32
+ const TOOL_CALL = /^([A-Z][A-Za-z]*)\((.*)\)\s*$/;
33
+ const RESULT = /^\s*⎿\s*(.*)$/;
34
+ const NUMBERED = /^\s{2,}(\d+) ?(.*)$/;
35
+ const MORE = /…\s*\+?\d+ lines?|ctrl\+o to expand/;
36
+ /**
37
+ * Whether a text looks like a Claude Code `/export`.
38
+ *
39
+ * @param text - The file's text.
40
+ * @returns True when it carries the export's header or both of its speaker markers.
41
+ */
42
+ export function looksLikeExportText(text) {
43
+ if (/^\s*[▐▛█▜▝▘▀▄▌ ]*Claude Code v\d/m.test(text.slice(0, 600)))
44
+ return true;
45
+ const lines = text.split('\n');
46
+ return lines.some((l) => /^(?:❯|>) \S/.test(l)) && lines.some((l) => /^(?:●|⏺) \S/.test(l));
47
+ }
48
+ /**
49
+ * Re-join text the terminal wrapped: consecutive lines of one paragraph become
50
+ * one line; a blank line ends a paragraph; list items keep their own lines.
51
+ *
52
+ * @param lines - The block's lines, already de-indented.
53
+ * @returns The prose.
54
+ */
55
+ function unwrap(lines) {
56
+ const out = [];
57
+ let para = [];
58
+ let inList = false;
59
+ const flush = () => {
60
+ if (para.length)
61
+ out.push(para.join(inList ? '\n' : ' '));
62
+ para = [];
63
+ inList = false;
64
+ };
65
+ for (const raw of lines) {
66
+ const line = raw.replace(/\s+$/, '');
67
+ if (!line.trim()) {
68
+ flush();
69
+ continue;
70
+ }
71
+ const item = /^(?:[-*•]|\d+\.) /.test(line);
72
+ if (item) {
73
+ if (!inList)
74
+ flush();
75
+ inList = true;
76
+ para.push(line);
77
+ }
78
+ else if (inList && /^\s+\S/.test(line)) {
79
+ para[para.length - 1] += ` ${line.trim()}`;
80
+ }
81
+ else {
82
+ if (inList)
83
+ flush();
84
+ para.push(line.trim());
85
+ }
86
+ }
87
+ flush();
88
+ return out.join('\n\n');
89
+ }
90
+ /**
91
+ * Read the file content a `Write` / `Update` result shows.
92
+ *
93
+ * @param name - The tool name.
94
+ * @param arg - The call's argument (the path).
95
+ * @param body - The lines under the call.
96
+ * @returns The write, or null for tools that do not write.
97
+ */
98
+ function writeFrom(name, arg, body) {
99
+ const kind = name === 'Write'
100
+ ? 'create'
101
+ : name === 'Update' || name === 'Edit' || name === 'MultiEdit'
102
+ ? 'edit'
103
+ : null;
104
+ if (!kind)
105
+ return null;
106
+ const out = [];
107
+ let complete = true;
108
+ let last = 'dropped';
109
+ for (const raw of body) {
110
+ if (MORE.test(raw)) {
111
+ complete = false;
112
+ continue;
113
+ }
114
+ if (RESULT.test(raw))
115
+ continue;
116
+ const m = NUMBERED.exec(raw);
117
+ if (m) {
118
+ let text = m[2].replace(/\s+$/, '');
119
+ if (/No newline at end of file/.test(text)) {
120
+ last = 'dropped';
121
+ continue;
122
+ }
123
+ if (kind === 'edit') {
124
+ // Diff rows: "+added", "-removed", " context". Only what the edit put in counts.
125
+ if (!text.startsWith('+')) {
126
+ last = 'dropped';
127
+ continue;
128
+ }
129
+ text = text.slice(1);
130
+ }
131
+ out.push(text);
132
+ last = 'kept';
133
+ }
134
+ else if (raw.trim() && last === 'kept') {
135
+ // A content line broken at the terminal width continues mid-word.
136
+ out[out.length - 1] += raw.trim();
137
+ }
138
+ }
139
+ return { path: arg.trim(), kind, text: out.join('\n'), complete: complete && out.length > 0 };
140
+ }
141
+ /**
142
+ * Read a Claude Code `/export` text.
143
+ *
144
+ * @param text - The export's text.
145
+ * @returns The normalized session.
146
+ */
147
+ export function readExportText(text) {
148
+ const lines = text.replace(/\r\n?/g, '\n').split('\n');
149
+ const header = lines.slice(0, 8).join('\n');
150
+ const version = /Claude Code v([\d.]+)/.exec(header)?.[1];
151
+ const modelLine = lines
152
+ .slice(0, 6)
153
+ .map((l) => l.replace(/^[\s▐▛█▜▝▘▀▄▌▗▖]+/, ''))
154
+ .find((l) => / · /.test(l) && !/Claude Code v/.test(l));
155
+ const model = modelLine?.split(' · ')[0]?.trim() || undefined;
156
+ const turns = [];
157
+ let assistant = null;
158
+ // The block being read: its kind, first line and the lines under it.
159
+ let block = null;
160
+ const closeBlock = () => {
161
+ if (!block)
162
+ return;
163
+ if (block.kind === 'user') {
164
+ const body = [];
165
+ for (const l of block.body) {
166
+ // A user message is one contiguous run; the first blank line ends it (what follows, indented, is
167
+ // the harness's collapsed activity: "Made 1 edit", "Read 3 files").
168
+ if (!l.trim())
169
+ break;
170
+ body.push(l.replace(/^ {2}/, ''));
171
+ }
172
+ turns.push({
173
+ role: 'user',
174
+ text: unwrap([block.first, ...body])
175
+ .replace(/\n\n/g, ' ')
176
+ .trim(),
177
+ files: [],
178
+ });
179
+ assistant = null;
180
+ }
181
+ else {
182
+ if (!assistant) {
183
+ assistant = { role: 'assistant', text: '', model, files: [] };
184
+ turns.push(assistant);
185
+ }
186
+ if (block.kind === 'tool') {
187
+ const call = TOOL_CALL.exec(block.first);
188
+ const w = call ? writeFrom(call[1], call[2], block.body) : null;
189
+ if (w)
190
+ assistant.files.push(w);
191
+ }
192
+ else {
193
+ const prose = unwrap([block.first, ...block.body.map((l) => l.replace(/^ {2}/, ''))]);
194
+ if (prose)
195
+ assistant.text = assistant.text ? `${assistant.text}\n\n${prose}` : prose;
196
+ }
197
+ }
198
+ block = null;
199
+ };
200
+ for (const line of lines) {
201
+ const u = USER.exec(line);
202
+ const a = u ? null : ASSISTANT.exec(line);
203
+ if (u && !/^\s/.test(line)) {
204
+ closeBlock();
205
+ block = { kind: 'user', first: u[1], body: [] };
206
+ }
207
+ else if (a && !/^\s/.test(line)) {
208
+ closeBlock();
209
+ block = { kind: TOOL_CALL.test(a[1]) ? 'tool' : 'text', first: a[1], body: [] };
210
+ }
211
+ else if (STATUS.test(line)) {
212
+ closeBlock();
213
+ }
214
+ else if (block) {
215
+ block.body.push(line);
216
+ }
217
+ }
218
+ closeBlock();
219
+ return {
220
+ format: 'claude-code',
221
+ harness: 'Claude Code',
222
+ harnessVersion: version,
223
+ model,
224
+ turns: turns.filter((t) => t.role === 'user' || t.text || t.files.length),
225
+ };
226
+ }
@@ -0,0 +1,43 @@
1
+ /**
2
+ * Claude Code transcript reader for `@molecule/api-agent-transcript`.
3
+ *
4
+ * Reads both forms of a Claude Code session into a normalized `AgentSession`:
5
+ * the session log Claude Code keeps at
6
+ * `~/.claude/projects/<project>/<session-id>.jsonl` (full fidelity: the
7
+ * assistant's markdown as written, per-message model and timestamps, and the
8
+ * complete text of every `Write` / `Edit` / `MultiEdit`), and the plain text
9
+ * `/export` writes (the terminal rendering: user `❯`, assistant `●`, tool
10
+ * calls such as `Write(path)` with the lines they wrote).
11
+ *
12
+ * @example
13
+ * ```typescript
14
+ * import { readFileSync } from 'node:fs'
15
+ * import { readTranscript, setProvider } from '@molecule/api-agent-transcript'
16
+ * import { provider } from '@molecule/api-agent-transcript-claude-code'
17
+ *
18
+ * setProvider(provider)
19
+ * const session = readTranscript({ text: readFileSync('session.jsonl', 'utf8'), fileName: 'session.jsonl' })
20
+ * // session.turns: [{ role: 'user', text: '…as typed…' }, { role: 'assistant', model: 'claude-…', text, files }]
21
+ * ```
22
+ *
23
+ * @remarks
24
+ * - **Prefer the `.jsonl` when you have it.** The `/export` text is a rendering:
25
+ * markdown is already applied (a `## heading` arrives as `heading`, `**bold**`
26
+ * as `bold`), paragraphs are re-joined from terminal wrapping, and the model
27
+ * is only the display name in the header (`Haiku 4.5`). Edits the terminal
28
+ * showed collapsed ("Made 1 edit") carry no text, and a truncated preview
29
+ * (`… +N lines`) is marked `complete: false`.
30
+ * - Dropped from user turns: slash-command echoes (`<command-name>` …), their
31
+ * output, `<system-reminder>` text, tool results, and lines Claude Code marks
32
+ * `isMeta`. A subagent's own messages (`isSidechain`) are not part of the
33
+ * session.
34
+ * - Verified against Claude Code 2.1.281's own files; the older `>` / `⏺`
35
+ * markers are accepted too.
36
+ *
37
+ * @module
38
+ */
39
+ export * from './browser-guard.js';
40
+ export * from './export-text.js';
41
+ export * from './jsonl.js';
42
+ export * from './provider.js';
43
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAqCG;AAEH,cAAc,oBAAoB,CAAA;AAClC,cAAc,kBAAkB,CAAA;AAChC,cAAc,YAAY,CAAA;AAC1B,cAAc,eAAe,CAAA"}
package/dist/index.js ADDED
@@ -0,0 +1,42 @@
1
+ /**
2
+ * Claude Code transcript reader for `@molecule/api-agent-transcript`.
3
+ *
4
+ * Reads both forms of a Claude Code session into a normalized `AgentSession`:
5
+ * the session log Claude Code keeps at
6
+ * `~/.claude/projects/<project>/<session-id>.jsonl` (full fidelity: the
7
+ * assistant's markdown as written, per-message model and timestamps, and the
8
+ * complete text of every `Write` / `Edit` / `MultiEdit`), and the plain text
9
+ * `/export` writes (the terminal rendering: user `❯`, assistant `●`, tool
10
+ * calls such as `Write(path)` with the lines they wrote).
11
+ *
12
+ * @example
13
+ * ```typescript
14
+ * import { readFileSync } from 'node:fs'
15
+ * import { readTranscript, setProvider } from '@molecule/api-agent-transcript'
16
+ * import { provider } from '@molecule/api-agent-transcript-claude-code'
17
+ *
18
+ * setProvider(provider)
19
+ * const session = readTranscript({ text: readFileSync('session.jsonl', 'utf8'), fileName: 'session.jsonl' })
20
+ * // session.turns: [{ role: 'user', text: '…as typed…' }, { role: 'assistant', model: 'claude-…', text, files }]
21
+ * ```
22
+ *
23
+ * @remarks
24
+ * - **Prefer the `.jsonl` when you have it.** The `/export` text is a rendering:
25
+ * markdown is already applied (a `## heading` arrives as `heading`, `**bold**`
26
+ * as `bold`), paragraphs are re-joined from terminal wrapping, and the model
27
+ * is only the display name in the header (`Haiku 4.5`). Edits the terminal
28
+ * showed collapsed ("Made 1 edit") carry no text, and a truncated preview
29
+ * (`… +N lines`) is marked `complete: false`.
30
+ * - Dropped from user turns: slash-command echoes (`<command-name>` …), their
31
+ * output, `<system-reminder>` text, tool results, and lines Claude Code marks
32
+ * `isMeta`. A subagent's own messages (`isSidechain`) are not part of the
33
+ * session.
34
+ * - Verified against Claude Code 2.1.281's own files; the older `>` / `⏺`
35
+ * markers are accepted too.
36
+ *
37
+ * @module
38
+ */
39
+ export * from './browser-guard.js';
40
+ export * from './export-text.js';
41
+ export * from './jsonl.js';
42
+ export * from './provider.js';
@@ -0,0 +1,23 @@
1
+ /**
2
+ * Claude Code's session log (`~/.claude/projects/<project>/<session>.jsonl`):
3
+ * one JSON object per line; `type: 'user' | 'assistant'` lines carry an
4
+ * Anthropic Messages API `message`.
5
+ *
6
+ * @module
7
+ */
8
+ import type { AgentSession } from '@molecule/api-agent-transcript';
9
+ /**
10
+ * Whether a text looks like a Claude Code session log.
11
+ *
12
+ * @param text - The file's text.
13
+ * @returns True when the first parseable lines are Claude Code log records.
14
+ */
15
+ export declare function looksLikeSessionJsonl(text: string): boolean;
16
+ /**
17
+ * Read a Claude Code session log.
18
+ *
19
+ * @param text - The `.jsonl` text.
20
+ * @returns The normalized session.
21
+ */
22
+ export declare function readSessionJsonl(text: string): AgentSession;
23
+ //# sourceMappingURL=jsonl.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"jsonl.d.ts","sourceRoot":"","sources":["../src/jsonl.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,KAAK,EAAkB,YAAY,EAAa,MAAM,gCAAgC,CAAA;AAwB7F;;;;;GAKG;AACH,wBAAgB,qBAAqB,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAkB3D;AA+BD;;;;;GAKG;AACH,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,MAAM,GAAG,YAAY,CAsE3D"}
package/dist/jsonl.js ADDED
@@ -0,0 +1,145 @@
1
+ /**
2
+ * Claude Code's session log (`~/.claude/projects/<project>/<session>.jsonl`):
3
+ * one JSON object per line; `type: 'user' | 'assistant'` lines carry an
4
+ * Anthropic Messages API `message`.
5
+ *
6
+ * @module
7
+ */
8
+ /** User text Claude Code writes on the user's behalf: slash-command echoes, their output, caveats. */
9
+ const INJECTED_USER_TEXT = /^\s*<(command-name|command-message|command-args|local-command-stdout|local-command-stderr|local-command-caveat|system-reminder|bash-input|bash-stdout|bash-stderr)>/;
10
+ /**
11
+ * Whether a text looks like a Claude Code session log.
12
+ *
13
+ * @param text - The file's text.
14
+ * @returns True when the first parseable lines are Claude Code log records.
15
+ */
16
+ export function looksLikeSessionJsonl(text) {
17
+ let seen = 0;
18
+ for (const raw of text.split('\n')) {
19
+ const line = raw.trim();
20
+ if (!line)
21
+ continue;
22
+ if (!line.startsWith('{'))
23
+ return false;
24
+ let o;
25
+ try {
26
+ o = JSON.parse(line);
27
+ }
28
+ catch (_error) {
29
+ // A line that is not JSON means this is not a .jsonl log.
30
+ return false;
31
+ }
32
+ if ((o.type === 'user' || o.type === 'assistant') && o.message && (o.sessionId || o.uuid))
33
+ return true;
34
+ if (++seen > 20)
35
+ return false;
36
+ }
37
+ return false;
38
+ }
39
+ /**
40
+ * A file write or edit from a tool call, or null when the tool does not write.
41
+ *
42
+ * @param block - The `tool_use` block.
43
+ * @returns The writes it made.
44
+ */
45
+ function writesOf(block) {
46
+ const input = block.input ?? {};
47
+ const path = typeof input.file_path === 'string'
48
+ ? input.file_path
49
+ : typeof input.path === 'string'
50
+ ? input.path
51
+ : '';
52
+ if (!path)
53
+ return [];
54
+ if (block.name === 'Write' && typeof input.content === 'string') {
55
+ return [{ path, kind: 'create', text: input.content, complete: true }];
56
+ }
57
+ if (block.name === 'Edit' && typeof input.new_string === 'string') {
58
+ return [{ path, kind: 'edit', text: input.new_string, complete: true }];
59
+ }
60
+ if (block.name === 'MultiEdit' && Array.isArray(input.edits)) {
61
+ return input.edits
62
+ .filter((e) => typeof e.new_string === 'string')
63
+ .map((e) => ({ path, kind: 'edit', text: e.new_string, complete: true }));
64
+ }
65
+ return [];
66
+ }
67
+ /**
68
+ * Read a Claude Code session log.
69
+ *
70
+ * @param text - The `.jsonl` text.
71
+ * @returns The normalized session.
72
+ */
73
+ export function readSessionJsonl(text) {
74
+ const turns = [];
75
+ let version;
76
+ let startedAt;
77
+ let current = null;
78
+ const models = new Set();
79
+ for (const raw of text.split('\n')) {
80
+ const line = raw.trim();
81
+ if (!line.startsWith('{'))
82
+ continue;
83
+ let o;
84
+ try {
85
+ o = JSON.parse(line);
86
+ }
87
+ catch (_error) {
88
+ // A torn final line (a log still being written) carries nothing to read.
89
+ continue;
90
+ }
91
+ if (o.type !== 'user' && o.type !== 'assistant')
92
+ continue;
93
+ // A subagent's own conversation, and lines Claude Code marks as meta, are not the session.
94
+ if (o.isSidechain || o.isMeta)
95
+ continue;
96
+ version ??= o.version;
97
+ startedAt ??= o.timestamp;
98
+ const content = o.message?.content;
99
+ const blocks = typeof content === 'string' ? [{ type: 'text', text: content }] : (content ?? []);
100
+ if (o.type === 'user') {
101
+ // Tool results arrive as user lines; they belong to the assistant turn in progress.
102
+ const typed = blocks
103
+ .filter((b) => b.type === 'text' && typeof b.text === 'string' && !INJECTED_USER_TEXT.test(b.text))
104
+ .map((b) => b.text);
105
+ if (typed.length === 0)
106
+ continue;
107
+ current = null;
108
+ turns.push({
109
+ role: 'user',
110
+ text: typed.join('\n\n').trim(),
111
+ timestamp: o.timestamp,
112
+ files: [],
113
+ });
114
+ continue;
115
+ }
116
+ if (!current) {
117
+ current = {
118
+ role: 'assistant',
119
+ text: '',
120
+ timestamp: o.timestamp,
121
+ files: [],
122
+ };
123
+ turns.push(current);
124
+ }
125
+ // `<synthetic>` marks a message Claude Code made itself (an interruption notice), not a model.
126
+ if (o.message?.model && !o.message.model.startsWith('<')) {
127
+ current.model ??= o.message.model;
128
+ models.add(o.message.model);
129
+ }
130
+ for (const b of blocks) {
131
+ if (b.type === 'text' && b.text?.trim())
132
+ current.text = current.text ? `${current.text}\n\n${b.text.trim()}` : b.text.trim();
133
+ else if (b.type === 'tool_use')
134
+ current.files.push(...writesOf(b));
135
+ }
136
+ }
137
+ return {
138
+ format: 'claude-code',
139
+ harness: 'Claude Code',
140
+ harnessVersion: version,
141
+ model: models.size === 1 ? [...models][0] : undefined,
142
+ startedAt,
143
+ turns: turns.filter((t) => t.role === 'user' || t.text || t.files.length),
144
+ };
145
+ }
@@ -0,0 +1,11 @@
1
+ /**
2
+ * The Claude Code transcript reader.
3
+ *
4
+ * @module
5
+ */
6
+ import type { AgentTranscriptReader } from '@molecule/api-agent-transcript';
7
+ /**
8
+ * Reads Claude Code sessions: the session log (`.jsonl`) and the `/export` text.
9
+ */
10
+ export declare const provider: AgentTranscriptReader;
11
+ //# sourceMappingURL=provider.d.ts.map
@@ -0,0 +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;;GAEG;AACH,eAAO,MAAM,QAAQ,EAAE,qBAatB,CAAA"}
@@ -0,0 +1,24 @@
1
+ /**
2
+ * The Claude Code transcript reader.
3
+ *
4
+ * @module
5
+ */
6
+ import { looksLikeExportText, readExportText } from './export-text.js';
7
+ import { looksLikeSessionJsonl, readSessionJsonl } from './jsonl.js';
8
+ /**
9
+ * Reads Claude Code sessions: the session log (`.jsonl`) and the `/export` text.
10
+ */
11
+ export const provider = {
12
+ format: 'claude-code',
13
+ label: 'Claude Code',
14
+ detect(input) {
15
+ return looksLikeSessionJsonl(input.text) || looksLikeExportText(input.text);
16
+ },
17
+ read(input) {
18
+ if (looksLikeSessionJsonl(input.text))
19
+ return readSessionJsonl(input.text);
20
+ if (looksLikeExportText(input.text))
21
+ return readExportText(input.text);
22
+ throw new Error(`Not a Claude Code transcript${input.fileName ? ` (${input.fileName})` : ''}: expected a session .jsonl or a /export text.`);
23
+ },
24
+ };
package/package.json ADDED
@@ -0,0 +1,52 @@
1
+ {
2
+ "name": "@molecule/api-agent-transcript-claude-code",
3
+ "version": "1.0.0",
4
+ "description": "Reads Claude Code sessions — the /export text and the session .jsonl — into a normalized agent session",
5
+ "type": "module",
6
+ "main": "dist/index.js",
7
+ "types": "dist/index.d.ts",
8
+ "scripts": {
9
+ "build": "tsc",
10
+ "test": "vitest run",
11
+ "test:watch": "vitest"
12
+ },
13
+ "exports": {
14
+ ".": {
15
+ "types": "./dist/index.d.ts",
16
+ "import": "./dist/index.js"
17
+ }
18
+ },
19
+ "files": [
20
+ "dist",
21
+ "README.md"
22
+ ],
23
+ "keywords": [
24
+ "molecule",
25
+ "agent",
26
+ "transcript",
27
+ "claude-code",
28
+ "export",
29
+ "jsonl"
30
+ ],
31
+ "license": "Apache-2.0",
32
+ "author": "Molecule Dev, Inc. (https://molecule.dev)",
33
+ "peerDependencies": {
34
+ "@molecule/api-agent-transcript": "^1.0.0"
35
+ },
36
+ "devDependencies": {
37
+ "@molecule/api-agent-transcript": "1.0.0",
38
+ "@types/node": "26.1.2",
39
+ "typescript": "6.0.3",
40
+ "vitest": "4.1.11"
41
+ },
42
+ "repository": {
43
+ "type": "git",
44
+ "url": "https://github.com/molecule-dev/molecule.git",
45
+ "directory": "packages/api/bonds/agent-transcript/claude-code"
46
+ },
47
+ "homepage": "https://www.molecule.dev/packages/api-agent-transcript-claude-code",
48
+ "bugs": "https://github.com/molecule-dev/molecule/issues",
49
+ "publishConfig": {
50
+ "access": "public"
51
+ }
52
+ }