@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.
- package/package.json +1 -1
- package/src/adapters/claude-code.ts +1 -0
- package/src/adapters/index.ts +4 -0
- package/src/adapters/pi-coding-agent.ts +633 -0
- package/src/render-html.ts +12 -10
- package/src/render.ts +6 -6
- package/src/serve.ts +206 -49
- package/src/types.ts +1 -0
- package/src/utils/summary.ts +3 -3
- package/test/adapters.test.ts +35 -1
- package/test/fixtures/claude/multiple-tools.output.md +3 -3
- package/test/fixtures/claude/skipped-message-chain.output.md +1 -1
- package/test/fixtures/claude/with-tools.output.md +1 -1
- package/test/fixtures/pi-coding-agent/basic-conversation.input.jsonl +5 -0
- package/test/fixtures/pi-coding-agent/basic-conversation.output.md +28 -0
- package/test/fixtures/pi-coding-agent/branching.input.jsonl +7 -0
- package/test/fixtures/pi-coding-agent/branching.output.md +25 -0
- package/test/fixtures/pi-coding-agent/with-compaction.input.jsonl +7 -0
- package/test/fixtures/pi-coding-agent/with-compaction.output.md +28 -0
- package/test/fixtures/pi-coding-agent/with-thinking.input.jsonl +3 -0
- package/test/fixtures/pi-coding-agent/with-thinking.output.md +21 -0
- package/test/fixtures/pi-coding-agent/with-tools.input.jsonl +5 -0
- package/test/fixtures/pi-coding-agent/with-tools.output.md +20 -0
- package/test/serve.test.ts +168 -0
- package/test/snapshots.test.ts +61 -37
- package/test/summary.test.ts +6 -0
|
@@ -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
|
+
});
|
package/test/snapshots.test.ts
CHANGED
|
@@ -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
|
|
7
|
+
const fixturesRoot = join(dirname(import.meta.path), "fixtures");
|
|
8
8
|
const binPath = join(dirname(import.meta.path), "../bin/agent-transcripts");
|
|
9
9
|
|
|
10
|
-
|
|
11
|
-
const
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
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
|
-
|
|
19
|
-
|
|
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
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
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
|
-
|
|
27
|
+
describe(`snapshot tests (${adapterName})`, () => {
|
|
28
|
+
for (const inputFile of inputFiles) {
|
|
29
|
+
const name = inputFile.replace(".input.jsonl", "");
|
|
29
30
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
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
|
-
|
|
36
|
+
const expectedOutput = await Bun.file(expectedPath).text();
|
|
36
37
|
|
|
37
|
-
|
|
38
|
-
|
|
38
|
+
const adapter = getAdapter(adapterName)!;
|
|
39
|
+
const content = await Bun.file(relativeInputPath).text();
|
|
40
|
+
const transcripts = adapter.parse(content, relativeInputPath);
|
|
39
41
|
|
|
40
|
-
|
|
41
|
-
});
|
|
42
|
-
}
|
|
43
|
-
});
|
|
42
|
+
expect(transcripts.length).toBeGreaterThan(0);
|
|
44
43
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
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
|
-
|
|
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
|
-
|
|
55
|
-
|
|
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
|
});
|
package/test/summary.test.ts
CHANGED
|
@@ -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", () => {
|