@arcreflex/agent-transcripts 0.1.1

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.
@@ -0,0 +1,6 @@
1
+ # Test fixtures should match exact tool output
2
+ test/fixtures/
3
+
4
+ # Symlinks (prettier doesn't handle them)
5
+ CLAUDE.md
6
+ AGENTS.md
package/README.md ADDED
@@ -0,0 +1,58 @@
1
+ # agent-transcripts
2
+
3
+ CLI tool that transforms AI coding agent session files into readable transcripts.
4
+
5
+ ## Stack
6
+
7
+ - Runtime: Bun
8
+ - CLI: cmd-ts
9
+ - TypeScript with strict mode
10
+
11
+ ## Structure
12
+
13
+ ```
14
+ src/
15
+ cli.ts # CLI entry point, subcommand routing
16
+ parse.ts # Source → intermediate JSON
17
+ render.ts # Intermediate JSON → markdown
18
+ types.ts # Core types (Transcript, Message, Adapter)
19
+ adapters/ # Source format adapters (currently: claude-code)
20
+ utils/ # Helpers (summary extraction)
21
+ test/
22
+ fixtures/ # Snapshot test inputs/outputs
23
+ snapshots.test.ts
24
+ ```
25
+
26
+ ## Commands
27
+
28
+ ```bash
29
+ bun run check # typecheck + prettier
30
+ bun run test # snapshot tests
31
+ bun run format # auto-format
32
+ ```
33
+
34
+ ## Architecture
35
+
36
+ Two-stage pipeline: Parse (source → JSON) → Render (JSON → markdown).
37
+
38
+ - Adapters handle source formats (see `src/adapters/index.ts` for registry)
39
+ - Auto-detection: paths containing `.claude/` → claude-code adapter
40
+ - Branching conversations preserved via `parentMessageRef` on messages
41
+
42
+ ## Key Types
43
+
44
+ - `Transcript`: source info, warnings, messages array
45
+ - `Message`: union of UserMessage | AssistantMessage | SystemMessage | ToolCallGroup | ErrorMessage
46
+ - `Adapter`: `{ name: string, parse(content, sourcePath): Transcript[] }`
47
+
48
+ ## Adding an Adapter
49
+
50
+ 1. Create `src/adapters/<name>.ts` implementing `Adapter`
51
+ 2. Register in `src/adapters/index.ts` (adapters map + detection rules)
52
+ 3. Add test fixtures in `test/fixtures/<name>/`
53
+
54
+ ## Tests
55
+
56
+ Snapshot-based: `*.input.jsonl` → parse → render → compare against `*.output.md`
57
+
58
+ To update snapshots: manually edit the expected `.output.md` files.
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env bun
2
+ import "../src/cli.ts";
package/bun.lock ADDED
@@ -0,0 +1,54 @@
1
+ {
2
+ "lockfileVersion": 1,
3
+ "configVersion": 1,
4
+ "workspaces": {
5
+ "": {
6
+ "name": "agent-transcripts",
7
+ "dependencies": {
8
+ "cmd-ts": "^0.13.0",
9
+ },
10
+ "devDependencies": {
11
+ "@types/bun": "^1.1.14",
12
+ "prettier": "^3.8.0",
13
+ "typescript": "^5.7.2",
14
+ },
15
+ },
16
+ },
17
+ "packages": {
18
+ "@types/bun": ["@types/bun@1.3.6", "", { "dependencies": { "bun-types": "1.3.6" } }, "sha512-uWCv6FO/8LcpREhenN1d1b6fcspAB+cefwD7uti8C8VffIv0Um08TKMn98FynpTiU38+y2dUO55T11NgDt8VAA=="],
19
+
20
+ "@types/node": ["@types/node@25.0.9", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-/rpCXHlCWeqClNBwUhDcusJxXYDjZTyE8v5oTO7WbL8eij2nKhUeU89/6xgjU7N4/Vh3He0BtyhJdQbDyhiXAw=="],
21
+
22
+ "ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="],
23
+
24
+ "ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="],
25
+
26
+ "bun-types": ["bun-types@1.3.6", "", { "dependencies": { "@types/node": "*" } }, "sha512-OlFwHcnNV99r//9v5IIOgQ9Uk37gZqrNMCcqEaExdkVq3Avwqok1bJFmvGMCkCE0FqzdY8VMOZpfpR3lwI+CsQ=="],
27
+
28
+ "chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="],
29
+
30
+ "cmd-ts": ["cmd-ts@0.13.0", "", { "dependencies": { "chalk": "^4.0.0", "debug": "^4.3.4", "didyoumean": "^1.2.2", "strip-ansi": "^6.0.0" } }, "sha512-nsnxf6wNIM/JAS7T/x/1JmbEsjH0a8tezXqqpaL0O6+eV0/aDEnRxwjxpu0VzDdRcaC1ixGSbRlUuf/IU59I4g=="],
31
+
32
+ "color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="],
33
+
34
+ "color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="],
35
+
36
+ "debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="],
37
+
38
+ "didyoumean": ["didyoumean@1.2.2", "", {}, "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw=="],
39
+
40
+ "has-flag": ["has-flag@4.0.0", "", {}, "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="],
41
+
42
+ "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],
43
+
44
+ "prettier": ["prettier@3.8.0", "", { "bin": { "prettier": "bin/prettier.cjs" } }, "sha512-yEPsovQfpxYfgWNhCfECjG5AQaO+K3dp6XERmOepyPDVqcJm+bjyCVO3pmU+nAPe0N5dDvekfGezt/EIiRe1TA=="],
45
+
46
+ "strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="],
47
+
48
+ "supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="],
49
+
50
+ "typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="],
51
+
52
+ "undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="],
53
+ }
54
+ }
package/package.json ADDED
@@ -0,0 +1,31 @@
1
+ {
2
+ "name": "@arcreflex/agent-transcripts",
3
+ "version": "0.1.1",
4
+ "description": "Transform AI coding agent session files into readable transcripts",
5
+ "type": "module",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "git+https://github.com/arcreflex/agent-transcripts.git"
9
+ },
10
+ "publishConfig": {
11
+ "access": "public"
12
+ },
13
+ "bin": {
14
+ "agent-transcripts": "./bin/agent-transcripts"
15
+ },
16
+ "scripts": {
17
+ "check": "bun run typecheck && bun run prettier:check",
18
+ "test": "bun test",
19
+ "typecheck": "tsc --noEmit",
20
+ "prettier:check": "prettier --check .",
21
+ "format": "prettier --write ."
22
+ },
23
+ "dependencies": {
24
+ "cmd-ts": "^0.13.0"
25
+ },
26
+ "devDependencies": {
27
+ "@types/bun": "^1.1.14",
28
+ "prettier": "^3.8.0",
29
+ "typescript": "^5.7.2"
30
+ }
31
+ }
package/spec.md ADDED
@@ -0,0 +1,239 @@
1
+ # agent-transcripts
2
+
3
+ A CLI tool that transforms AI coding agent session files into human-readable and LLM-readable transcripts.
4
+
5
+ ## Overview
6
+
7
+ The tool converts session logs from various AI coding agents (starting with Claude Code, with planned support for Codex and others) into a standardized intermediate JSON format, then renders that to markdown. The primary audience is ~70% LLM consumption, ~30% human reading.
8
+
9
+ ## Architecture
10
+
11
+ ### Two-Stage Pipeline
12
+
13
+ 1. **Parse stage**: Source format (e.g., Claude Code JSONL) → Intermediate JSON
14
+ 2. **Render stage**: Intermediate JSON → Markdown
15
+
16
+ Both stages are exposed as separate commands, plus a default command that pipelines them.
17
+
18
+ ### Adapter Pattern
19
+
20
+ Different source formats are handled by adapters. Adapter selection:
21
+
22
+ - **Auto-detection by file path**: e.g., `.claude/` in path → Claude Code adapter
23
+ - **Explicit flag required**: for stdin input or ambiguous paths
24
+ - **Override available**: explicit adapter flag always takes precedence
25
+
26
+ ## CLI Interface
27
+
28
+ Built with `cmd-ts`. Supports both file arguments and stdin piping.
29
+
30
+ ### Commands
31
+
32
+ ```
33
+ agent-transcripts [file] # Full pipeline: source → JSON → markdown
34
+ agent-transcripts parse [file] # Source → intermediate JSON only
35
+ agent-transcripts render [file] # Intermediate JSON → markdown only
36
+ ```
37
+
38
+ ### Options
39
+
40
+ - `-o, --output <path>` - Output file path (default: current working directory)
41
+ - `--adapter <name>` - Explicitly specify source adapter (required for stdin if not auto-detectable)
42
+ - `--head <id>` - Render branch ending at this message ID (default: latest leaf)
43
+
44
+ ### Examples
45
+
46
+ ```bash
47
+ # File argument, auto-detect adapter
48
+ agent-transcripts ~/.claude/projects/foo/sessions/abc123.jsonl
49
+
50
+ # Piped input with explicit adapter
51
+ cat session.jsonl | agent-transcripts --adapter claude-code
52
+
53
+ # Just parse to intermediate format
54
+ agent-transcripts parse session.jsonl -o transcript.json
55
+
56
+ # Just render existing intermediate format
57
+ agent-transcripts render transcript.json -o transcript.md
58
+ ```
59
+
60
+ ## Intermediate JSON Format
61
+
62
+ TypeScript-typed JSON format. Each item includes provenance (source file + UUID) for traceability.
63
+
64
+ ### Structure
65
+
66
+ Flat sequence of messages, each tagged with role/type. Parallel tool calls are grouped together.
67
+
68
+ ```typescript
69
+ interface Transcript {
70
+ source: {
71
+ file: string; // Original source file path
72
+ adapter: string; // Adapter used (e.g., "claude-code")
73
+ };
74
+ metadata: {
75
+ warnings: Warning[]; // Parse warnings (malformed lines, etc.)
76
+ };
77
+ messages: Message[];
78
+ }
79
+
80
+ interface Warning {
81
+ type: string;
82
+ detail: string;
83
+ sourceRef?: string; // UUID or line reference if available
84
+ }
85
+
86
+ type Message =
87
+ | UserMessage
88
+ | AssistantMessage
89
+ | SystemMessage
90
+ | ToolCallGroup
91
+ | ErrorMessage;
92
+
93
+ interface BaseMessage {
94
+ sourceRef: string; // UUID from source for provenance
95
+ timestamp: string; // ISO 8601
96
+ parentMessageRef?: string; // Parent message UUID (for tree reconstruction)
97
+ }
98
+
99
+ interface UserMessage extends BaseMessage {
100
+ type: "user";
101
+ content: string;
102
+ }
103
+
104
+ interface AssistantMessage extends BaseMessage {
105
+ type: "assistant";
106
+ content: string;
107
+ thinking?: string; // Full thinking trace if present
108
+ }
109
+
110
+ interface SystemMessage extends BaseMessage {
111
+ type: "system";
112
+ content: string;
113
+ }
114
+
115
+ interface ToolCallGroup extends BaseMessage {
116
+ type: "tool_calls";
117
+ calls: ToolCall[]; // Grouped if parallel, single-element if sequential
118
+ }
119
+
120
+ interface ToolCall {
121
+ name: string;
122
+ summary: string; // One-line summary extracted from result
123
+ error?: string; // If tool call failed, verbatim error
124
+ }
125
+
126
+ interface ErrorMessage extends BaseMessage {
127
+ type: "error";
128
+ content: string; // Verbatim error message
129
+ }
130
+ ```
131
+
132
+ ### Design Decisions
133
+
134
+ - **Timestamps**: Always preserved
135
+ - **Thinking traces**: Included in full
136
+ - **Tool call details**: Name + one-line summary only (extract from result, don't generate)
137
+ - **Tool call results**: Discarded (recoverable via provenance if needed)
138
+ - **Cache markers**: Stripped (API optimization detail, not relevant to transcript)
139
+ - **System messages/metadata**: Included inline with type markers
140
+ - **Conversation boundaries**: Split into separate output files with indexed suffixes (e.g., `transcript_1.json`, `transcript_2.json`)
141
+
142
+ ### Branching Conversations
143
+
144
+ When a conversation has multiple branches (same parent with multiple children), the structure is preserved via `parentMessageRef` fields. During rendering:
145
+
146
+ - **Default**: Render the "primary" branch (path to the latest leaf by timestamp), with references to other branches at branch points
147
+ - **`--head <id>`**: Render from root to the specified message ID (for viewing non-primary branches)
148
+
149
+ Branch references appear as blockquotes showing the message ID and first line of each alternate branch.
150
+
151
+ ## Markdown Output
152
+
153
+ Optimized for both GitHub rendering and LLM consumption.
154
+
155
+ ### Formatting
156
+
157
+ - **Thinking blocks**: Collapsible `<details>` sections
158
+ - **Tool calls**: Inline, showing name and summary
159
+ - **Parallel tool calls**: Visually grouped
160
+ - **System messages**: Clearly marked inline
161
+ - **Errors**: Preserved verbatim with clear markers
162
+
163
+ ### Example Output
164
+
165
+ ```markdown
166
+ # Transcript
167
+
168
+ **Source**: `~/.claude/projects/foo/sessions/abc123.jsonl`
169
+ **Adapter**: claude-code
170
+
171
+ ---
172
+
173
+ ## User
174
+
175
+ Can you help me fix the type error in auth.ts?
176
+
177
+ ## Assistant
178
+
179
+ <details>
180
+ <summary>Thinking...</summary>
181
+
182
+ Let me look at the auth.ts file to understand the type error...
183
+
184
+ </details>
185
+
186
+ I'll take a look at that file.
187
+
188
+ **Tools**: Read `/src/auth.ts`
189
+
190
+ The issue is on line 42 where...
191
+
192
+ ---
193
+
194
+ ## User
195
+
196
+ ...
197
+ ```
198
+
199
+ ## Error Handling
200
+
201
+ - **Malformed input**: Best effort processing
202
+ - Warn about skipped/malformed lines
203
+ - Record warnings in output metadata
204
+ - Continue processing valid content
205
+ - **Truncated files**: Process what's available, warn in output
206
+ - **Missing fields**: Use sensible defaults, warn if significant
207
+
208
+ ## Adapters
209
+
210
+ ### Claude Code (initial)
211
+
212
+ Parses `.jsonl` session files from `~/.claude/projects/*/sessions/`.
213
+
214
+ Detection: File path contains `.claude/`
215
+
216
+ Key mappings:
217
+
218
+ - Extract message UUIDs for provenance
219
+ - Parse `content` arrays for text, thinking, tool_use, tool_result blocks
220
+ - Extract one-line summaries from tool results (look for existing summary fields or first meaningful line)
221
+ - Handle conversation boundaries (e.g., `/clear` commands)
222
+
223
+ ### Future: Codex
224
+
225
+ TBD - investigate format when adding support.
226
+
227
+ ## Non-Goals
228
+
229
+ - **Filtering**: No time-range or content filtering (full transcript only)
230
+ - **Annotations**: Out of scope (separate tooling concern)
231
+ - **Schema versioning**: Keep simple; handle breaking changes ad-hoc
232
+ - **Full tool results**: Not preserved (use provenance to recover from source if needed)
233
+
234
+ ## Implementation Notes
235
+
236
+ - Runtime: Bun (script, not compiled)
237
+ - CLI framework: cmd-ts
238
+ - Files small enough (without tool results) to handle in-memory; no streaming needed
239
+ - Provenance uses source UUIDs (stable across file edits)
@@ -0,0 +1,310 @@
1
+ /**
2
+ * Claude Code JSONL adapter.
3
+ *
4
+ * Parses session files from ~/.claude/projects/{project}/sessions/{session}.jsonl
5
+ */
6
+
7
+ import type {
8
+ Adapter,
9
+ Transcript,
10
+ Message,
11
+ Warning,
12
+ ToolCall,
13
+ } from "../types.ts";
14
+ import { extractToolSummary } from "../utils/summary.ts";
15
+
16
+ // Claude Code JSONL record types
17
+ interface ClaudeRecord {
18
+ type: string;
19
+ uuid?: string;
20
+ parentUuid?: string | null;
21
+ timestamp?: string;
22
+ message?: {
23
+ role: string;
24
+ content: string | ContentBlock[];
25
+ };
26
+ content?: string;
27
+ subtype?: string;
28
+ }
29
+
30
+ interface ContentBlock {
31
+ type: string;
32
+ text?: string;
33
+ thinking?: string;
34
+ id?: string;
35
+ name?: string;
36
+ input?: Record<string, unknown>;
37
+ tool_use_id?: string;
38
+ content?: string;
39
+ }
40
+
41
+ /**
42
+ * Parse JSONL content with best-effort error recovery.
43
+ */
44
+ function parseJsonl(content: string): {
45
+ records: ClaudeRecord[];
46
+ warnings: Warning[];
47
+ } {
48
+ const records: ClaudeRecord[] = [];
49
+ const warnings: Warning[] = [];
50
+ const lines = content.split("\n");
51
+
52
+ for (let i = 0; i < lines.length; i++) {
53
+ const line = lines[i].trim();
54
+ if (!line) continue;
55
+
56
+ try {
57
+ const record = JSON.parse(line) as ClaudeRecord;
58
+ records.push(record);
59
+ } catch (e) {
60
+ warnings.push({
61
+ type: "parse_error",
62
+ detail: `Line ${i + 1}: ${e instanceof Error ? e.message : "Invalid JSON"}`,
63
+ });
64
+ }
65
+ }
66
+
67
+ return { records, warnings };
68
+ }
69
+
70
+ /**
71
+ * Build message graph and find conversation boundaries.
72
+ * Returns array of conversation groups (each is array of records in order).
73
+ */
74
+ function splitConversations(records: ClaudeRecord[]): ClaudeRecord[][] {
75
+ // Filter to only message records (user, assistant, system with uuid)
76
+ const messageRecords = records.filter(
77
+ (r) =>
78
+ r.uuid &&
79
+ (r.type === "user" || r.type === "assistant" || r.type === "system"),
80
+ );
81
+
82
+ if (messageRecords.length === 0) return [];
83
+
84
+ // Build parent → children map
85
+ const byUuid = new Map<string, ClaudeRecord>();
86
+ const children = new Map<string, string[]>();
87
+
88
+ for (const rec of messageRecords) {
89
+ if (rec.uuid) {
90
+ byUuid.set(rec.uuid, rec);
91
+ const parent = rec.parentUuid;
92
+ if (parent) {
93
+ const existing = children.get(parent) || [];
94
+ existing.push(rec.uuid);
95
+ children.set(parent, existing);
96
+ }
97
+ }
98
+ }
99
+
100
+ // Find roots (no parent or parent not in our set)
101
+ const roots: string[] = [];
102
+ for (const rec of messageRecords) {
103
+ if (!rec.parentUuid || !byUuid.has(rec.parentUuid)) {
104
+ if (rec.uuid) roots.push(rec.uuid);
105
+ }
106
+ }
107
+
108
+ // BFS from each root to collect conversation
109
+ const visited = new Set<string>();
110
+ const conversations: ClaudeRecord[][] = [];
111
+
112
+ for (const root of roots) {
113
+ if (visited.has(root)) continue;
114
+
115
+ const conversation: ClaudeRecord[] = [];
116
+ const queue = [root];
117
+
118
+ while (queue.length > 0) {
119
+ const uuid = queue.shift();
120
+ if (!uuid || visited.has(uuid)) continue;
121
+ visited.add(uuid);
122
+
123
+ const rec = byUuid.get(uuid);
124
+ if (rec) conversation.push(rec);
125
+
126
+ // Add children to queue
127
+ const childUuids = children.get(uuid) || [];
128
+ queue.push(...childUuids);
129
+ }
130
+
131
+ // Note: we don't sort here - renderer handles ordering via tree traversal
132
+ if (conversation.length > 0) {
133
+ conversations.push(conversation);
134
+ }
135
+ }
136
+
137
+ // Sort conversations by their first message timestamp
138
+ conversations.sort((a, b) => {
139
+ const ta = a[0]?.timestamp ? new Date(a[0].timestamp).getTime() : 0;
140
+ const tb = b[0]?.timestamp ? new Date(b[0].timestamp).getTime() : 0;
141
+ return ta - tb;
142
+ });
143
+
144
+ return conversations;
145
+ }
146
+
147
+ /**
148
+ * Extract text content from message content blocks.
149
+ */
150
+ function extractText(content: string | ContentBlock[]): string {
151
+ if (typeof content === "string") return content;
152
+
153
+ return content
154
+ .flatMap((b) => (b.type === "text" && b.text ? [b.text] : []))
155
+ .join("\n");
156
+ }
157
+
158
+ /**
159
+ * Extract thinking from content blocks.
160
+ */
161
+ function extractThinking(content: string | ContentBlock[]): string | undefined {
162
+ if (typeof content === "string") return undefined;
163
+
164
+ const thinking = content
165
+ .flatMap((b) => (b.type === "thinking" && b.thinking ? [b.thinking] : []))
166
+ .join("\n\n");
167
+
168
+ return thinking || undefined;
169
+ }
170
+
171
+ /**
172
+ * Extract tool calls from content blocks.
173
+ */
174
+ function extractToolCalls(content: string | ContentBlock[]): ToolCall[] {
175
+ if (typeof content === "string") return [];
176
+
177
+ return content.flatMap((b) => {
178
+ if (b.type === "tool_use" && b.name) {
179
+ return [
180
+ {
181
+ name: b.name,
182
+ summary: extractToolSummary(b.name, b.input || {}),
183
+ },
184
+ ];
185
+ }
186
+ return [];
187
+ });
188
+ }
189
+
190
+ /**
191
+ * Check if a user message contains only tool results (no actual user text).
192
+ */
193
+ function isToolResultOnly(content: string | ContentBlock[]): boolean {
194
+ if (typeof content === "string") return false;
195
+
196
+ const hasToolResult = content.some((b) => b.type === "tool_result");
197
+ const hasText = content.some((b) => b.type === "text" && b.text?.trim());
198
+
199
+ return hasToolResult && !hasText;
200
+ }
201
+
202
+ /**
203
+ * Transform a conversation into our intermediate format.
204
+ */
205
+ function transformConversation(
206
+ records: ClaudeRecord[],
207
+ sourcePath: string,
208
+ warnings: Warning[],
209
+ ): Transcript {
210
+ const messages: Message[] = [];
211
+
212
+ for (const rec of records) {
213
+ const sourceRef = rec.uuid || "";
214
+ const timestamp = rec.timestamp || new Date().toISOString();
215
+ const parentMessageRef = rec.parentUuid || undefined;
216
+
217
+ if (rec.type === "user" && rec.message) {
218
+ // Skip tool-result-only user messages (they're just tool responses)
219
+ if (isToolResultOnly(rec.message.content)) continue;
220
+
221
+ const text = extractText(rec.message.content);
222
+ if (text.trim()) {
223
+ messages.push({
224
+ type: "user",
225
+ sourceRef,
226
+ timestamp,
227
+ parentMessageRef,
228
+ content: text,
229
+ });
230
+ }
231
+ } else if (rec.type === "assistant" && rec.message) {
232
+ const text = extractText(rec.message.content);
233
+ const thinking = extractThinking(rec.message.content);
234
+ const toolCalls = extractToolCalls(rec.message.content);
235
+
236
+ // Add assistant message if there's text or thinking
237
+ if (text.trim() || thinking) {
238
+ messages.push({
239
+ type: "assistant",
240
+ sourceRef,
241
+ timestamp,
242
+ parentMessageRef,
243
+ content: text,
244
+ thinking,
245
+ });
246
+ }
247
+
248
+ // Add tool calls as separate group
249
+ if (toolCalls.length > 0) {
250
+ messages.push({
251
+ type: "tool_calls",
252
+ sourceRef,
253
+ timestamp,
254
+ parentMessageRef,
255
+ calls: toolCalls,
256
+ });
257
+ }
258
+ } else if (rec.type === "system") {
259
+ const text = rec.content || "";
260
+ if (text.trim()) {
261
+ messages.push({
262
+ type: "system",
263
+ sourceRef,
264
+ timestamp,
265
+ parentMessageRef,
266
+ content: text,
267
+ });
268
+ }
269
+ }
270
+ }
271
+
272
+ return {
273
+ source: {
274
+ file: sourcePath,
275
+ adapter: "claude-code",
276
+ },
277
+ metadata: { warnings },
278
+ messages,
279
+ };
280
+ }
281
+
282
+ export const claudeCodeAdapter: Adapter = {
283
+ name: "claude-code",
284
+
285
+ parse(content: string, sourcePath: string): Transcript[] {
286
+ const { records, warnings } = parseJsonl(content);
287
+ const conversations = splitConversations(records);
288
+
289
+ if (conversations.length === 0) {
290
+ // Return single empty transcript with warnings
291
+ return [
292
+ {
293
+ source: { file: sourcePath, adapter: "claude-code" },
294
+ metadata: { warnings },
295
+ messages: [],
296
+ },
297
+ ];
298
+ }
299
+
300
+ // For single conversation, include all warnings
301
+ if (conversations.length === 1) {
302
+ return [transformConversation(conversations[0], sourcePath, warnings)];
303
+ }
304
+
305
+ // For multiple conversations, only first gets warnings
306
+ return conversations.map((conv, i) =>
307
+ transformConversation(conv, sourcePath, i === 0 ? warnings : []),
308
+ );
309
+ },
310
+ };