@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.
- package/.github/workflows/publish.yml +5 -0
- package/CLAUDE.md +4 -0
- package/README.md +70 -17
- package/bun.lock +89 -0
- package/package.json +3 -2
- package/src/adapters/claude-code.ts +300 -33
- package/src/cache.ts +129 -0
- package/src/cli.ts +95 -68
- package/src/convert.ts +82 -42
- package/src/parse.ts +7 -101
- package/src/render-html.ts +1096 -0
- package/src/render-index.ts +611 -0
- package/src/render.ts +7 -194
- package/src/serve.ts +308 -0
- package/src/sync.ts +211 -98
- package/src/title.ts +172 -0
- package/src/types.ts +18 -2
- package/src/utils/html.ts +12 -0
- package/src/utils/naming.ts +30 -143
- package/src/utils/openrouter.ts +116 -0
- package/src/utils/provenance.ts +167 -69
- 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/test/snapshots.test.ts +39 -33
|
@@ -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
|
package/test/snapshots.test.ts
CHANGED
|
@@ -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
|
-
//
|
|
29
|
-
const
|
|
30
|
-
const
|
|
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
|
-
|
|
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
|
|
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
|
+
});
|