@arcreflex/agent-transcripts 0.1.15 → 0.1.17

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,21 @@
1
+ # Transcript
2
+
3
+ **Source**: `test/fixtures/pi-coding-agent/with-thinking.input.jsonl`
4
+ **Adapter**: pi-coding-agent
5
+
6
+ ---
7
+
8
+ ## User
9
+
10
+ What's the best sorting algorithm for nearly-sorted data?
11
+
12
+ ## Assistant
13
+
14
+
15
+ <details>
16
+ <summary>Thinking...</summary>
17
+
18
+ The user is asking about sorting algorithms for nearly-sorted data. Insertion sort is O(n) for nearly sorted data, which makes it ideal. Timsort also handles this well since it detects runs.
19
+ </details>
20
+
21
+ For nearly-sorted data, **insertion sort** is excellent — it runs in O(n) time when the data is almost sorted. Python's built-in Timsort also handles this case well since it detects existing sorted runs.
@@ -0,0 +1,5 @@
1
+ {"type":"session","version":3,"id":"def-456","timestamp":"2024-12-03T15:00:00.000Z","cwd":"/home/user/project"}
2
+ {"type":"message","id":"e1000001","parentId":null,"timestamp":"2024-12-03T15:00:01.000Z","message":{"role":"user","content":"Read the package.json file","timestamp":1733238001000}}
3
+ {"type":"message","id":"e2000002","parentId":"e1000001","timestamp":"2024-12-03T15:00:05.000Z","message":{"role":"assistant","content":[{"type":"text","text":"Let me read that file for you."},{"type":"toolCall","id":"call_001","name":"Read","arguments":{"path":"package.json"}}],"provider":"anthropic","model":"claude-sonnet-4-5","api":"messages","stopReason":"toolUse","usage":{"input":100,"output":30,"cacheRead":0,"cacheWrite":0,"totalTokens":130,"cost":{"input":0.001,"output":0.001,"cacheRead":0,"cacheWrite":0,"total":0.002}},"timestamp":1733238005000}}
4
+ {"type":"message","id":"e3000003","parentId":"e2000002","timestamp":"2024-12-03T15:00:06.000Z","message":{"role":"toolResult","toolCallId":"call_001","toolName":"Read","content":[{"type":"text","text":"{\"name\": \"my-project\", \"version\": \"1.0.0\"}"}],"isError":false,"timestamp":1733238006000}}
5
+ {"type":"message","id":"e4000004","parentId":"e3000003","timestamp":"2024-12-03T15:00:10.000Z","message":{"role":"assistant","content":[{"type":"text","text":"The package.json contains a project named \"my-project\" at version 1.0.0."}],"provider":"anthropic","model":"claude-sonnet-4-5","api":"messages","stopReason":"stop","usage":{"input":200,"output":25,"cacheRead":0,"cacheWrite":0,"totalTokens":225,"cost":{"input":0.002,"output":0.001,"cacheRead":0,"cacheWrite":0,"total":0.003}},"timestamp":1733238010000}}
@@ -0,0 +1,20 @@
1
+ # Transcript
2
+
3
+ **Source**: `test/fixtures/pi-coding-agent/with-tools.input.jsonl`
4
+ **Adapter**: pi-coding-agent
5
+
6
+ ---
7
+
8
+ ## User
9
+
10
+ Read the package.json file
11
+
12
+ ## Assistant
13
+
14
+ Let me read that file for you.
15
+
16
+ **Tool**: Read `package.json` <!-- tool:call_001 -->
17
+
18
+ ## Assistant
19
+
20
+ The package.json contains a project named "my-project" at version 1.0.0.
@@ -0,0 +1,168 @@
1
+ import { describe, expect, it, beforeAll, afterAll } from "bun:test";
2
+ import { join } from "path";
3
+ import { mkdtemp, rm } from "fs/promises";
4
+ import { tmpdir } from "os";
5
+ import { archiveSession } from "../src/archive.ts";
6
+ import { claudeCodeAdapter } from "../src/adapters/claude-code.ts";
7
+ import { serve } from "../src/serve.ts";
8
+
9
+ const fixturesDir = join(import.meta.dir, "fixtures/claude");
10
+ const fixture = join(fixturesDir, "basic-conversation.input.jsonl");
11
+
12
+ let archiveDir: string;
13
+ let baseUrl: string;
14
+ let stopServer: () => void;
15
+
16
+ beforeAll(async () => {
17
+ archiveDir = await mkdtemp(join(tmpdir(), "serve-test-"));
18
+
19
+ // Archive a fixture so the server has something to serve
20
+ await archiveSession(
21
+ archiveDir,
22
+ {
23
+ path: fixture,
24
+ relativePath: "basic-conversation.input.jsonl",
25
+ mtime: Date.now(),
26
+ summary: "Test conversation",
27
+ },
28
+ claudeCodeAdapter,
29
+ );
30
+
31
+ // Start the server on an ephemeral port.
32
+ // serve() doesn't return the server directly, so we call the internals.
33
+ // Instead, we'll use a random high port and just call serve().
34
+ const port = 49152 + Math.floor(Math.random() * 10000);
35
+ baseUrl = `http://localhost:${port}`;
36
+
37
+ // serve() installs a SIGINT handler and never resolves. We just need
38
+ // the Bun.serve to be running, so we call it and keep a reference.
39
+ const servePromise = serve({ archiveDir, port, quiet: true });
40
+ stopServer = () => {
41
+ // The server will be cleaned up when the process exits.
42
+ // For test isolation, we rely on Bun.serve stopping when the test ends.
43
+ };
44
+
45
+ // Give the server a moment to start
46
+ await Bun.sleep(100);
47
+ });
48
+
49
+ afterAll(async () => {
50
+ await rm(archiveDir, { recursive: true, force: true });
51
+ });
52
+
53
+ async function get(path: string): Promise<Response> {
54
+ return fetch(`${baseUrl}${path}`);
55
+ }
56
+
57
+ describe("serve", () => {
58
+ // ========================================================================
59
+ // Index routes
60
+ // ========================================================================
61
+
62
+ describe("index", () => {
63
+ it("GET / returns HTML index", async () => {
64
+ const res = await get("/");
65
+ expect(res.status).toBe(200);
66
+ expect(res.headers.get("content-type")).toContain("text/html");
67
+ const body = await res.text();
68
+ expect(body).toContain("Agent Transcripts");
69
+ expect(body).toContain("<!DOCTYPE html>");
70
+ });
71
+
72
+ it("GET /index.html returns HTML index", async () => {
73
+ const res = await get("/index.html");
74
+ expect(res.status).toBe(200);
75
+ expect(res.headers.get("content-type")).toContain("text/html");
76
+ });
77
+
78
+ it("GET /index.md returns markdown index", async () => {
79
+ const res = await get("/index.md");
80
+ expect(res.status).toBe(200);
81
+ expect(res.headers.get("content-type")).toContain("text/markdown");
82
+ const body = await res.text();
83
+ expect(body).toContain("# Agent Transcripts");
84
+ expect(body).toContain(".md)");
85
+ });
86
+
87
+ it("GET /index.json returns JSON index", async () => {
88
+ const res = await get("/index.json");
89
+ expect(res.status).toBe(200);
90
+ expect(res.headers.get("content-type")).toContain("application/json");
91
+ const body = await res.json();
92
+ expect(body.sessions).toBeArray();
93
+ expect(body.sessions.length).toBeGreaterThan(0);
94
+ const session = body.sessions[0];
95
+ expect(session.links.html).toEndWith(".html");
96
+ expect(session.links.md).toEndWith(".md");
97
+ expect(session.links.json).toEndWith(".json");
98
+ });
99
+ });
100
+
101
+ // ========================================================================
102
+ // Session routes
103
+ // ========================================================================
104
+
105
+ describe("session", () => {
106
+ // Discover the baseName from the JSON index
107
+ let baseName: string;
108
+
109
+ beforeAll(async () => {
110
+ const res = await get("/index.json");
111
+ const body = await res.json();
112
+ // Extract baseName from an html link like "2024-01-01-0000-sessionId.html"
113
+ baseName = body.sessions[0].links.html.replace(/\.html$/, "");
114
+ });
115
+
116
+ it("GET /{baseName}.html returns HTML transcript", async () => {
117
+ const res = await get(`/${baseName}.html`);
118
+ expect(res.status).toBe(200);
119
+ expect(res.headers.get("content-type")).toContain("text/html");
120
+ const body = await res.text();
121
+ expect(body).toContain("<!DOCTYPE html>");
122
+ expect(body).toContain("Test conversation");
123
+ });
124
+
125
+ it("GET /{baseName}.md returns markdown transcript", async () => {
126
+ const res = await get(`/${baseName}.md`);
127
+ expect(res.status).toBe(200);
128
+ expect(res.headers.get("content-type")).toContain("text/markdown");
129
+ const body = await res.text();
130
+ expect(body).toContain("# Test conversation");
131
+ expect(body).toContain(`${baseName}.html`);
132
+ expect(body).toContain(`${baseName}.json`);
133
+ expect(body).toContain("## User");
134
+ expect(body).toContain("## Assistant");
135
+ });
136
+
137
+ it("GET /{baseName}.json returns JSON transcript", async () => {
138
+ const res = await get(`/${baseName}.json`);
139
+ expect(res.status).toBe(200);
140
+ expect(res.headers.get("content-type")).toContain("application/json");
141
+ const body = await res.json();
142
+ expect(body.source).toBeDefined();
143
+ expect(body.messages).toBeArray();
144
+ expect(body.messages.length).toBeGreaterThan(0);
145
+ });
146
+ });
147
+
148
+ // ========================================================================
149
+ // 404s
150
+ // ========================================================================
151
+
152
+ describe("not found", () => {
153
+ it("returns 404 for unknown session", async () => {
154
+ const res = await get("/nonexistent.html");
155
+ expect(res.status).toBe(404);
156
+ });
157
+
158
+ it("returns 404 for unknown extension", async () => {
159
+ const res = await get("/index.xml");
160
+ expect(res.status).toBe(404);
161
+ });
162
+
163
+ it("returns 404 for bare path with no extension", async () => {
164
+ const res = await get("/something");
165
+ expect(res.status).toBe(404);
166
+ });
167
+ });
168
+ });
@@ -4,59 +4,60 @@ import { Glob } from "bun";
4
4
  import { getAdapter } from "../src/adapters/index.ts";
