@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.
- package/.prettierignore +6 -0
- package/README.md +58 -0
- package/bin/agent-transcripts +2 -0
- package/bun.lock +54 -0
- package/package.json +31 -0
- package/spec.md +239 -0
- package/src/adapters/claude-code.ts +310 -0
- package/src/adapters/index.ts +45 -0
- package/src/cli.ts +114 -0
- package/src/parse.ts +127 -0
- package/src/render.ts +354 -0
- package/src/types.ts +75 -0
- package/src/utils/summary.ts +43 -0
- package/test/fixtures/claude/basic-conversation.input.jsonl +4 -0
- package/test/fixtures/claude/basic-conversation.output.md +28 -0
- package/test/fixtures/claude/multiple-tools.input.jsonl +2 -0
- package/test/fixtures/claude/multiple-tools.output.md +15 -0
- package/test/fixtures/claude/with-thinking.input.jsonl +2 -0
- package/test/fixtures/claude/with-thinking.output.md +24 -0
- package/test/fixtures/claude/with-tools.input.jsonl +2 -0
- package/test/fixtures/claude/with-tools.output.md +16 -0
- package/test/snapshots.test.ts +67 -0
- package/tsconfig.json +15 -0
package/.prettierignore
ADDED
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.
|
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
|
+
};
|