@arcreflex/agent-transcripts 0.1.5 → 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.
@@ -0,0 +1,116 @@
1
+ /**
2
+ * Tree/branch navigation utilities for transcript messages.
3
+ */
4
+
5
+ import type { Message } from "../types.ts";
6
+
7
+ export interface MessageTree {
8
+ bySourceRef: Map<string, Message[]>;
9
+ children: Map<string, Set<string>>;
10
+ parents: Map<string, string>;
11
+ roots: string[];
12
+ }
13
+
14
+ /**
15
+ * Build tree structure from messages.
16
+ * Returns maps for navigation and the messages grouped by sourceRef.
17
+ */
18
+ export function buildTree(messages: Message[]): MessageTree {
19
+ const bySourceRef = new Map<string, Message[]>();
20
+ for (const msg of messages) {
21
+ const existing = bySourceRef.get(msg.sourceRef) || [];
22
+ existing.push(msg);
23
+ bySourceRef.set(msg.sourceRef, existing);
24
+ }
25
+
26
+ const children = new Map<string, Set<string>>();
27
+ const parents = new Map<string, string>();
28
+
29
+ for (const msg of messages) {
30
+ if (msg.parentMessageRef && bySourceRef.has(msg.parentMessageRef)) {
31
+ parents.set(msg.sourceRef, msg.parentMessageRef);
32
+ const existing = children.get(msg.parentMessageRef) || new Set();
33
+ existing.add(msg.sourceRef);
34
+ children.set(msg.parentMessageRef, existing);
35
+ }
36
+ }
37
+
38
+ const roots: string[] = [];
39
+ for (const sourceRef of bySourceRef.keys()) {
40
+ if (!parents.has(sourceRef)) {
41
+ roots.push(sourceRef);
42
+ }
43
+ }
44
+
45
+ return { bySourceRef, children, parents, roots };
46
+ }
47
+
48
+ /**
49
+ * Find the latest leaf in the tree (for primary branch).
50
+ */
51
+ export function findLatestLeaf(
52
+ bySourceRef: Map<string, Message[]>,
53
+ children: Map<string, Set<string>>,
54
+ ): string | undefined {
55
+ let latestLeaf: string | undefined;
56
+ let latestTime = 0;
57
+
58
+ for (const sourceRef of bySourceRef.keys()) {
59
+ const childSet = children.get(sourceRef);
60
+ if (!childSet || childSet.size === 0) {
61
+ const msgs = bySourceRef.get(sourceRef);
62
+ if (msgs && msgs.length > 0) {
63
+ const time = new Date(msgs[0].timestamp).getTime();
64
+ if (time > latestTime) {
65
+ latestTime = time;
66
+ latestLeaf = sourceRef;
67
+ }
68
+ }
69
+ }
70
+ }
71
+
72
+ return latestLeaf;
73
+ }
74
+
75
+ /**
76
+ * Trace path from root to target.
77
+ */
78
+ export function tracePath(
79
+ target: string,
80
+ parents: Map<string, string>,
81
+ ): string[] {
82
+ const path: string[] = [];
83
+ let current: string | undefined = target;
84
+
85
+ while (current) {
86
+ path.unshift(current);
87
+ current = parents.get(current);
88
+ }
89
+
90
+ return path;
91
+ }
92
+
93
+ /**
94
+ * Get first line of message content for branch reference display.
95
+ */
96
+ export function getFirstLine(msg: Message): string {
97
+ let text: string;
98
+ switch (msg.type) {
99
+ case "user":
100
+ case "assistant":
101
+ case "system":
102
+ case "error":
103
+ text = msg.content;
104
+ break;
105
+ case "tool_calls":
106
+ text = msg.calls.map((c) => c.name).join(", ");
107
+ break;
108
+ default:
109
+ text = "";
110
+ }
111
+ const firstLine = text.split("\n")[0].trim();
112
+ const maxLen = 60;
113
+ return firstLine.length > maxLen
114
+ ? firstLine.slice(0, maxLen) + "..."
115
+ : firstLine;
116
+ }
@@ -0,0 +1,9 @@
1
+ {"type": "user", "uuid": "msg-1", "message": {"role": "user", "content": "First message"}, "timestamp": "2024-01-15T10:00:00Z"}
2
+ {"type": "assistant", "uuid": "msg-2", "parentUuid": "msg-1", "message": {"role": "assistant", "content": [{"type": "text", "text": "First response"}]}, "timestamp": "2024-01-15T10:00:05Z"}
3
+ {"type": "progress", "uuid": "progress-1", "parentUuid": "msg-2", "content": "Processing...", "timestamp": "2024-01-15T10:00:10Z"}
4
+ {"type": "user", "uuid": "msg-3", "parentUuid": "progress-1", "message": {"role": "user", "content": "Second message (parent is progress record)"}, "timestamp": "2024-01-15T10:00:15Z"}
5
+ {"type": "assistant", "uuid": "msg-4", "parentUuid": "msg-3", "message": {"role": "assistant", "content": [{"type": "text", "text": "Second response"}]}, "timestamp": "2024-01-15T10:00:20Z"}
6
+ {"type": "progress", "uuid": "progress-2", "parentUuid": "msg-4", "content": "More processing...", "timestamp": "2024-01-15T10:00:25Z"}
7
+ {"type": "progress", "uuid": "progress-3", "parentUuid": "progress-2", "content": "Nested progress...", "timestamp": "2024-01-15T10:00:26Z"}
8
+ {"type": "user", "uuid": "msg-5", "parentUuid": "progress-3", "message": {"role": "user", "content": "Third message (parent is nested progress)"}, "timestamp": "2024-01-15T10:00:30Z"}
9
+ {"type": "assistant", "uuid": "msg-6", "parentUuid": "msg-5", "message": {"role": "assistant", "content": [{"type": "text", "text": "Third response"}]}, "timestamp": "2024-01-15T10:00:35Z"}
@@ -0,0 +1,30 @@
1
+ # Transcript
2
+
3
+ **Source**: `test/fixtures/claude/non-message-parents.input.jsonl`
4
+ **Adapter**: claude-code
5
+
6
+ ---
7
+
8
+ ## User
9
+
10
+ First message
11
+
12
+ ## Assistant
13
+
14
+ First response
15
+
16
+ ## User
17
+
18
+ Second message (parent is progress record)
19
+
20
+ ## Assistant
21
+
22
+ Second response
23
+
24
+ ## User
25
+
26
+ Third message (parent is nested progress)
27
+
28
+ ## Assistant
29
+
30
+ Third response
@@ -1,6 +1,8 @@
1
1
  import { describe, expect, it } from "bun:test";