5
5
  import { renderTranscript } from "../src/render.ts";
6
6
 
7
- const fixturesDir = join(dirname(import.meta.path), "fixtures/claude");
7
+ const fixturesRoot = join(dirname(import.meta.path), "fixtures");
8
8
  const binPath = join(dirname(import.meta.path), "../bin/agent-transcripts");
9
9
 
10
- // Find all input fixtures
11
- const inputFiles: string[] = [];
12
- const glob = new Glob("*.input.jsonl");
13
- for await (const file of glob.scan(fixturesDir)) {
14
- inputFiles.push(file);
15
- }
16
- inputFiles.sort();
10
+ /** Map fixture directory name → adapter name */
11
+ const fixtureAdapters: Record<string, string> = {
12
+ claude: "claude-code",
13
+ "pi-coding-agent": "pi-coding-agent",
14
+ };
17
15
 
18
- describe("snapshot tests", () => {
19
- for (const inputFile of inputFiles) {
20
- const name = inputFile.replace(".input.jsonl", "");
16
+ for (const [fixtureDir, adapterName] of Object.entries(fixtureAdapters)) {
17
+ const fixturesDir = join(fixturesRoot, fixtureDir);
21
18
 
22
- it(name, async () => {
23
- const inputPath = join(fixturesDir, inputFile);
24
- const expectedPath = inputPath.replace(".input.jsonl", ".output.md");
25
- // Use relative path for consistent Source: field in output
26
- const relativeInputPath = `test/fixtures/claude/${inputFile}`;
19
+ // Find all input fixtures
20
+ const inputFiles: string[] = [];
21
+ const glob = new Glob("*.input.jsonl");
22
+ for await (const file of glob.scan(fixturesDir)) {
23
+ inputFiles.push(file);
24
+ }
25
+ inputFiles.sort();
27
26
 
28
- const expectedOutput = await Bun.file(expectedPath).text();
27
+ describe(`snapshot tests (${adapterName})`, () => {
28
+ for (const inputFile of inputFiles) {
29
+ const name = inputFile.replace(".input.jsonl", "");
29
30
 
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
+ it(name, async () => {
32
+ const inputPath = join(fixturesDir, inputFile);
33
+ const expectedPath = inputPath.replace(".input.jsonl", ".output.md");
34
+ const relativeInputPath = `test/fixtures/${fixtureDir}/${inputFile}`;
34
35
 
35
- expect(transcripts.length).toBeGreaterThan(0);
36
+ const expectedOutput = await Bun.file(expectedPath).text();
36
37
 
37
- // Render the first transcript (our fixtures are single-transcript)
38
- const actualOutput = renderTranscript(transcripts[0]);
38
+ const adapter = getAdapter(adapterName)!;
39
+ const content = await Bun.file(relativeInputPath).text();
40
+ const transcripts = adapter.parse(content, relativeInputPath);
39
41
 
40
- expect(actualOutput.trimEnd()).toBe(expectedOutput.trimEnd());
41
- });
42
- }
43
- });
42
+ expect(transcripts.length).toBeGreaterThan(0);
44
43
 
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");
44
+ const actualOutput = renderTranscript(transcripts[0]);
45
+ expect(actualOutput.trimEnd()).toBe(expectedOutput.trimEnd());
46
+ });
50
47
  }
48
+ });
49
+ }
51
50
 
