@arcreflex/agent-transcripts 0.1.8 → 0.1.9
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/CLAUDE.md +4 -0
- package/README.md +40 -3
- package/bun.lock +89 -0
- package/package.json +3 -2
- package/src/adapters/claude-code.ts +203 -32
- package/src/cache.ts +129 -0
- package/src/cli.ts +86 -5
- package/src/convert.ts +6 -14
- package/src/render-html.ts +1096 -0
- package/src/render-index.ts +611 -0
- package/src/render.ts +6 -110
- package/src/serve.ts +308 -0
- package/src/sync.ts +131 -18
- package/src/title.ts +172 -0
- package/src/types.ts +7 -0
- package/src/utils/html.ts +12 -0
- package/src/utils/openrouter.ts +116 -0
- package/src/utils/provenance.ts +25 -41
- package/src/utils/tree.ts +116 -0
- package/test/fixtures/claude/non-message-parents.input.jsonl +9 -0
- package/test/fixtures/claude/non-message-parents.output.md +30 -0
package/CLAUDE.md
CHANGED
|
@@ -2,6 +2,10 @@
|
|
|
2
2
|
|
|
3
3
|
@README.md
|
|
4
4
|
|
|
5
|
+
## Architectural Notes
|
|
6
|
+
|
|
7
|
+
- **Source paths are stable**: Session source paths (e.g., `~/.claude/projects/.../sessions/`) are standardized by the tools that create them. Don't over-engineer for path changes—use source paths as cache keys directly.
|
|
8
|
+
|
|
5
9
|
## Verification
|
|
6
10
|
|
|
7
11
|
Before committing:
|
package/README.md
CHANGED
|
@@ -15,14 +15,22 @@ src/
|
|
|
15
15
|
cli.ts # CLI entry point, subcommand routing
|
|
16
16
|
parse.ts # Source → intermediate format
|
|
17
17
|
render.ts # Intermediate format → markdown
|
|
18
|
+
render-html.ts # HTML transcript rendering
|
|
19
|
+
render-index.ts # Index page rendering
|
|
18
20
|
convert.ts # Full pipeline with provenance tracking
|
|
19
21
|
sync.ts # Batch sync sessions → markdown
|
|
22
|
+
serve.ts # HTTP server for dynamic transcript serving
|
|
23
|
+
cache.ts # Content-hash-based caching (~/.cache/agent-transcripts/)
|
|
24
|
+
title.ts # LLM title generation
|
|
20
25
|
types.ts # Core types (Transcript, Message, Adapter)
|
|
21
26
|
adapters/ # Source format adapters (currently: claude-code)
|
|
22
27
|
utils/
|
|
23
28
|
naming.ts # Deterministic output file naming
|
|
24
29
|
provenance.ts # Source tracking via transcripts.json + YAML front matter
|
|
25
30
|
summary.ts # Tool call summary extraction
|
|
31
|
+
openrouter.ts # OpenRouter API client for title generation
|
|
32
|
+
html.ts # HTML escaping utility
|
|
33
|
+
tree.ts # Tree navigation utilities
|
|
26
34
|
test/
|
|
27
35
|
fixtures/ # Snapshot test inputs/outputs
|
|
28
36
|
snapshots.test.ts
|
|
@@ -43,6 +51,8 @@ bun run format # auto-format
|
|
|
43
51
|
agent-transcripts convert <file> # Parse and render to stdout
|
|
44
52
|
agent-transcripts convert <file> -o <dir> # Parse and render to directory
|
|
45
53
|
agent-transcripts sync <dir> -o <out> # Batch sync sessions
|
|
54
|
+
agent-transcripts serve <dir> # Serve transcripts via HTTP
|
|
55
|
+
agent-transcripts serve <dir> -p 8080 # Serve on custom port
|
|
46
56
|
|
|
47
57
|
# Use "-" for stdin
|
|
48
58
|
cat session.jsonl | agent-transcripts -
|
|
@@ -58,11 +68,33 @@ Two-stage pipeline: Parse (source → intermediate) → Render (intermediate →
|
|
|
58
68
|
- Provenance tracking via `transcripts.json` index + YAML front matter
|
|
59
69
|
- Deterministic naming: `{datetime}-{sessionId}.md`
|
|
60
70
|
- Sync uses sessions-index.json for discovery (claude-code), skipping subagent files
|
|
61
|
-
- Sync uses
|
|
71
|
+
- Sync uses content hash to skip unchanged sources (see Cache section)
|
|
72
|
+
|
|
73
|
+
### Cache
|
|
74
|
+
|
|
75
|
+
Derived content (rendered outputs, LLM-generated titles) is cached at `~/.cache/agent-transcripts/`:
|
|
76
|
+
|
|
77
|
+
```
|
|
78
|
+
~/.cache/agent-transcripts/
|
|
79
|
+
{source-path-hash}.json → CacheEntry
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
```typescript
|
|
83
|
+
interface CacheEntry {
|
|
84
|
+
contentHash: string; // hash of source content (invalidation key)
|
|
85
|
+
segments: Array<{
|
|
86
|
+
title?: string; // LLM-generated title
|
|
87
|
+
html?: string; // rendered HTML
|
|
88
|
+
md?: string; // rendered markdown
|
|
89
|
+
}>;
|
|
90
|
+
}
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
Cache is keyed by source path (hashed), invalidated by content hash. When source content changes, all cached data is invalidated and regenerated.
|
|
62
94
|
|
|
63
95
|
### transcripts.json
|
|
64
96
|
|
|
65
|
-
The index file
|
|
97
|
+
The index file is a table of contents for the output directory:
|
|
66
98
|
|
|
67
99
|
```typescript
|
|
68
100
|
interface TranscriptsIndex {
|
|
@@ -70,10 +102,15 @@ interface TranscriptsIndex {
|
|
|
70
102
|
entries: {
|
|
71
103
|
[outputFilename: string]: {
|
|
72
104
|
source: string; // absolute path to source
|
|
73
|
-
sourceMtime: number; // ms since epoch
|
|
74
105
|
sessionId: string; // full session ID from filename
|
|
75
106
|
segmentIndex?: number; // for multi-transcript sources (1-indexed)
|
|
76
107
|
syncedAt: string; // ISO timestamp
|
|
108
|
+
firstUserMessage: string; // first user message content
|
|
109
|
+
title?: string; // copied from cache for convenience
|
|
110
|
+
messageCount: number;
|
|
111
|
+
startTime: string; // ISO timestamp
|
|
112
|
+
endTime: string; // ISO timestamp
|
|
113
|
+
cwd?: string; // working directory
|
|
77
114
|
};
|
|
78
115
|
};
|
|
79
116
|
}
|
package/bun.lock
CHANGED
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
"name": "agent-transcripts",
|
|
7
7
|
"dependencies": {
|
|
8
8
|
"cmd-ts": "^0.13.0",
|
|
9
|
+
"shiki": "^3.21.0",
|
|
9
10
|
},
|
|
10
11
|
"devDependencies": {
|
|
11
12
|
"@types/bun": "^1.1.14",
|
|
@@ -15,40 +16,128 @@
|
|
|
15
16
|
},
|
|
16
17
|
},
|
|
17
18
|
"packages": {
|
|
19
|
+
"@shikijs/core": ["@shikijs/core@3.21.0", "", { "dependencies": { "@shikijs/types": "3.21.0", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4", "hast-util-to-html": "^9.0.5" } }, "sha512-AXSQu/2n1UIQekY8euBJlvFYZIw0PHY63jUzGbrOma4wPxzznJXTXkri+QcHeBNaFxiiOljKxxJkVSoB3PjbyA=="],
|
|
20
|
+
|
|
21
|
+
"@shikijs/engine-javascript": ["@shikijs/engine-javascript@3.21.0", "", { "dependencies": { "@shikijs/types": "3.21.0", "@shikijs/vscode-textmate": "^10.0.2", "oniguruma-to-es": "^4.3.4" } }, "sha512-ATwv86xlbmfD9n9gKRiwuPpWgPENAWCLwYCGz9ugTJlsO2kOzhOkvoyV/UD+tJ0uT7YRyD530x6ugNSffmvIiQ=="],
|
|
22
|
+
|
|
23
|
+
"@shikijs/engine-oniguruma": ["@shikijs/engine-oniguruma@3.21.0", "", { "dependencies": { "@shikijs/types": "3.21.0", "@shikijs/vscode-textmate": "^10.0.2" } }, "sha512-OYknTCct6qiwpQDqDdf3iedRdzj6hFlOPv5hMvI+hkWfCKs5mlJ4TXziBG9nyabLwGulrUjHiCq3xCspSzErYQ=="],
|
|
24
|
+
|
|
25
|
+
"@shikijs/langs": ["@shikijs/langs@3.21.0", "", { "dependencies": { "@shikijs/types": "3.21.0" } }, "sha512-g6mn5m+Y6GBJ4wxmBYqalK9Sp0CFkUqfNzUy2pJglUginz6ZpWbaWjDB4fbQ/8SHzFjYbtU6Ddlp1pc+PPNDVA=="],
|
|
26
|
+
|
|
27
|
+
"@shikijs/themes": ["@shikijs/themes@3.21.0", "", { "dependencies": { "@shikijs/types": "3.21.0" } }, "sha512-BAE4cr9EDiZyYzwIHEk7JTBJ9CzlPuM4PchfcA5ao1dWXb25nv6hYsoDiBq2aZK9E3dlt3WB78uI96UESD+8Mw=="],
|
|
28
|
+
|
|
29
|
+
"@shikijs/types": ["@shikijs/types@3.21.0", "", { "dependencies": { "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-zGrWOxZ0/+0ovPY7PvBU2gIS9tmhSUUt30jAcNV0Bq0gb2S98gwfjIs1vxlmH5zM7/4YxLamT6ChlqqAJmPPjA=="],
|
|
30
|
+
|
|
31
|
+
"@shikijs/vscode-textmate": ["@shikijs/vscode-textmate@10.0.2", "", {}, "sha512-83yeghZ2xxin3Nj8z1NMd/NCuca+gsYXswywDy5bHvwlWL8tpTQmzGeUuHd9FC3E/SBEMvzJRwWEOz5gGes9Qg=="],
|
|
32
|
+
|
|
18
33
|
"@types/bun": ["@types/bun@1.3.6", "", { "dependencies": { "bun-types": "1.3.6" } }, "sha512-uWCv6FO/8LcpREhenN1d1b6fcspAB+cefwD7uti8C8VffIv0Um08TKMn98FynpTiU38+y2dUO55T11NgDt8VAA=="],
|
|
19
34
|
|
|
35
|
+
"@types/hast": ["@types/hast@3.0.4", "", { "dependencies": { "@types/unist": "*" } }, "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ=="],
|
|
36
|
+
|
|
37
|
+
"@types/mdast": ["@types/mdast@4.0.4", "", { "dependencies": { "@types/unist": "*" } }, "sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA=="],
|
|
38
|
+
|
|
20
39
|
"@types/node": ["@types/node@25.0.9", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-/rpCXHlCWeqClNBwUhDcusJxXYDjZTyE8v5oTO7WbL8eij2nKhUeU89/6xgjU7N4/Vh3He0BtyhJdQbDyhiXAw=="],
|
|
21
40
|
|
|
41
|
+
"@types/unist": ["@types/unist@3.0.3", "", {}, "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q=="],
|
|
42
|
+
|
|
43
|
+
"@ungap/structured-clone": ["@ungap/structured-clone@1.3.0", "", {}, "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g=="],
|
|
44
|
+
|
|
22
45
|
"ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="],
|
|
23
46
|
|
|
24
47
|
"ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="],
|
|
25
48
|
|
|
26
49
|
"bun-types": ["bun-types@1.3.6", "", { "dependencies": { "@types/node": "*" } }, "sha512-OlFwHcnNV99r//9v5IIOgQ9Uk37gZqrNMCcqEaExdkVq3Avwqok1bJFmvGMCkCE0FqzdY8VMOZpfpR3lwI+CsQ=="],
|
|
27
50
|
|
|
51
|
+
"ccount": ["ccount@2.0.1", "", {}, "sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg=="],
|
|
52
|
+
|
|
28
53
|
"chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="],
|
|
29
54
|
|
|
55
|
+
"character-entities-html4": ["character-entities-html4@2.1.0", "", {}, "sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA=="],
|
|
56
|
+
|
|
57
|
+
"character-entities-legacy": ["character-entities-legacy@3.0.0", "", {}, "sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ=="],
|
|
58
|
+
|
|
30
59
|
"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
60
|
|
|
32
61
|
"color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="],
|
|
33
62
|
|
|
34
63
|
"color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="],
|
|
35
64
|
|
|
65
|
+
"comma-separated-tokens": ["comma-separated-tokens@2.0.3", "", {}, "sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg=="],
|
|
66
|
+
|
|
36
67
|
"debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="],
|
|
37
68
|
|
|
69
|
+
"dequal": ["dequal@2.0.3", "", {}, "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA=="],
|
|
70
|
+
|
|
71
|
+
"devlop": ["devlop@1.1.0", "", { "dependencies": { "dequal": "^2.0.0" } }, "sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA=="],
|
|
72
|
+
|
|
38
73
|
"didyoumean": ["didyoumean@1.2.2", "", {}, "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw=="],
|
|
39
74
|
|
|
40
75
|
"has-flag": ["has-flag@4.0.0", "", {}, "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="],
|
|
41
76
|
|
|
77
|
+
"hast-util-to-html": ["hast-util-to-html@9.0.5", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/unist": "^3.0.0", "ccount": "^2.0.0", "comma-separated-tokens": "^2.0.0", "hast-util-whitespace": "^3.0.0", "html-void-elements": "^3.0.0", "mdast-util-to-hast": "^13.0.0", "property-information": "^7.0.0", "space-separated-tokens": "^2.0.0", "stringify-entities": "^4.0.0", "zwitch": "^2.0.4" } }, "sha512-OguPdidb+fbHQSU4Q4ZiLKnzWo8Wwsf5bZfbvu7//a9oTYoqD/fWpe96NuHkoS9h0ccGOTe0C4NGXdtS0iObOw=="],
|
|
78
|
+
|
|
79
|
+
"hast-util-whitespace": ["hast-util-whitespace@3.0.0", "", { "dependencies": { "@types/hast": "^3.0.0" } }, "sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw=="],
|
|
80
|
+
|
|
81
|
+
"html-void-elements": ["html-void-elements@3.0.0", "", {}, "sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg=="],
|
|
82
|
+
|
|
83
|
+
"mdast-util-to-hast": ["mdast-util-to-hast@13.2.1", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/mdast": "^4.0.0", "@ungap/structured-clone": "^1.0.0", "devlop": "^1.0.0", "micromark-util-sanitize-uri": "^2.0.0", "trim-lines": "^3.0.0", "unist-util-position": "^5.0.0", "unist-util-visit": "^5.0.0", "vfile": "^6.0.0" } }, "sha512-cctsq2wp5vTsLIcaymblUriiTcZd0CwWtCbLvrOzYCDZoWyMNV8sZ7krj09FSnsiJi3WVsHLM4k6Dq/yaPyCXA=="],
|
|
84
|
+
|
|
85
|
+
"micromark-util-character": ["micromark-util-character@2.1.1", "", { "dependencies": { "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q=="],
|
|
86
|
+
|
|
87
|
+
"micromark-util-encode": ["micromark-util-encode@2.0.1", "", {}, "sha512-c3cVx2y4KqUnwopcO9b/SCdo2O67LwJJ/UyqGfbigahfegL9myoEFoDYZgkT7f36T0bLrM9hZTAaAyH+PCAXjw=="],
|
|
88
|
+
|
|
89
|
+
"micromark-util-sanitize-uri": ["micromark-util-sanitize-uri@2.0.1", "", { "dependencies": { "micromark-util-character": "^2.0.0", "micromark-util-encode": "^2.0.0", "micromark-util-symbol": "^2.0.0" } }, "sha512-9N9IomZ/YuGGZZmQec1MbgxtlgougxTodVwDzzEouPKo3qFWvymFHWcnDi2vzV1ff6kas9ucW+o3yzJK9YB1AQ=="],
|
|
90
|
+
|
|
91
|
+
"micromark-util-symbol": ["micromark-util-symbol@2.0.1", "", {}, "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q=="],
|
|
92
|
+
|
|
93
|
+
"micromark-util-types": ["micromark-util-types@2.0.2", "", {}, "sha512-Yw0ECSpJoViF1qTU4DC6NwtC4aWGt1EkzaQB8KPPyCRR8z9TWeV0HbEFGTO+ZY1wB22zmxnJqhPyTpOVCpeHTA=="],
|
|
94
|
+
|
|
42
95
|
"ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],
|
|
43
96
|
|
|
97
|
+
"oniguruma-parser": ["oniguruma-parser@0.12.1", "", {}, "sha512-8Unqkvk1RYc6yq2WBYRj4hdnsAxVze8i7iPfQr8e4uSP3tRv0rpZcbGUDvxfQQcdwHt/e9PrMvGCsa8OqG9X3w=="],
|
|
98
|
+
|
|
99
|
+
"oniguruma-to-es": ["oniguruma-to-es@4.3.4", "", { "dependencies": { "oniguruma-parser": "^0.12.1", "regex": "^6.0.1", "regex-recursion": "^6.0.2" } }, "sha512-3VhUGN3w2eYxnTzHn+ikMI+fp/96KoRSVK9/kMTcFqj1NRDh2IhQCKvYxDnWePKRXY/AqH+Fuiyb7VHSzBjHfA=="],
|
|
100
|
+
|
|
44
101
|
"prettier": ["prettier@3.8.0", "", { "bin": { "prettier": "bin/prettier.cjs" } }, "sha512-yEPsovQfpxYfgWNhCfECjG5AQaO+K3dp6XERmOepyPDVqcJm+bjyCVO3pmU+nAPe0N5dDvekfGezt/EIiRe1TA=="],
|
|
45
102
|
|
|
103
|
+
"property-information": ["property-information@7.1.0", "", {}, "sha512-TwEZ+X+yCJmYfL7TPUOcvBZ4QfoT5YenQiJuX//0th53DE6w0xxLEtfK3iyryQFddXuvkIk51EEgrJQ0WJkOmQ=="],
|
|
104
|
+
|
|
105
|
+
"regex": ["regex@6.1.0", "", { "dependencies": { "regex-utilities": "^2.3.0" } }, "sha512-6VwtthbV4o/7+OaAF9I5L5V3llLEsoPyq9P1JVXkedTP33c7MfCG0/5NOPcSJn0TzXcG9YUrR0gQSWioew3LDg=="],
|
|
106
|
+
|
|
107
|
+
"regex-recursion": ["regex-recursion@6.0.2", "", { "dependencies": { "regex-utilities": "^2.3.0" } }, "sha512-0YCaSCq2VRIebiaUviZNs0cBz1kg5kVS2UKUfNIx8YVs1cN3AV7NTctO5FOKBA+UT2BPJIWZauYHPqJODG50cg=="],
|
|
108
|
+
|
|
109
|
+
"regex-utilities": ["regex-utilities@2.3.0", "", {}, "sha512-8VhliFJAWRaUiVvREIiW2NXXTmHs4vMNnSzuJVhscgmGav3g9VDxLrQndI3dZZVVdp0ZO/5v0xmX516/7M9cng=="],
|
|
110
|
+
|
|
111
|
+
"shiki": ["shiki@3.21.0", "", { "dependencies": { "@shikijs/core": "3.21.0", "@shikijs/engine-javascript": "3.21.0", "@shikijs/engine-oniguruma": "3.21.0", "@shikijs/langs": "3.21.0", "@shikijs/themes": "3.21.0", "@shikijs/types": "3.21.0", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-N65B/3bqL/TI2crrXr+4UivctrAGEjmsib5rPMMPpFp1xAx/w03v8WZ9RDDFYteXoEgY7qZ4HGgl5KBIu1153w=="],
|
|
112
|
+
|
|
113
|
+
"space-separated-tokens": ["space-separated-tokens@2.0.2", "", {}, "sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q=="],
|
|
114
|
+
|
|
115
|
+
"stringify-entities": ["stringify-entities@4.0.4", "", { "dependencies": { "character-entities-html4": "^2.0.0", "character-entities-legacy": "^3.0.0" } }, "sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg=="],
|
|
116
|
+
|
|
46
117
|
"strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="],
|
|
47
118
|
|
|
48
119
|
"supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="],
|
|
49
120
|
|
|
121
|
+
"trim-lines": ["trim-lines@3.0.1", "", {}, "sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg=="],
|
|
122
|
+
|
|
50
123
|
"typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="],
|
|
51
124
|
|
|
52
125
|
"undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="],
|
|
126
|
+
|
|
127
|
+
"unist-util-is": ["unist-util-is@6.0.1", "", { "dependencies": { "@types/unist": "^3.0.0" } }, "sha512-LsiILbtBETkDz8I9p1dQ0uyRUWuaQzd/cuEeS1hoRSyW5E5XGmTzlwY1OrNzzakGowI9Dr/I8HVaw4hTtnxy8g=="],
|
|
128
|
+
|
|
129
|
+
"unist-util-position": ["unist-util-position@5.0.0", "", { "dependencies": { "@types/unist": "^3.0.0" } }, "sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA=="],
|
|
130
|
+
|
|
131
|
+
"unist-util-stringify-position": ["unist-util-stringify-position@4.0.0", "", { "dependencies": { "@types/unist": "^3.0.0" } }, "sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ=="],
|
|
132
|
+
|
|
133
|
+
"unist-util-visit": ["unist-util-visit@5.1.0", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-is": "^6.0.0", "unist-util-visit-parents": "^6.0.0" } }, "sha512-m+vIdyeCOpdr/QeQCu2EzxX/ohgS8KbnPDgFni4dQsfSCtpz8UqDyY5GjRru8PDKuYn7Fq19j1CQ+nJSsGKOzg=="],
|
|
134
|
+
|
|
135
|
+
"unist-util-visit-parents": ["unist-util-visit-parents@6.0.2", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-is": "^6.0.0" } }, "sha512-goh1s1TBrqSqukSc8wrjwWhL0hiJxgA8m4kFxGlQ+8FYQ3C/m11FcTs4YYem7V664AhHVvgoQLk890Ssdsr2IQ=="],
|
|
136
|
+
|
|
137
|
+
"vfile": ["vfile@6.0.3", "", { "dependencies": { "@types/unist": "^3.0.0", "vfile-message": "^4.0.0" } }, "sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q=="],
|
|
138
|
+
|
|
139
|
+
"vfile-message": ["vfile-message@4.0.3", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-stringify-position": "^4.0.0" } }, "sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw=="],
|
|
140
|
+
|
|
141
|
+
"zwitch": ["zwitch@2.0.4", "", {}, "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A=="],
|
|
53
142
|
}
|
|
54
143
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@arcreflex/agent-transcripts",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.9",
|
|
4
4
|
"description": "Transform AI coding agent session files into readable transcripts",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"repository": {
|
|
@@ -21,7 +21,8 @@
|
|
|
21
21
|
"format": "prettier --write ."
|
|
22
22
|
},
|
|
23
23
|
"dependencies": {
|
|
24
|
-
"cmd-ts": "^0.13.0"
|
|
24
|
+
"cmd-ts": "^0.13.0",
|
|
25
|
+
"shiki": "^3.21.0"
|
|
25
26
|
},
|
|
26
27
|
"devDependencies": {
|
|
27
28
|
"@types/bun": "^1.1.14",
|
|
@@ -44,6 +44,7 @@ interface ClaudeRecord {
|
|
|
44
44
|
};
|
|
45
45
|
content?: string;
|
|
46
46
|
subtype?: string;
|
|
47
|
+
cwd?: string;
|
|
47
48
|
}
|
|
48
49
|
|
|
49
50
|
interface ContentBlock {
|
|
@@ -54,7 +55,7 @@ interface ContentBlock {
|
|
|
54
55
|
name?: string;
|
|
55
56
|
input?: Record<string, unknown>;
|
|
56
57
|
tool_use_id?: string;
|
|
57
|
-
content?: string
|
|
58
|
+
content?: unknown; // Can be string, array, or other structure
|
|
58
59
|
}
|
|
59
60
|
|
|
60
61
|
/**
|
|
@@ -86,11 +87,42 @@ function parseJsonl(content: string): {
|
|
|
86
87
|
return { records, warnings };
|
|
87
88
|
}
|
|
88
89
|
|
|
90
|
+
/**
|
|
91
|
+
* Find the nearest message ancestor by walking up the parent chain.
|
|
92
|
+
* Returns undefined if no message ancestor exists.
|
|
93
|
+
*/
|
|
94
|
+
function findMessageAncestor(
|
|
95
|
+
parentUuid: string | null | undefined,
|
|
96
|
+
allByUuid: Map<string, ClaudeRecord>,
|
|
97
|
+
messageUuids: Set<string>,
|
|
98
|
+
): string | undefined {
|
|
99
|
+
const visited = new Set<string>();
|
|
100
|
+
let current = parentUuid;
|
|
101
|
+
while (current) {
|
|
102
|
+
if (visited.has(current)) {
|
|
103
|
+
return undefined; // Cycle detected
|
|
104
|
+
}
|
|
105
|
+
visited.add(current);
|
|
106
|
+
if (messageUuids.has(current)) {
|
|
107
|
+
return current;
|
|
108
|
+
}
|
|
109
|
+
const rec = allByUuid.get(current);
|
|
110
|
+
current = rec?.parentUuid ?? null;
|
|
111
|
+
}
|
|
112
|
+
return undefined;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
interface SplitResult {
|
|
116
|
+
conversations: ClaudeRecord[][];
|
|
117
|
+
/** Map from message UUID to its resolved parent (nearest message ancestor) */
|
|
118
|
+
resolvedParents: Map<string, string | undefined>;
|
|
119
|
+
}
|
|
120
|
+
|
|
89
121
|
/**
|
|
90
122
|
* Build message graph and find conversation boundaries.
|
|
91
|
-
* Returns
|
|
123
|
+
* Returns conversations and a map of resolved parent references.
|
|
92
124
|
*/
|
|
93
|
-
function splitConversations(records: ClaudeRecord[]):
|
|
125
|
+
function splitConversations(records: ClaudeRecord[]): SplitResult {
|
|
94
126
|
// Filter to only message records (user, assistant, system with uuid)
|
|
95
127
|
const messageRecords = records.filter(
|
|
96
128
|
(r) =>
|
|
@@ -98,29 +130,52 @@ function splitConversations(records: ClaudeRecord[]): ClaudeRecord[][] {
|
|
|
98
130
|
(r.type === "user" || r.type === "assistant" || r.type === "system"),
|
|
99
131
|
);
|
|
100
132
|
|
|
101
|
-
if (messageRecords.length === 0)
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
const byUuid = new Map<string, ClaudeRecord>();
|
|
105
|
-
const children = new Map<string, string[]>();
|
|
133
|
+
if (messageRecords.length === 0) {
|
|
134
|
+
return { conversations: [], resolvedParents: new Map() };
|
|
135
|
+
}
|
|
106
136
|
|
|
107
|
-
for
|
|
137
|
+
// Build UUID lookup for ALL records to track parent chains through non-messages
|
|
138
|
+
const allByUuid = new Map<string, ClaudeRecord>();
|
|
139
|
+
for (const rec of records) {
|
|
108
140
|
if (rec.uuid) {
|
|
109
|
-
|
|
110
|
-
const parent = rec.parentUuid;
|
|
111
|
-
if (parent) {
|
|
112
|
-
const existing = children.get(parent) || [];
|
|
113
|
-
existing.push(rec.uuid);
|
|
114
|
-
children.set(parent, existing);
|
|
115
|
-
}
|
|
141
|
+
allByUuid.set(rec.uuid, rec);
|
|
116
142
|
}
|
|
117
143
|
}
|
|
118
144
|
|
|
119
|
-
//
|
|
145
|
+
// Set of message UUIDs for quick lookup
|
|
146
|
+
const messageUuids = new Set<string>();
|
|
147
|
+
for (const rec of messageRecords) {
|
|
148
|
+
if (rec.uuid) messageUuids.add(rec.uuid);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// Build parent → children map, resolving through non-message records
|
|
152
|
+
// Also track resolved parents for use in transformation
|
|
153
|
+
const byUuid = new Map<string, ClaudeRecord>();
|
|
154
|
+
const children = new Map<string, string[]>();
|
|
155
|
+
const resolvedParents = new Map<string, string | undefined>();
|
|
120
156
|
const roots: string[] = [];
|
|
157
|
+
|
|
121
158
|
for (const rec of messageRecords) {
|
|
122
|
-
if (!rec.
|
|
123
|
-
|
|
159
|
+
if (!rec.uuid) continue;
|
|
160
|
+
byUuid.set(rec.uuid, rec);
|
|
161
|
+
|
|
162
|
+
// Find nearest message ancestor (walking through non-message records)
|
|
163
|
+
const ancestor = findMessageAncestor(
|
|
164
|
+
rec.parentUuid,
|
|
165
|
+
allByUuid,
|
|
166
|
+
messageUuids,
|
|
167
|
+
);
|
|
168
|
+
|
|
169
|
+
// Store resolved parent for this message
|
|
170
|
+
resolvedParents.set(rec.uuid, ancestor);
|
|
171
|
+
|
|
172
|
+
if (ancestor) {
|
|
173
|
+
const existing = children.get(ancestor) || [];
|
|
174
|
+
existing.push(rec.uuid);
|
|
175
|
+
children.set(ancestor, existing);
|
|
176
|
+
} else {
|
|
177
|
+
// No message ancestor - this is a root
|
|
178
|
+
roots.push(rec.uuid);
|
|
124
179
|
}
|
|
125
180
|
}
|
|
126
181
|
|
|
@@ -160,7 +215,7 @@ function splitConversations(records: ClaudeRecord[]): ClaudeRecord[][] {
|
|
|
160
215
|
return ta - tb;
|
|
161
216
|
});
|
|
162
217
|
|
|
163
|
-
return conversations;
|
|
218
|
+
return { conversations, resolvedParents };
|
|
164
219
|
}
|
|
165
220
|
|
|
166
221
|
/**
|
|
@@ -189,16 +244,23 @@ function extractThinking(content: string | ContentBlock[]): string | undefined {
|
|
|
189
244
|
|
|
190
245
|
/**
|
|
191
246
|
* Extract tool calls from content blocks.
|
|
247
|
+
* Matches with results from the toolResults map.
|
|
192
248
|
*/
|
|
193
|
-
function extractToolCalls(
|
|
249
|
+
function extractToolCalls(
|
|
250
|
+
content: string | ContentBlock[],
|
|
251
|
+
toolResults: Map<string, string>,
|
|
252
|
+
): ToolCall[] {
|
|
194
253
|
if (typeof content === "string") return [];
|
|
195
254
|
|
|
196
255
|
return content.flatMap((b) => {
|
|
197
|
-
if (b.type === "tool_use" && b.name) {
|
|
256
|
+
if (b.type === "tool_use" && b.name && b.id) {
|
|
257
|
+
const result = toolResults.get(b.id);
|
|
198
258
|
return [
|
|
199
259
|
{
|
|
200
260
|
name: b.name,
|
|
201
261
|
summary: extractToolSummary(b.name, b.input || {}),
|
|
262
|
+
input: b.input,
|
|
263
|
+
result,
|
|
202
264
|
},
|
|
203
265
|
];
|
|
204
266
|
}
|
|
@@ -206,6 +268,39 @@ function extractToolCalls(content: string | ContentBlock[]): ToolCall[] {
|
|
|
206
268
|
});
|
|
207
269
|
}
|
|
208
270
|
|
|
271
|
+
/**
|
|
272
|
+
* Safely convert tool result content to string.
|
|
273
|
+
* Content can be a string, array, or other structure.
|
|
274
|
+
*/
|
|
275
|
+
function stringifyToolResult(content: unknown): string {
|
|
276
|
+
if (typeof content === "string") return content;
|
|
277
|
+
if (content === null || content === undefined) return "";
|
|
278
|
+
// For arrays or objects, JSON stringify for display
|
|
279
|
+
try {
|
|
280
|
+
return JSON.stringify(content, null, 2);
|
|
281
|
+
} catch {
|
|
282
|
+
return String(content);
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
/**
|
|
287
|
+
* Extract tool results from content blocks.
|
|
288
|
+
* Returns a map of tool_use_id → result content.
|
|
289
|
+
*/
|
|
290
|
+
function extractToolResults(
|
|
291
|
+
content: string | ContentBlock[],
|
|
292
|
+
): Map<string, string> {
|
|
293
|
+
const results = new Map<string, string>();
|
|
294
|
+
if (typeof content === "string") return results;
|
|
295
|
+
|
|
296
|
+
for (const b of content) {
|
|
297
|
+
if (b.type === "tool_result" && b.tool_use_id && b.content !== undefined) {
|
|
298
|
+
results.set(b.tool_use_id, stringifyToolResult(b.content));
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
return results;
|
|
302
|
+
}
|
|
303
|
+
|
|
209
304
|
/**
|
|
210
305
|
* Check if a user message contains only tool results (no actual user text).
|
|
211
306
|
*/
|
|
@@ -230,8 +325,13 @@ function resolveParent(
|
|
|
230
325
|
if (!parentUuid) return undefined;
|
|
231
326
|
|
|
232
327
|
// Follow the chain through any skipped messages
|
|
328
|
+
const visited = new Set<string>();
|
|
233
329
|
let current: string | undefined = parentUuid;
|
|
234
330
|
while (current && skippedParents.has(current)) {
|
|
331
|
+
if (visited.has(current)) {
|
|
332
|
+
return undefined; // Cycle detected
|
|
333
|
+
}
|
|
334
|
+
visited.add(current);
|
|
235
335
|
current = skippedParents.get(current);
|
|
236
336
|
}
|
|
237
337
|
|
|
@@ -245,17 +345,36 @@ function transformConversation(
|
|
|
245
345
|
records: ClaudeRecord[],
|
|
246
346
|
sourcePath: string,
|
|
247
347
|
warnings: Warning[],
|
|
348
|
+
resolvedParents: Map<string, string | undefined>,
|
|
248
349
|
): Transcript {
|
|
249
350
|
const messages: Message[] = [];
|
|
250
|
-
// Track skipped message UUIDs → their parent UUIDs for chain repair
|
|
351
|
+
// Track skipped message UUIDs → their resolved parent UUIDs for chain repair
|
|
251
352
|
const skippedParents = new Map<string, string | undefined>();
|
|
252
353
|
|
|
354
|
+
// Collect all tool results from user messages (tool_use_id → result)
|
|
355
|
+
const allToolResults = new Map<string, string>();
|
|
356
|
+
for (const rec of records) {
|
|
357
|
+
if (rec.type === "user" && rec.message) {
|
|
358
|
+
const results = extractToolResults(rec.message.content);
|
|
359
|
+
for (const [id, content] of results) {
|
|
360
|
+
allToolResults.set(id, content);
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
let cwd: string | undefined;
|
|
366
|
+
|
|
253
367
|
// First pass: identify which messages will be skipped
|
|
254
368
|
for (const rec of records) {
|
|
255
369
|
if (!rec.uuid) continue;
|
|
256
370
|
|
|
257
371
|
let willSkip = false;
|
|
258
372
|
|
|
373
|
+
// Take the first cwd we find.
|
|
374
|
+
if (!cwd && rec.cwd) {
|
|
375
|
+
cwd = rec.cwd;
|
|
376
|
+
}
|
|
377
|
+
|
|
259
378
|
if (rec.type === "user" && rec.message) {
|
|
260
379
|
if (isToolResultOnly(rec.message.content)) {
|
|
261
380
|
willSkip = true;
|
|
@@ -266,7 +385,7 @@ function transformConversation(
|
|
|
266
385
|
} else if (rec.type === "assistant" && rec.message) {
|
|
267
386
|
const text = extractText(rec.message.content);
|
|
268
387
|
const thinking = extractThinking(rec.message.content);
|
|
269
|
-
const toolCalls = extractToolCalls(rec.message.content);
|
|
388
|
+
const toolCalls = extractToolCalls(rec.message.content, allToolResults);
|
|
270
389
|
// Only skip if no text, no thinking, AND no tool calls
|
|
271
390
|
if (!text.trim() && !thinking && toolCalls.length === 0) {
|
|
272
391
|
willSkip = true;
|
|
@@ -277,7 +396,8 @@ function transformConversation(
|
|
|
277
396
|
}
|
|
278
397
|
|
|
279
398
|
if (willSkip) {
|
|
280
|
-
|
|
399
|
+
// Use the resolved parent (already walked through non-message records)
|
|
400
|
+
skippedParents.set(rec.uuid, resolvedParents.get(rec.uuid));
|
|
281
401
|
}
|
|
282
402
|
}
|
|
283
403
|
|
|
@@ -285,7 +405,12 @@ function transformConversation(
|
|
|
285
405
|
for (const rec of records) {
|
|
286
406
|
const sourceRef = rec.uuid || "";
|
|
287
407
|
const timestamp = rec.timestamp || new Date().toISOString();
|
|
288
|
-
|
|
408
|
+
// Start with the resolved parent (through non-message records),
|
|
409
|
+
// then walk through any skipped messages
|
|
410
|
+
const parentMessageRef = rec.uuid
|
|
411
|
+
? resolveParent(resolvedParents.get(rec.uuid), skippedParents)
|
|
412
|
+
: undefined;
|
|
413
|
+
const rawJson = JSON.stringify(rec);
|
|
289
414
|
|
|
290
415
|
if (rec.type === "user" && rec.message) {
|
|
291
416
|
// Skip tool-result-only user messages (they're just tool responses)
|
|
@@ -298,13 +423,14 @@ function transformConversation(
|
|
|
298
423
|
sourceRef,
|
|
299
424
|
timestamp,
|
|
300
425
|
parentMessageRef,
|
|
426
|
+
rawJson,
|
|
301
427
|
content: text,
|
|
302
428
|
});
|
|
303
429
|
}
|
|
304
430
|
} else if (rec.type === "assistant" && rec.message) {
|
|
305
431
|
const text = extractText(rec.message.content);
|
|
306
432
|
const thinking = extractThinking(rec.message.content);
|
|
307
|
-
const toolCalls = extractToolCalls(rec.message.content);
|
|
433
|
+
const toolCalls = extractToolCalls(rec.message.content, allToolResults);
|
|
308
434
|
|
|
309
435
|
// Add assistant message if there's text or thinking
|
|
310
436
|
if (text.trim() || thinking) {
|
|
@@ -313,6 +439,7 @@ function transformConversation(
|
|
|
313
439
|
sourceRef,
|
|
314
440
|
timestamp,
|
|
315
441
|
parentMessageRef,
|
|
442
|
+
rawJson,
|
|
316
443
|
content: text,
|
|
317
444
|
thinking,
|
|
318
445
|
});
|
|
@@ -325,6 +452,7 @@ function transformConversation(
|
|
|
325
452
|
sourceRef,
|
|
326
453
|
timestamp,
|
|
327
454
|
parentMessageRef,
|
|
455
|
+
rawJson,
|
|
328
456
|
calls: toolCalls,
|
|
329
457
|
});
|
|
330
458
|
}
|
|
@@ -336,18 +464,42 @@ function transformConversation(
|
|
|
336
464
|
sourceRef,
|
|
337
465
|
timestamp,
|
|
338
466
|
parentMessageRef,
|
|
467
|
+
rawJson,
|
|
339
468
|
content: text,
|
|
340
469
|
});
|
|
341
470
|
}
|
|
342
471
|
}
|
|
343
472
|
}
|
|
344
473
|
|
|
474
|
+
// Compute time bounds from min/max across all messages (not array order,
|
|
475
|
+
// which is BFS traversal order and may not be chronological for branches)
|
|
476
|
+
let minTime = Infinity;
|
|
477
|
+
let maxTime = -Infinity;
|
|
478
|
+
for (const msg of messages) {
|
|
479
|
+
const t = new Date(msg.timestamp).getTime();
|
|
480
|
+
if (t < minTime) minTime = t;
|
|
481
|
+
if (t > maxTime) maxTime = t;
|
|
482
|
+
}
|
|
483
|
+
const now = new Date().toISOString();
|
|
484
|
+
const startTime = Number.isFinite(minTime)
|
|
485
|
+
? new Date(minTime).toISOString()
|
|
486
|
+
: now;
|
|
487
|
+
const endTime = Number.isFinite(maxTime)
|
|
488
|
+
? new Date(maxTime).toISOString()
|
|
489
|
+
: startTime;
|
|
490
|
+
|
|
345
491
|
return {
|
|
346
492
|
source: {
|
|
347
493
|
file: sourcePath,
|
|
348
494
|
adapter: "claude-code",
|
|
349
495
|
},
|
|
350
|
-
metadata: {
|
|
496
|
+
metadata: {
|
|
497
|
+
warnings,
|
|
498
|
+
messageCount: messages.length,
|
|
499
|
+
startTime,
|
|
500
|
+
endTime,
|
|
501
|
+
cwd,
|
|
502
|
+
},
|
|
351
503
|
messages,
|
|
352
504
|
};
|
|
353
505
|
}
|
|
@@ -435,14 +587,21 @@ export const claudeCodeAdapter: Adapter = {
|
|
|
435
587
|
|
|
436
588
|
parse(content: string, sourcePath: string): Transcript[] {
|
|
437
589
|
const { records, warnings } = parseJsonl(content);
|
|
438
|
-
const conversations = splitConversations(records);
|
|
590
|
+
const { conversations, resolvedParents } = splitConversations(records);
|
|
439
591
|
|
|
440
592
|
if (conversations.length === 0) {
|
|
441
593
|
// Return single empty transcript with warnings
|
|
594
|
+
const now = new Date().toISOString();
|
|
442
595
|
return [
|
|
443
596
|
{
|
|
444
597
|
source: { file: sourcePath, adapter: "claude-code" },
|
|
445
|
-
metadata: {
|
|
598
|
+
metadata: {
|
|
599
|
+
warnings,
|
|
600
|
+
messageCount: 0,
|
|
601
|
+
startTime: now,
|
|
602
|
+
endTime: now,
|
|
603
|
+
cwd: undefined,
|
|
604
|
+
},
|
|
446
605
|
messages: [],
|
|
447
606
|
},
|
|
448
607
|
];
|
|
@@ -450,12 +609,24 @@ export const claudeCodeAdapter: Adapter = {
|
|
|
450
609
|
|
|
451
610
|
// For single conversation, include all warnings
|
|
452
611
|
if (conversations.length === 1) {
|
|
453
|
-
return [
|
|
612
|
+
return [
|
|
613
|
+
transformConversation(
|
|
614
|
+
conversations[0],
|
|
615
|
+
sourcePath,
|
|
616
|
+
warnings,
|
|
617
|
+
resolvedParents,
|
|
618
|
+
),
|
|
619
|
+
];
|
|
454
620
|
}
|
|
455
621
|
|
|
456
622
|
// For multiple conversations, only first gets warnings
|
|
457
623
|
return conversations.map((conv, i) =>
|
|
458
|
-
transformConversation(
|
|
624
|
+
transformConversation(
|
|
625
|
+
conv,
|
|
626
|
+
sourcePath,
|
|
627
|
+
i === 0 ? warnings : [],
|
|
628
|
+
resolvedParents,
|
|
629
|
+
),
|
|
459
630
|
);
|
|
460
631
|
},
|
|
461
632
|
};
|