@arcreflex/agent-transcripts 0.1.1 → 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,24 @@
1
+ name: Publish
2
+
3
+ on:
4
+ push:
5
+ tags:
6
+ - "v*"
7
+
8
+ jobs:
9
+ publish:
10
+ runs-on: ubuntu-latest
11
+ permissions:
12
+ id-token: write
13
+ steps:
14
+ - uses: actions/checkout@v4
15
+
16
+ - uses: oven-sh/setup-bun@v2
17
+
18
+ - run: bun install
19
+
20
+ - run: bun run check
21
+
22
+ - run: bun test
23
+
24
+ - run: npm publish --provenance --access public
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@arcreflex/agent-transcripts",
3
- "version": "0.1.1",
3
+ "version": "0.1.2",
4
4
  "description": "Transform AI coding agent session files into readable transcripts",
5
5
  "type": "module",
6
6
  "repository": {
@@ -199,6 +199,26 @@ function isToolResultOnly(content: string | ContentBlock[]): boolean {
199
199
  return hasToolResult && !hasText;
200
200
  }
201
201
 
202
+ /**
203
+ * Resolve a parent reference through any skipped messages.
204
+ * When messages are skipped (e.g., tool-result-only user messages),
205
+ * we redirect parent references to the skipped message's parent.
206
+ */
207
+ function resolveParent(
208
+ parentUuid: string | null | undefined,
209
+ skippedParents: Map<string, string | undefined>,
210
+ ): string | undefined {
211
+ if (!parentUuid) return undefined;
212
+
213
+ // Follow the chain through any skipped messages
214
+ let current: string | undefined = parentUuid;
215
+ while (current && skippedParents.has(current)) {
216
+ current = skippedParents.get(current);
217
+ }
218
+
219
+ return current;
220
+ }
221
+
202
222
  /**
203
223
  * Transform a conversation into our intermediate format.
204
224
  */
@@ -208,11 +228,45 @@ function transformConversation(
208
228
  warnings: Warning[],
209
229
  ): Transcript {
210
230
  const messages: Message[] = [];
231
+ // Track skipped message UUIDs → their parent UUIDs for chain repair
232
+ const skippedParents = new Map<string, string | undefined>();
233
+
234
+ // First pass: identify which messages will be skipped
235
+ for (const rec of records) {
236
+ if (!rec.uuid) continue;
237
+
238
+ let willSkip = false;
239
+
240
+ if (rec.type === "user" && rec.message) {
241
+ if (isToolResultOnly(rec.message.content)) {
242
+ willSkip = true;
243
+ } else {
244
+ const text = extractText(rec.message.content);
245
+ if (!text.trim()) willSkip = true;
246
+ }
247
+ } else if (rec.type === "assistant" && rec.message) {
248
+ const text = extractText(rec.message.content);
249
+ const thinking = extractThinking(rec.message.content);
250
+ const toolCalls = extractToolCalls(rec.message.content);
251
+ // Only skip if no text, no thinking, AND no tool calls
252
+ if (!text.trim() && !thinking && toolCalls.length === 0) {
253
+ willSkip = true;
254
+ }
255
+ } else if (rec.type === "system") {
256
+ const text = rec.content || "";
257
+ if (!text.trim()) willSkip = true;
258
+ }
259
+
260
+ if (willSkip) {
261
+ skippedParents.set(rec.uuid, rec.parentUuid || undefined);
262
+ }
263
+ }
211
264
 
265
+ // Second pass: build messages with corrected parent references
212
266
  for (const rec of records) {
213
267
  const sourceRef = rec.uuid || "";
214
268
  const timestamp = rec.timestamp || new Date().toISOString();
215
- const parentMessageRef = rec.parentUuid || undefined;
269
+ const parentMessageRef = resolveParent(rec.parentUuid, skippedParents);
216
270
 
217
271
  if (rec.type === "user" && rec.message) {
218
272
  // Skip tool-result-only user messages (they're just tool responses)
package/src/cli.ts CHANGED
@@ -11,8 +11,8 @@ import {
11
11
  optional,
12
12
  positional,
13
13
  } from "cmd-ts";
14
- import { parse } from "./parse.ts";
15
- import { render } from "./render.ts";
14
+ import { parse, parseToTranscripts } from "./parse.ts";
15
+ import { render, renderTranscript } from "./render.ts";
16
16
 
17
17
  // Shared options
18
18
  const inputArg = positional({
@@ -25,7 +25,7 @@ const outputOpt = option({
25
25
  type: optional(string),
26
26
  long: "output",
27
27
  short: "o",
28
- description: "Output path (defaults to current directory)",
28
+ description: "Output path (prints to stdout if not specified)",
29
29
  });
30
30
 
31
31
  const adapterOpt = option({
@@ -51,7 +51,15 @@ const parseCmd = command({
51
51
  adapter: adapterOpt,
52
52
  },
53
53
  async handler({ input, output, adapter }) {
54
- await parse({ input, output, adapter });
54
+ if (output) {
55
+ await parse({ input, output, adapter });
56
+ } else {
57
+ // Print JSONL to stdout (one transcript per line)
58
+ const { transcripts } = await parseToTranscripts({ input, adapter });
59
+ for (const transcript of transcripts) {
60
+ console.log(JSON.stringify(transcript));
61
+ }
62
+ }
55
63
  },
56
64
  });
57
65
 
@@ -80,13 +88,20 @@ const defaultCmd = command({
80
88
  head: headOpt,
81
89
  },
82
90
  async handler({ input, output, adapter, head }) {
83
- // Parse to JSON - parse() determines output paths and returns them
84
- const { outputPaths } = await parse({ input, output, adapter });
85
-
86
- // Render each transcript (JSON path markdown path)
87
- for (const jsonPath of outputPaths) {
88
- const mdPath = jsonPath.replace(/\.json$/, ".md");
89
- await render({ input: jsonPath, output: mdPath, head });
91
+ if (output) {
92
+ // Write intermediate JSON and markdown files
93
+ const { outputPaths } = await parse({ input, output, adapter });
94
+ for (const jsonPath of outputPaths) {
95
+ const mdPath = jsonPath.replace(/\.json$/, ".md");
96
+ await render({ input: jsonPath, output: mdPath, head });
97
+ }
98
+ } else {
99
+ // Stream to stdout - no intermediate files
100
+ const { transcripts } = await parseToTranscripts({ input, adapter });
101
+ for (let i = 0; i < transcripts.length; i++) {
102
+ if (i > 0) console.log(); // blank line between transcripts
103
+ console.log(renderTranscript(transcripts[i], head));
104
+ }
90
105
  }
91
106
  },
92
107
  });
package/src/parse.ts CHANGED
@@ -80,13 +80,19 @@ function getOutputPaths(
80
80
 
81
81
  export interface ParseResult {
82
82
  transcripts: Transcript[];
83
+ inputPath: string;
84
+ }
85
+
86
+ export interface ParseAndWriteResult extends ParseResult {
83
87
  outputPaths: string[];
84
88
  }
85
89
 
86
90
  /**
87
- * Parse source file(s) to intermediate JSON.
91
+ * Parse source file(s) to transcripts (no file I/O beyond reading input).
88
92
  */
89
- export async function parse(options: ParseOptions): Promise<ParseResult> {
93
+ export async function parseToTranscripts(
94
+ options: ParseOptions,
95
+ ): Promise<ParseResult> {
90
96
  const { content, path: inputPath } = await readInput(options.input);
91
97
 
92
98
  // Determine adapter
@@ -108,8 +114,17 @@ export async function parse(options: ParseOptions): Promise<ParseResult> {
108
114
  );
109
115
  }
110
116
 
111
- // Parse
112
117
  const transcripts = adapter.parse(content, inputPath);
118
+ return { transcripts, inputPath };
119
+ }
120
+
121
+ /**
122
+ * Parse source file(s) to intermediate JSON and write to files.
123
+ */
124
+ export async function parse(
125
+ options: ParseOptions,
126
+ ): Promise<ParseAndWriteResult> {
127
+ const { transcripts, inputPath } = await parseToTranscripts(options);
113
128
 
114
129
  // Write output files
115
130
  const outputPaths = getOutputPaths(transcripts, inputPath, options.output);
@@ -123,5 +138,5 @@ export async function parse(options: ParseOptions): Promise<ParseResult> {
123
138
  console.error(`Wrote: ${outputPaths[i]}`);
124
139
  }
125
140
 
126
- return { transcripts, outputPaths };
141
+ return { transcripts, inputPath, outputPaths };
127
142
  }
package/src/render.ts CHANGED
@@ -216,7 +216,10 @@ function tracePath(target: string, parents: Map<string, string>): string[] {
216
216
  /**
217
217
  * Render transcript to markdown with branch awareness.
218
218
  */
219
- function renderTranscript(transcript: Transcript, head?: string): string {
219
+ export function renderTranscript(
220
+ transcript: Transcript,
221
+ head?: string,
222
+ ): string {
220
223
  const lines: string[] = [];
221
224
 
222
225
  // Header
@@ -345,10 +348,15 @@ export async function render(options: RenderOptions): Promise<void> {
345
348
  const { transcript, path: inputPath } = await readTranscript(options.input);
346
349
 
347
350
  const markdown = renderTranscript(transcript, options.head);
348
- const outputPath = getOutputPath(inputPath, options.output);
349
351
 
350
- // Ensure directory exists
351
- await mkdir(dirname(outputPath), { recursive: true });
352
- await Bun.write(outputPath, markdown);
353
- console.error(`Wrote: ${outputPath}`);
352
+ if (options.output) {
353
+ const outputPath = getOutputPath(inputPath, options.output);
354
+ // Ensure directory exists
355
+ await mkdir(dirname(outputPath), { recursive: true });
356
+ await Bun.write(outputPath, markdown);
357
+ console.error(`Wrote: ${outputPath}`);
358
+ } else {
359
+ // Default: print to stdout
360
+ console.log(markdown);
361
+ }
354
362
  }
@@ -0,0 +1,6 @@
1
+ {"type": "user", "uuid": "msg-1", "message": {"role": "user", "content": "Read the config file"}, "timestamp": "2024-01-15T10:00:00Z"}
2
+ {"type": "assistant", "uuid": "msg-2", "parentUuid": "msg-1", "message": {"role": "assistant", "content": [{"type": "text", "text": "I'll read the config file for you."}, {"type": "tool_use", "id": "tool-1", "name": "Read", "input": {"file_path": "/project/config.json"}}]}, "timestamp": "2024-01-15T10:00:02Z"}
3
+ {"type": "user", "uuid": "msg-3", "parentUuid": "msg-2", "message": {"role": "user", "content": [{"type": "tool_result", "tool_use_id": "tool-1", "content": "config contents here"}]}, "timestamp": "2024-01-15T10:00:03Z"}
4
+ {"type": "assistant", "uuid": "msg-4", "parentUuid": "msg-3", "message": {"role": "assistant", "content": [{"type": "text", "text": "The config file contains your settings."}]}, "timestamp": "2024-01-15T10:00:05Z"}
5
+ {"type": "user", "uuid": "msg-5", "parentUuid": "msg-4", "message": {"role": "user", "content": "Thanks!"}, "timestamp": "2024-01-15T10:00:10Z"}
6
+ {"type": "assistant", "uuid": "msg-6", "parentUuid": "msg-5", "message": {"role": "assistant", "content": [{"type": "text", "text": "You're welcome!"}]}, "timestamp": "2024-01-15T10:00:12Z"}
@@ -0,0 +1,28 @@
1
+ # Transcript
2
+
3
+ **Source**: `test/fixtures/claude/skipped-message-chain.input.jsonl`
4
+ **Adapter**: claude-code
5
+
6
+ ---
7
+
8
+ ## User
9
+
10
+ Read the config file
11
+
12
+ ## Assistant
13
+
14
+ I'll read the config file for you.
15
+
16
+ **Tool**: Read `/project/config.json`
17
+
18
+ ## Assistant
19
+
20
+ The config file contains your settings.
21
+
22
+ ## User
23
+
24
+ Thanks!
25
+
26
+ ## Assistant
27
+
28
+ You're welcome!