@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 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 mtime via index to skip unchanged sources
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 tracks the relationship between source files and outputs:
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.8",
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 array of conversation groups (each is array of records in order).
123
+ * Returns conversations and a map of resolved parent references.
92
124
  */
93
- function splitConversations(records: ClaudeRecord[]): 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) return [];
102
-
103
- // Build parent → children map
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 (const rec of messageRecords) {
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
- byUuid.set(rec.uuid, rec);
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
- // Find roots (no parent or parent not in our set)
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.parentUuid || !byUuid.has(rec.parentUuid)) {
123
- if (rec.uuid) roots.push(rec.uuid);
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(content: string | ContentBlock[]): ToolCall[] {
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
- skippedParents.set(rec.uuid, rec.parentUuid || undefined);
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
- const parentMessageRef = resolveParent(rec.parentUuid, skippedParents);
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: { warnings },
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: { warnings },
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 [transformConversation(conversations[0], sourcePath, warnings)];
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(conv, sourcePath, i === 0 ? warnings : []),
624
+ transformConversation(
625
+ conv,
626
+ sourcePath,
627
+ i === 0 ? warnings : [],
628
+ resolvedParents,
629
+ ),
459
630
  );
460
631
  },
461
632
  };