2
2
  import { join, dirname } from "path";
3
3
  import { Glob } from "bun";
4
+ import { getAdapter } from "../src/adapters/index.ts";
5
+ import { renderTranscript } from "../src/render.ts";
4
6
 
5
7
  const fixturesDir = join(dirname(import.meta.path), "fixtures/claude");
6
8
  const binPath = join(dirname(import.meta.path), "../bin/agent-transcripts");
@@ -25,43 +27,47 @@ describe("snapshot tests", () => {
25
27
 
26
28
  const expectedOutput = await Bun.file(expectedPath).text();
27
29
 
28
- // Run the CLI: parse to temp JSON, then render to temp MD
29
- const tempJson = `/tmp/test-${name}-${Date.now()}.json`;
30
- const tempMd = `/tmp/test-${name}-${Date.now()}.md`;
30
+ // Direct function call: parse with adapter, then render
31
+ const adapter = getAdapter("claude-code")!;
32
+ const content = await Bun.file(relativeInputPath).text();
33
+ const transcripts = adapter.parse(content, relativeInputPath);
31
34
 
32
- // Parse
33
- const parseResult = Bun.spawnSync([
34
- binPath,
35
- "parse",
36
- relativeInputPath,
37
- "--adapter",
38
- "claude-code",
39
- "-o",
40
- tempJson,
41
- ]);
42
- expect(parseResult.exitCode).toBe(0);
35
+ expect(transcripts.length).toBeGreaterThan(0);
43
36
 
44
- // Render
45
- const renderResult = Bun.spawnSync([
46
- binPath,
47
- "render",
48
- tempJson,
49
- "-o",
50
- tempMd,
51
- ]);
52
- expect(renderResult.exitCode).toBe(0);
37
+ // Render the first transcript (our fixtures are single-transcript)
38
+ const actualOutput = renderTranscript(transcripts[0]);
53
39
 
54
- // Compare output
55
- const actualOutput = await Bun.file(tempMd).text();
56
40
  expect(actualOutput.trimEnd()).toBe(expectedOutput.trimEnd());
57
-
58
- // Cleanup
59
- await Bun.file(tempJson)
60
- .delete()
61
- .catch(() => {});
62
- await Bun.file(tempMd)
63
- .delete()
64
- .catch(() => {});
65
41
  });
66
42
  }
67
43
  });
44
+
45
+ describe("CLI integration", () => {
46
+ it("convert to stdout works", async () => {
47
+ const inputFile = inputFiles[0];
48
+ if (!inputFile) {
49
+ throw new Error("No input fixtures found");
50
+ }
51
+
52
+ const relativeInputPath = `test/fixtures/claude/${inputFile}`;
53
+ const expectedPath = join(
54
+ fixturesDir,
55
+ inputFile.replace(".input.jsonl", ".output.md"),
56
+ );
57
+ const expectedOutput = await Bun.file(expectedPath).text();
58
+
59
+ // Run CLI: convert with stdout output
60
+ const result = Bun.spawnSync([
61
+ binPath,
62
+ "convert",
63
+ relativeInputPath,
64
+ "--adapter",
65
+ "claude-code",
66
+ ]);
67
+
68
+ expect(result.exitCode).toBe(0);
69
+
70
+ const actualOutput = result.stdout.toString();
71
+ expect(actualOutput.trimEnd()).toBe(expectedOutput.trimEnd());
72
+ });
73
+ });