52
- const relativeInputPath = `test/fixtures/claude/${inputFile}`;
51
+ describe("CLI integration", () => {
52
+ it("convert to stdout works (claude-code)", async () => {
53
+ const relativeInputPath =
54
+ "test/fixtures/claude/basic-conversation.input.jsonl";
53
55
  const expectedPath = join(
54
- fixturesDir,
55
- inputFile.replace(".input.jsonl", ".output.md"),
56
+ fixturesRoot,
57
+ "claude/basic-conversation.output.md",
56
58
  );
57
59
  const expectedOutput = await Bun.file(expectedPath).text();
58
60
 
59
- // Run CLI: convert with stdout output
60
61
  const result = Bun.spawnSync([
61
62
  binPath,
62
63
  "convert",
@@ -70,4 +71,27 @@ describe("CLI integration", () => {
70
71
  const actualOutput = result.stdout.toString();
71
72
  expect(actualOutput.trimEnd()).toBe(expectedOutput.trimEnd());
72
73
  });
74
+
75
+ it("convert to stdout works (pi-coding-agent)", async () => {
76
+ const relativeInputPath =
77
+ "test/fixtures/pi-coding-agent/basic-conversation.input.jsonl";
78
+ const expectedPath = join(
79
+ fixturesRoot,
80
+ "pi-coding-agent/basic-conversation.output.md",
81
+ );
82
+ const expectedOutput = await Bun.file(expectedPath).text();
83
+
84
+ const result = Bun.spawnSync([
85
+ binPath,
86
+ "convert",
87
+ relativeInputPath,
88
+ "--adapter",
89
+ "pi-coding-agent",
90
+ ]);
91
+
92
+ expect(result.exitCode).toBe(0);
93
+
94
+ const actualOutput = result.stdout.toString();
95
+ expect(actualOutput.trimEnd()).toBe(expectedOutput.trimEnd());
96
+ });
73
97
  });
@@ -12,6 +12,12 @@ describe("extractToolSummary", () => {
12
12
  it("returns empty for missing file_path", () => {
13
13
  expect(extractToolSummary("Read", {})).toBe("");
14
14
  });
15
+
16
+ it("returns path (pi-coding-agent style)", () => {
17
+ expect(extractToolSummary("Read", { path: "src/index.ts" })).toBe(
18
+ "src/index.ts",
19
+ );
20
+ });
15
21
  });
16
22
 
17
23
  describe("Write", () => {