@tracemarketplace/shared 0.0.6 → 0.0.8
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/dist/chunker.d.ts.map +1 -1
- package/dist/chunker.js +14 -2
- package/dist/chunker.js.map +1 -1
- package/dist/extractor-claude-code.test.d.ts +2 -0
- package/dist/extractor-claude-code.test.d.ts.map +1 -0
- package/dist/extractor-claude-code.test.js +227 -0
- package/dist/extractor-claude-code.test.js.map +1 -0
- package/dist/extractors/claude-code.d.ts.map +1 -1
- package/dist/extractors/claude-code.js +161 -68
- package/dist/extractors/claude-code.js.map +1 -1
- package/dist/utils.d.ts +1 -1
- package/dist/utils.d.ts.map +1 -1
- package/dist/utils.js +4 -0
- package/dist/utils.js.map +1 -1
- package/package.json +1 -1
- package/src/chunker.ts +17 -2
- package/src/extractor-claude-code.test.ts +257 -0
- package/src/extractors/claude-code.ts +168 -64
- package/src/utils.ts +3 -1
package/dist/chunker.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"chunker.d.ts","sourceRoot":"","sources":["../src/chunker.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAQ,MAAM,YAAY,CAAC;AAuDxD;;;;GAIG;AACH,wBAAgB,UAAU,CACxB,KAAK,EAAE,eAAe,EACtB,eAAe,SAAoB,GAClC,eAAe,EAAE,
|
|
1
|
+
{"version":3,"file":"chunker.d.ts","sourceRoot":"","sources":["../src/chunker.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAQ,MAAM,YAAY,CAAC;AAuDxD;;;;GAIG;AACH,wBAAgB,UAAU,CACxB,KAAK,EAAE,eAAe,EACtB,eAAe,SAAoB,GAClC,eAAe,EAAE,CAuDnB"}
|
package/dist/chunker.js
CHANGED
|
@@ -39,13 +39,25 @@ export function chunkTrace(trace, maxOutputTokens = MAX_OUTPUT_TOKENS) {
|
|
|
39
39
|
if (totalOutput <= maxOutputTokens) {
|
|
40
40
|
return [{ ...trace, chunk_index: 0, chunk_start_turn: 0 }];
|
|
41
41
|
}
|
|
42
|
+
// If per-turn usage isn't populated, distribute total_output_tokens uniformly
|
|
43
|
+
// across assistant turns so the chunk boundary logic still fires correctly.
|
|
44
|
+
const perTurnSum = sumOutputTokens(trace.turns);
|
|
45
|
+
const turns = perTurnSum > 0
|
|
46
|
+
? trace.turns
|
|
47
|
+
: (() => {
|
|
48
|
+
const assistantTurns = trace.turns.filter(t => t.role === "assistant").length || 1;
|
|
49
|
+
const tokensPerAssistantTurn = Math.ceil(totalOutput / assistantTurns);
|
|
50
|
+
return trace.turns.map(t => t.role === "assistant" && (t.usage?.output_tokens ?? 0) === 0
|
|
51
|
+
? { ...t, usage: { input_tokens: t.usage?.input_tokens ?? 0, output_tokens: tokensPerAssistantTurn, cache_read_input_tokens: t.usage?.cache_read_input_tokens ?? null, cache_creation_input_tokens: t.usage?.cache_creation_input_tokens ?? null, reasoning_tokens: t.usage?.reasoning_tokens ?? null } }
|
|
52
|
+
: t);
|
|
53
|
+
})();
|
|
42
54
|
const chunks = [];
|
|
43
55
|
let chunkStartTurn = 0;
|
|
44
56
|
let chunkOutputTokens = 0;
|
|
45
57
|
let chunkTurns = [];
|
|
46
58
|
let chunkIndex = 0;
|
|
47
|
-
for (let i = 0; i <
|
|
48
|
-
const turn =
|
|
59
|
+
for (let i = 0; i < turns.length; i++) {
|
|
60
|
+
const turn = turns[i];
|
|
49
61
|
// Break at user-turn boundary once we've accumulated enough
|
|
50
62
|
if (turn.role === "user" &&
|
|
51
63
|
chunkOutputTokens >= maxOutputTokens &&
|
package/dist/chunker.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"chunker.js","sourceRoot":"","sources":["../src/chunker.ts"],"names":[],"mappings":"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
|
|
1
|
+
{"version":3,"file":"chunker.js","sourceRoot":"","sources":["../src/chunker.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"extractor-claude-code.test.d.ts","sourceRoot":"","sources":["../src/extractor-claude-code.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
import { describe, it, expect, beforeAll } from "vitest";
|
|
2
|
+
import { writeFileSync, mkdirSync } from "fs";
|
|
3
|
+
import { tmpdir } from "os";
|
|
4
|
+
import { join } from "path";
|
|
5
|
+
import { extractClaudeCode } from "./extractors/claude-code.js";
|
|
6
|
+
// Build a JSONL fixture on disk once, then test against it.
|
|
7
|
+
// The fixture exercises the key bugs that were fixed:
|
|
8
|
+
// 1. Chained assistant entries (thinking+tool_use) must be merged into one turn
|
|
9
|
+
// 2. Cache tokens (cache_creation + cache_read) must be included in total_input_tokens
|
|
10
|
+
// 3. Output tokens must be summed across all chain entries, not just the root
|
|
11
|
+
const FIXTURE_PATH = join(tmpdir(), "tracemp-extractor-test.jsonl");
|
|
12
|
+
const FIXTURE_LINES = [
|
|
13
|
+
// Skipped: file-history-snapshot
|
|
14
|
+
{ type: "file-history-snapshot", messageId: "snap1", snapshot: {} },
|
|
15
|
+
// Turn 0 — user initial message (string content)
|
|
16
|
+
{
|
|
17
|
+
type: "user",
|
|
18
|
+
uuid: "u1",
|
|
19
|
+
parentUuid: null,
|
|
20
|
+
timestamp: "2024-01-01T00:00:00.000Z",
|
|
21
|
+
gitBranch: "main",
|
|
22
|
+
cwd: "/home/user/project",
|
|
23
|
+
message: { role: "user", content: "Please help me with a task" },
|
|
24
|
+
},
|
|
25
|
+
// Turn 1 — assistant: thinking (root) → tool_use (child), must MERGE
|
|
26
|
+
{
|
|
27
|
+
type: "assistant",
|
|
28
|
+
uuid: "a1",
|
|
29
|
+
parentUuid: "u1",
|
|
30
|
+
timestamp: "2024-01-01T00:00:01.000Z",
|
|
31
|
+
message: {
|
|
32
|
+
role: "assistant",
|
|
33
|
+
model: "claude-sonnet-4-5",
|
|
34
|
+
content: [{ type: "thinking", thinking: "Let me think..." }],
|
|
35
|
+
usage: {
|
|
36
|
+
input_tokens: 5,
|
|
37
|
+
cache_creation_input_tokens: 1000,
|
|
38
|
+
cache_read_input_tokens: 500,
|
|
39
|
+
output_tokens: 10,
|
|
40
|
+
},
|
|
41
|
+
},
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
type: "assistant",
|
|
45
|
+
uuid: "a2",
|
|
46
|
+
parentUuid: "a1",
|
|
47
|
+
timestamp: "2024-01-01T00:00:01.000Z",
|
|
48
|
+
message: {
|
|
49
|
+
role: "assistant",
|
|
50
|
+
model: "claude-sonnet-4-5",
|
|
51
|
+
content: [{ type: "tool_use", id: "tu1", name: "Bash", input: { command: "ls" } }],
|
|
52
|
+
usage: {
|
|
53
|
+
input_tokens: 5,
|
|
54
|
+
cache_creation_input_tokens: 1000,
|
|
55
|
+
cache_read_input_tokens: 500,
|
|
56
|
+
output_tokens: 25,
|
|
57
|
+
},
|
|
58
|
+
},
|
|
59
|
+
},
|
|
60
|
+
// Skipped: progress entries
|
|
61
|
+
{ type: "progress", uuid: "p1", parentUuid: "a2" },
|
|
62
|
+
{ type: "progress", uuid: "p2", parentUuid: "p1" },
|
|
63
|
+
// Turn 2 — user tool_result
|
|
64
|
+
{
|
|
65
|
+
type: "user",
|
|
66
|
+
uuid: "u2",
|
|
67
|
+
parentUuid: "a2",
|
|
68
|
+
timestamp: "2024-01-01T00:00:02.000Z",
|
|
69
|
+
message: {
|
|
70
|
+
role: "user",
|
|
71
|
+
content: [
|
|
72
|
+
{
|
|
73
|
+
type: "tool_result",
|
|
74
|
+
tool_use_id: "tu1",
|
|
75
|
+
content: [{ type: "text", text: "file1.ts\nfile2.ts" }],
|
|
76
|
+
is_error: false,
|
|
77
|
+
},
|
|
78
|
+
],
|
|
79
|
+
},
|
|
80
|
+
},
|
|
81
|
+
// Turn 3 — assistant: text (root) → tool_use (child), must MERGE
|
|
82
|
+
{
|
|
83
|
+
type: "assistant",
|
|
84
|
+
uuid: "a3",
|
|
85
|
+
parentUuid: "u2",
|
|
86
|
+
timestamp: "2024-01-01T00:00:03.000Z",
|
|
87
|
+
message: {
|
|
88
|
+
role: "assistant",
|
|
89
|
+
model: "claude-sonnet-4-5",
|
|
90
|
+
content: [{ type: "text", text: "Here is what I found:" }],
|
|
91
|
+
usage: {
|
|
92
|
+
input_tokens: 3,
|
|
93
|
+
cache_creation_input_tokens: 0,
|
|
94
|
+
cache_read_input_tokens: 1500,
|
|
95
|
+
output_tokens: 8,
|
|
96
|
+
},
|
|
97
|
+
},
|
|
98
|
+
},
|
|
99
|
+
{
|
|
100
|
+
type: "assistant",
|
|
101
|
+
uuid: "a4",
|
|
102
|
+
parentUuid: "a3",
|
|
103
|
+
timestamp: "2024-01-01T00:00:03.000Z",
|
|
104
|
+
message: {
|
|
105
|
+
role: "assistant",
|
|
106
|
+
model: "claude-sonnet-4-5",
|
|
107
|
+
content: [{ type: "tool_use", id: "tu2", name: "Read", input: { file_path: "file1.ts" } }],
|
|
108
|
+
usage: {
|
|
109
|
+
input_tokens: 3,
|
|
110
|
+
cache_creation_input_tokens: 0,
|
|
111
|
+
cache_read_input_tokens: 1500,
|
|
112
|
+
output_tokens: 15,
|
|
113
|
+
},
|
|
114
|
+
},
|
|
115
|
+
},
|
|
116
|
+
// Turn 4 — user tool_result
|
|
117
|
+
{
|
|
118
|
+
type: "user",
|
|
119
|
+
uuid: "u3",
|
|
120
|
+
parentUuid: "a4",
|
|
121
|
+
timestamp: "2024-01-01T00:00:04.000Z",
|
|
122
|
+
message: {
|
|
123
|
+
role: "user",
|
|
124
|
+
content: [
|
|
125
|
+
{
|
|
126
|
+
type: "tool_result",
|
|
127
|
+
tool_use_id: "tu2",
|
|
128
|
+
content: [{ type: "text", text: "const x = 1;" }],
|
|
129
|
+
is_error: false,
|
|
130
|
+
},
|
|
131
|
+
],
|
|
132
|
+
},
|
|
133
|
+
},
|
|
134
|
+
// Turn 5 — simple assistant text, no chain
|
|
135
|
+
{
|
|
136
|
+
type: "assistant",
|
|
137
|
+
uuid: "a5",
|
|
138
|
+
parentUuid: "u3",
|
|
139
|
+
timestamp: "2024-01-01T00:00:05.000Z",
|
|
140
|
+
message: {
|
|
141
|
+
role: "assistant",
|
|
142
|
+
model: "claude-sonnet-4-5",
|
|
143
|
+
content: [{ type: "text", text: "Done! The file contains a constant." }],
|
|
144
|
+
usage: {
|
|
145
|
+
input_tokens: 2,
|
|
146
|
+
cache_creation_input_tokens: 0,
|
|
147
|
+
cache_read_input_tokens: 2000,
|
|
148
|
+
output_tokens: 20,
|
|
149
|
+
},
|
|
150
|
+
},
|
|
151
|
+
},
|
|
152
|
+
// Skipped: last-prompt
|
|
153
|
+
{ type: "last-prompt", uuid: "" },
|
|
154
|
+
];
|
|
155
|
+
beforeAll(() => {
|
|
156
|
+
mkdirSync(tmpdir(), { recursive: true });
|
|
157
|
+
writeFileSync(FIXTURE_PATH, FIXTURE_LINES.map((l) => JSON.stringify(l)).join("\n") + "\n");
|
|
158
|
+
});
|
|
159
|
+
describe("extractClaudeCode", () => {
|
|
160
|
+
it("merges chained assistant entries into one turn", async () => {
|
|
161
|
+
const trace = await extractClaudeCode(FIXTURE_PATH, "test@test.com");
|
|
162
|
+
// Should have 6 turns: user, assistant(merged), user, assistant(merged), user, assistant
|
|
163
|
+
expect(trace.turn_count).toBe(6);
|
|
164
|
+
// Roles must strictly alternate
|
|
165
|
+
const roles = trace.turns.map((t) => t.role);
|
|
166
|
+
for (let i = 1; i < roles.length; i++) {
|
|
167
|
+
expect(roles[i]).not.toBe(roles[i - 1]);
|
|
168
|
+
}
|
|
169
|
+
// Turn 1: thinking+tool_use merged from two chained entries
|
|
170
|
+
expect(trace.turns[1].role).toBe("assistant");
|
|
171
|
+
const t1Types = trace.turns[1].content.map((c) => c.type);
|
|
172
|
+
expect(t1Types).toContain("thinking");
|
|
173
|
+
expect(t1Types).toContain("tool_use");
|
|
174
|
+
// Turn 3: text+tool_use merged
|
|
175
|
+
expect(trace.turns[3].role).toBe("assistant");
|
|
176
|
+
const t3Types = trace.turns[3].content.map((c) => c.type);
|
|
177
|
+
expect(t3Types).toContain("text");
|
|
178
|
+
expect(t3Types).toContain("tool_use");
|
|
179
|
+
});
|
|
180
|
+
it("counts tool calls correctly", async () => {
|
|
181
|
+
const trace = await extractClaudeCode(FIXTURE_PATH, "test@test.com");
|
|
182
|
+
expect(trace.tool_call_count).toBe(2);
|
|
183
|
+
});
|
|
184
|
+
it("includes cache tokens in total_input_tokens", async () => {
|
|
185
|
+
const trace = await extractClaudeCode(FIXTURE_PATH, "test@test.com");
|
|
186
|
+
// Turn 1: input=5 + cache_create=1000 + cache_read=500 = 1505 (counted once, not twice)
|
|
187
|
+
// Turn 3: input=3 + cache_create=0 + cache_read=1500 = 1503
|
|
188
|
+
// Turn 5: input=2 + cache_create=0 + cache_read=2000 = 2002
|
|
189
|
+
// Total: 1505 + 1503 + 2002 = 5010
|
|
190
|
+
expect(trace.total_input_tokens).toBe(5010);
|
|
191
|
+
});
|
|
192
|
+
it("does not double-count input tokens from chained entries", async () => {
|
|
193
|
+
const trace = await extractClaudeCode(FIXTURE_PATH, "test@test.com");
|
|
194
|
+
// Turn 1 root (a1) and child (a2) both carry the same input/cache usage (same API call).
|
|
195
|
+
// We must count it only once from the root.
|
|
196
|
+
// If double-counted: 1505 * 2 + 1503 + 2002 = 6010. Correct: 5010.
|
|
197
|
+
expect(trace.total_input_tokens).toBe(5010);
|
|
198
|
+
});
|
|
199
|
+
it("sums output tokens across all chain entries", async () => {
|
|
200
|
+
const trace = await extractClaudeCode(FIXTURE_PATH, "test@test.com");
|
|
201
|
+
// Turn 1: root output=10 (thinking) + child output=25 (tool_use) = 35
|
|
202
|
+
// Turn 3: root output=8 (text) + child output=15 (tool_use) = 23
|
|
203
|
+
// Turn 5: output=20
|
|
204
|
+
// Total: 35 + 23 + 20 = 78
|
|
205
|
+
expect(trace.total_output_tokens).toBe(78);
|
|
206
|
+
// Turn 1's per-turn usage should also reflect the summed output
|
|
207
|
+
expect(trace.turns[1].usage?.output_tokens).toBe(35);
|
|
208
|
+
});
|
|
209
|
+
it("skips progress, file-history-snapshot, and last-prompt entries", async () => {
|
|
210
|
+
const trace = await extractClaudeCode(FIXTURE_PATH, "test@test.com");
|
|
211
|
+
// All entries that should be skipped produce no turns
|
|
212
|
+
expect(trace.turn_count).toBe(6);
|
|
213
|
+
});
|
|
214
|
+
it("captures git branch and session metadata", async () => {
|
|
215
|
+
const trace = await extractClaudeCode(FIXTURE_PATH, "test@test.com");
|
|
216
|
+
expect(trace.git_branch).toBe("main");
|
|
217
|
+
expect(trace.cwd_hash).toBeTruthy();
|
|
218
|
+
expect(trace.source_tool).toBe("claude_code");
|
|
219
|
+
expect(trace.submitted_by).toBe("test@test.com");
|
|
220
|
+
});
|
|
221
|
+
it("sets started_at and ended_at from first and last turn timestamps", async () => {
|
|
222
|
+
const trace = await extractClaudeCode(FIXTURE_PATH, "test@test.com");
|
|
223
|
+
expect(trace.started_at).toBe("2024-01-01T00:00:00.000Z");
|
|
224
|
+
expect(trace.ended_at).toBe("2024-01-01T00:00:05.000Z");
|
|
225
|
+
});
|
|
226
|
+
});
|
|
227
|
+
//# sourceMappingURL=extractor-claude-code.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"extractor-claude-code.test.js","sourceRoot":"","sources":["../src/extractor-claude-code.test.ts"],"names":[],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"claude-code.d.ts","sourceRoot":"","sources":["../../src/extractors/claude-code.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EACV,eAAe,EAIhB,MAAM,aAAa,CAAC;AAErB,wBAAsB,iBAAiB,CACrC,eAAe,EAAE,MAAM,EACvB,WAAW,SAAY,GACtB,OAAO,CAAC,eAAe,CAAC,
|
|
1
|
+
{"version":3,"file":"claude-code.d.ts","sourceRoot":"","sources":["../../src/extractors/claude-code.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EACV,eAAe,EAIhB,MAAM,aAAa,CAAC;AAErB,wBAAsB,iBAAiB,CACrC,eAAe,EAAE,MAAM,EACvB,WAAW,SAAY,GACtB,OAAO,CAAC,eAAe,CAAC,CA2Q1B"}
|
|
@@ -17,89 +17,182 @@ export async function extractClaudeCode(sessionFilePath, submittedBy = "unknown"
|
|
|
17
17
|
catch { }
|
|
18
18
|
}
|
|
19
19
|
const sessionId = sessionFilePath.split("/").pop()?.replace(".jsonl", "") ?? randomUUID();
|
|
20
|
+
// Build uuid→entry map for parent lookups
|
|
21
|
+
const uuidMap = new Map();
|
|
22
|
+
for (const line of lines) {
|
|
23
|
+
if (line.uuid)
|
|
24
|
+
uuidMap.set(line.uuid, line);
|
|
25
|
+
}
|
|
26
|
+
// Filter to lines that carry message content (skip progress, snapshots, etc.)
|
|
27
|
+
const SKIP_TYPES = new Set([
|
|
28
|
+
"progress",
|
|
29
|
+
"file-history-snapshot",
|
|
30
|
+
"system",
|
|
31
|
+
"last-prompt",
|
|
32
|
+
]);
|
|
33
|
+
const entries = lines.filter((l) => !SKIP_TYPES.has(l.type) && l.message);
|
|
34
|
+
// Claude Code splits a single logical turn into multiple chained JSONL entries:
|
|
35
|
+
//
|
|
36
|
+
// 1. Assistant turns: thinking → tool_use → text each parent the next
|
|
37
|
+
// (assistant→assistant parentage). Merge into one turn.
|
|
38
|
+
//
|
|
39
|
+
// 2. User tool_results for parallel tool calls: each result is a separate entry
|
|
40
|
+
// whose parentUuid points to a different entry in the same assistant chain.
|
|
41
|
+
// Merge into one user turn.
|
|
42
|
+
// For assistant entries: walk up until the parent is NOT an assistant entry.
|
|
43
|
+
function getAssistantRoot(entry) {
|
|
44
|
+
let cur = entry;
|
|
45
|
+
while (cur.message?.role === "assistant") {
|
|
46
|
+
const parent = uuidMap.get(cur.parentUuid);
|
|
47
|
+
if (!parent?.message || parent.message.role !== "assistant")
|
|
48
|
+
break;
|
|
49
|
+
cur = parent;
|
|
50
|
+
}
|
|
51
|
+
return cur;
|
|
52
|
+
}
|
|
53
|
+
// For a user entry: return the uuid of the assistant group root that it responds to
|
|
54
|
+
// (null if its parent is not an assistant).
|
|
55
|
+
function parentAssistantRootUuid(entry) {
|
|
56
|
+
const parent = uuidMap.get(entry.parentUuid);
|
|
57
|
+
if (!parent?.message || parent.message.role !== "assistant")
|
|
58
|
+
return null;
|
|
59
|
+
return getAssistantRoot(parent).uuid ?? null;
|
|
60
|
+
}
|
|
61
|
+
// Group entries in file order.
|
|
62
|
+
// Key: root uuid for assistant groups; entry uuid for user groups.
|
|
63
|
+
const groupMap = new Map();
|
|
64
|
+
const rootOrder = [];
|
|
65
|
+
for (const entry of entries) {
|
|
66
|
+
const role = entry.message?.role;
|
|
67
|
+
if (role === "assistant") {
|
|
68
|
+
const root = getAssistantRoot(entry);
|
|
69
|
+
const rootUuid = root.uuid ?? randomUUID();
|
|
70
|
+
if (!groupMap.has(rootUuid)) {
|
|
71
|
+
groupMap.set(rootUuid, [root]);
|
|
72
|
+
rootOrder.push(rootUuid);
|
|
73
|
+
}
|
|
74
|
+
else if (entry !== root) {
|
|
75
|
+
groupMap.get(rootUuid).push(entry);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
else {
|
|
79
|
+
// User entry: merge with the previous user group if they share the same
|
|
80
|
+
// parent assistant root (i.e., parallel tool results for the same tool call batch).
|
|
81
|
+
const myParentRoot = parentAssistantRootUuid(entry);
|
|
82
|
+
const lastKey = rootOrder[rootOrder.length - 1];
|
|
83
|
+
const lastGroup = lastKey ? groupMap.get(lastKey) : undefined;
|
|
84
|
+
const lastRole = lastGroup?.[0]?.message?.role;
|
|
85
|
+
if (myParentRoot &&
|
|
86
|
+
lastRole === "user" &&
|
|
87
|
+
parentAssistantRootUuid(lastGroup[0]) === myParentRoot) {
|
|
88
|
+
lastGroup.push(entry);
|
|
89
|
+
}
|
|
90
|
+
else {
|
|
91
|
+
const groupKey = entry.uuid ?? randomUUID();
|
|
92
|
+
groupMap.set(groupKey, [entry]);
|
|
93
|
+
rootOrder.push(groupKey);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
20
97
|
const turns = [];
|
|
21
98
|
let totalInputTokens = 0;
|
|
22
99
|
let totalOutputTokens = 0;
|
|
23
100
|
let totalCacheReadTokens = 0;
|
|
24
101
|
let gitBranch = null;
|
|
25
102
|
let cwdHash = null;
|
|
26
|
-
for (const
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
cache_creation_input_tokens: usage.cache_creation_input_tokens ?? null,
|
|
36
|
-
reasoning_tokens: null,
|
|
37
|
-
}
|
|
38
|
-
: null;
|
|
39
|
-
if (tokenUsage) {
|
|
40
|
-
totalInputTokens += tokenUsage.input_tokens;
|
|
41
|
-
totalOutputTokens += tokenUsage.output_tokens;
|
|
42
|
-
totalCacheReadTokens += tokenUsage.cache_read_input_tokens ?? 0;
|
|
43
|
-
}
|
|
44
|
-
if (line.gitBranch)
|
|
45
|
-
gitBranch = line.gitBranch;
|
|
46
|
-
if (line.cwd && !cwdHash)
|
|
47
|
-
cwdHash = hashString(line.cwd);
|
|
103
|
+
for (const rootUuid of rootOrder) {
|
|
104
|
+
const group = groupMap.get(rootUuid);
|
|
105
|
+
const root = group[0];
|
|
106
|
+
if (root.gitBranch)
|
|
107
|
+
gitBranch = root.gitBranch;
|
|
108
|
+
if (root.cwd && !cwdHash)
|
|
109
|
+
cwdHash = hashString(root.cwd);
|
|
110
|
+
const role = root.message.role === "assistant" ? "assistant" : "user";
|
|
111
|
+
// Collect content blocks from all entries in the group
|
|
48
112
|
const contentBlocks = [];
|
|
49
|
-
const
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
?
|
|
53
|
-
:
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
113
|
+
for (const entry of group) {
|
|
114
|
+
const raw = entry.message.content;
|
|
115
|
+
const rawContent = Array.isArray(raw)
|
|
116
|
+
? raw
|
|
117
|
+
: typeof raw === "string"
|
|
118
|
+
? [{ type: "text", text: raw }]
|
|
119
|
+
: [];
|
|
120
|
+
for (const block of rawContent) {
|
|
121
|
+
if (!block || !block.type)
|
|
122
|
+
continue;
|
|
123
|
+
if (block.type === "file-history-snapshot")
|
|
124
|
+
continue;
|
|
125
|
+
if (block.type === "text") {
|
|
126
|
+
contentBlocks.push({ type: "text", text: block.text ?? "" });
|
|
127
|
+
}
|
|
128
|
+
else if (block.type === "thinking") {
|
|
129
|
+
contentBlocks.push({
|
|
130
|
+
type: "thinking",
|
|
131
|
+
text: block.thinking ?? block.text ?? "",
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
else if (block.type === "tool_use") {
|
|
135
|
+
contentBlocks.push({
|
|
136
|
+
type: "tool_use",
|
|
137
|
+
tool_call_id: block.id ?? randomUUID(),
|
|
138
|
+
tool_name: block.name ?? "",
|
|
139
|
+
tool_input: block.input ?? {},
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
else if (block.type === "tool_result") {
|
|
143
|
+
const resultContent = Array.isArray(block.content)
|
|
144
|
+
? block.content.map((c) => c.text ?? "").join("\n")
|
|
145
|
+
: block.content ?? null;
|
|
146
|
+
contentBlocks.push({
|
|
147
|
+
type: "tool_result",
|
|
148
|
+
tool_call_id: block.tool_use_id ?? "",
|
|
149
|
+
is_error: block.is_error ?? false,
|
|
150
|
+
result_content: resultContent,
|
|
151
|
+
exit_code: null,
|
|
152
|
+
});
|
|
153
|
+
}
|
|
87
154
|
}
|
|
88
155
|
}
|
|
89
156
|
if (contentBlocks.length === 0)
|
|
90
157
|
continue;
|
|
158
|
+
// Token accounting for assistant turns:
|
|
159
|
+
// - input/cache tokens come from the root entry only (same API call repeated on each chain entry)
|
|
160
|
+
// - output tokens must be summed across all chain entries (each has its own generated content)
|
|
161
|
+
let tokenUsage = null;
|
|
162
|
+
if (role === "assistant") {
|
|
163
|
+
const rootUsage = root.message.usage;
|
|
164
|
+
if (rootUsage) {
|
|
165
|
+
const inputTokens = rootUsage.input_tokens ?? 0;
|
|
166
|
+
const cacheCreate = rootUsage.cache_creation_input_tokens ?? 0;
|
|
167
|
+
const cacheRead = rootUsage.cache_read_input_tokens ?? 0;
|
|
168
|
+
// Sum output tokens across all chain entries
|
|
169
|
+
const outputTokens = group.reduce((sum, entry) => {
|
|
170
|
+
return sum + (entry.message.usage?.output_tokens ?? 0);
|
|
171
|
+
}, 0);
|
|
172
|
+
tokenUsage = {
|
|
173
|
+
input_tokens: inputTokens + cacheCreate + cacheRead,
|
|
174
|
+
output_tokens: outputTokens,
|
|
175
|
+
cache_read_input_tokens: cacheRead,
|
|
176
|
+
cache_creation_input_tokens: cacheCreate,
|
|
177
|
+
reasoning_tokens: null,
|
|
178
|
+
};
|
|
179
|
+
totalInputTokens += inputTokens + cacheCreate + cacheRead;
|
|
180
|
+
totalOutputTokens += outputTokens;
|
|
181
|
+
totalCacheReadTokens += cacheRead;
|
|
182
|
+
}
|
|
183
|
+
}
|
|
91
184
|
turns.push({
|
|
92
|
-
turn_id:
|
|
93
|
-
parent_turn_id:
|
|
94
|
-
role
|
|
95
|
-
timestamp:
|
|
185
|
+
turn_id: root.uuid ?? randomUUID(),
|
|
186
|
+
parent_turn_id: root.parentUuid ?? null,
|
|
187
|
+
role,
|
|
188
|
+
timestamp: root.timestamp ?? null,
|
|
96
189
|
content: contentBlocks,
|
|
97
|
-
model: model ?? null,
|
|
190
|
+
model: root.message.model ?? null,
|
|
98
191
|
usage: tokenUsage,
|
|
99
192
|
source_metadata: {
|
|
100
|
-
uuid:
|
|
101
|
-
parentUuid:
|
|
102
|
-
gitBranch:
|
|
193
|
+
uuid: root.uuid,
|
|
194
|
+
parentUuid: root.parentUuid,
|
|
195
|
+
gitBranch: root.gitBranch,
|
|
103
196
|
},
|
|
104
197
|
});
|
|
105
198
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"claude-code.js","sourceRoot":"","sources":["../../src/extractors/claude-code.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,gBAAgB,EAAE,MAAM,IAAI,CAAC;AACtC,OAAO,EAAE,eAAe,EAAE,MAAM,UAAU,CAAC;AAC3C,OAAO,EAAE,UAAU,EAAE,MAAM,QAAQ,CAAC;AACpC,OAAO,EAAE,UAAU,EAAE,kBAAkB,EAAE,MAAM,YAAY,CAAC;AAQ5D,MAAM,CAAC,KAAK,UAAU,iBAAiB,CACrC,eAAuB,EACvB,WAAW,GAAG,SAAS;IAEvB,MAAM,KAAK,GAAU,EAAE,CAAC;IACxB,MAAM,EAAE,GAAG,eAAe,CAAC;QACzB,KAAK,EAAE,gBAAgB,CAAC,eAAe,CAAC;QACxC,SAAS,EAAE,QAAQ;KACpB,CAAC,CAAC;IACH,IAAI,KAAK,EAAE,MAAM,IAAI,IAAI,EAAE,EAAE,CAAC;QAC5B,IAAI,CAAC,IAAI,CAAC,IAAI,EAAE;YAAE,SAAS;QAC3B,IAAI,CAAC;YACH,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC;QAC/B,CAAC;QAAC,MAAM,CAAC,CAAA,CAAC;IACZ,CAAC;IAED,MAAM,SAAS,GACb,eAAe,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,GAAG,EAAE,EAAE,OAAO,CAAC,QAAQ,EAAE,EAAE,CAAC,IAAI,UAAU,EAAE,CAAC;
|
|
1
|
+
{"version":3,"file":"claude-code.js","sourceRoot":"","sources":["../../src/extractors/claude-code.ts"],"names":[],"mappings":"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"}
|
package/dist/utils.d.ts
CHANGED
package/dist/utils.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"utils.d.ts","sourceRoot":"","sources":["../src/utils.ts"],"names":[],"mappings":"AAAA,wBAAgB,WAAW,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,GAAG,MAAM,CAGxD;AAED,wBAAgB,eAAe,CAAC,EAAE,EAAE,MAAM,GAAG,IAAI,GAAG,MAAM,
|
|
1
|
+
{"version":3,"file":"utils.d.ts","sourceRoot":"","sources":["../src/utils.ts"],"names":[],"mappings":"AAAA,wBAAgB,WAAW,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,GAAG,MAAM,CAGxD;AAED,wBAAgB,eAAe,CAAC,EAAE,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,GAAG,IAAI,GAAG,MAAM,CAKzE"}
|
package/dist/utils.js
CHANGED
|
@@ -6,6 +6,10 @@ export function formatCents(cents) {
|
|
|
6
6
|
export function formatTimestamp(ts) {
|
|
7
7
|
if (!ts)
|
|
8
8
|
return "—";
|
|
9
|
+
if (ts instanceof Date)
|
|
10
|
+
return ts.toLocaleString();
|
|
11
|
+
if (typeof ts === "string")
|
|
12
|
+
return new Date(ts).toLocaleString();
|
|
9
13
|
return new Date(ts * 1000).toLocaleString();
|
|
10
14
|
}
|
|
11
15
|
//# sourceMappingURL=utils.js.map
|
package/dist/utils.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"utils.js","sourceRoot":"","sources":["../src/utils.ts"],"names":[],"mappings":"AAAA,MAAM,UAAU,WAAW,CAAC,KAAoB;IAC9C,IAAI,KAAK,KAAK,IAAI;QAAE,OAAO,GAAG,CAAC;IAC/B,OAAO,IAAI,CAAC,KAAK,GAAG,GAAG,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,EAAE,CAAC;AACxC,CAAC;AAED,MAAM,UAAU,eAAe,CAAC,
|
|
1
|
+
{"version":3,"file":"utils.js","sourceRoot":"","sources":["../src/utils.ts"],"names":[],"mappings":"AAAA,MAAM,UAAU,WAAW,CAAC,KAAoB;IAC9C,IAAI,KAAK,KAAK,IAAI;QAAE,OAAO,GAAG,CAAC;IAC/B,OAAO,IAAI,CAAC,KAAK,GAAG,GAAG,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,EAAE,CAAC;AACxC,CAAC;AAED,MAAM,UAAU,eAAe,CAAC,EAAiC;IAC/D,IAAI,CAAC,EAAE;QAAE,OAAO,GAAG,CAAC;IACpB,IAAI,EAAE,YAAY,IAAI;QAAE,OAAO,EAAE,CAAC,cAAc,EAAE,CAAC;IACnD,IAAI,OAAO,EAAE,KAAK,QAAQ;QAAE,OAAO,IAAI,IAAI,CAAC,EAAE,CAAC,CAAC,cAAc,EAAE,CAAC;IACjE,OAAO,IAAI,IAAI,CAAC,EAAE,GAAG,IAAI,CAAC,CAAC,cAAc,EAAE,CAAC;AAC9C,CAAC"}
|
package/package.json
CHANGED
package/src/chunker.ts
CHANGED
|
@@ -69,14 +69,29 @@ export function chunkTrace(
|
|
|
69
69
|
return [{ ...trace, chunk_index: 0, chunk_start_turn: 0 }];
|
|
70
70
|
}
|
|
71
71
|
|
|
72
|
+
// If per-turn usage isn't populated, distribute total_output_tokens uniformly
|
|
73
|
+
// across assistant turns so the chunk boundary logic still fires correctly.
|
|
74
|
+
const perTurnSum = sumOutputTokens(trace.turns);
|
|
75
|
+
const turns: Turn[] = perTurnSum > 0
|
|
76
|
+
? trace.turns
|
|
77
|
+
: (() => {
|
|
78
|
+
const assistantTurns = trace.turns.filter(t => t.role === "assistant").length || 1;
|
|
79
|
+
const tokensPerAssistantTurn = Math.ceil(totalOutput / assistantTurns);
|
|
80
|
+
return trace.turns.map(t =>
|
|
81
|
+
t.role === "assistant" && (t.usage?.output_tokens ?? 0) === 0
|
|
82
|
+
? { ...t, usage: { input_tokens: t.usage?.input_tokens ?? 0, output_tokens: tokensPerAssistantTurn, cache_read_input_tokens: t.usage?.cache_read_input_tokens ?? null, cache_creation_input_tokens: t.usage?.cache_creation_input_tokens ?? null, reasoning_tokens: t.usage?.reasoning_tokens ?? null } } as Turn
|
|
83
|
+
: t
|
|
84
|
+
);
|
|
85
|
+
})();
|
|
86
|
+
|
|
72
87
|
const chunks: NormalizedTrace[] = [];
|
|
73
88
|
let chunkStartTurn = 0;
|
|
74
89
|
let chunkOutputTokens = 0;
|
|
75
90
|
let chunkTurns: Turn[] = [];
|
|
76
91
|
let chunkIndex = 0;
|
|
77
92
|
|
|
78
|
-
for (let i = 0; i <
|
|
79
|
-
const turn =
|
|
93
|
+
for (let i = 0; i < turns.length; i++) {
|
|
94
|
+
const turn = turns[i];
|
|
80
95
|
|
|
81
96
|
// Break at user-turn boundary once we've accumulated enough
|
|
82
97
|
if (
|
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
import { describe, it, expect, beforeAll } from "vitest";
|
|
2
|
+
import { writeFileSync, mkdirSync } from "fs";
|
|
3
|
+
import { tmpdir } from "os";
|
|
4
|
+
import { join } from "path";
|
|
5
|
+
import { extractClaudeCode } from "./extractors/claude-code.js";
|
|
6
|
+
|
|
7
|
+
// Build a JSONL fixture on disk once, then test against it.
|
|
8
|
+
// The fixture exercises the key bugs that were fixed:
|
|
9
|
+
// 1. Chained assistant entries (thinking+tool_use) must be merged into one turn
|
|
10
|
+
// 2. Cache tokens (cache_creation + cache_read) must be included in total_input_tokens
|
|
11
|
+
// 3. Output tokens must be summed across all chain entries, not just the root
|
|
12
|
+
|
|
13
|
+
const FIXTURE_PATH = join(tmpdir(), "tracemp-extractor-test.jsonl");
|
|
14
|
+
|
|
15
|
+
const FIXTURE_LINES = [
|
|
16
|
+
// Skipped: file-history-snapshot
|
|
17
|
+
{ type: "file-history-snapshot", messageId: "snap1", snapshot: {} },
|
|
18
|
+
|
|
19
|
+
// Turn 0 — user initial message (string content)
|
|
20
|
+
{
|
|
21
|
+
type: "user",
|
|
22
|
+
uuid: "u1",
|
|
23
|
+
parentUuid: null,
|
|
24
|
+
timestamp: "2024-01-01T00:00:00.000Z",
|
|
25
|
+
gitBranch: "main",
|
|
26
|
+
cwd: "/home/user/project",
|
|
27
|
+
message: { role: "user", content: "Please help me with a task" },
|
|
28
|
+
},
|
|
29
|
+
|
|
30
|
+
// Turn 1 — assistant: thinking (root) → tool_use (child), must MERGE
|
|
31
|
+
{
|
|
32
|
+
type: "assistant",
|
|
33
|
+
uuid: "a1",
|
|
34
|
+
parentUuid: "u1",
|
|
35
|
+
timestamp: "2024-01-01T00:00:01.000Z",
|
|
36
|
+
message: {
|
|
37
|
+
role: "assistant",
|
|
38
|
+
model: "claude-sonnet-4-5",
|
|
39
|
+
content: [{ type: "thinking", thinking: "Let me think..." }],
|
|
40
|
+
usage: {
|
|
41
|
+
input_tokens: 5,
|
|
42
|
+
cache_creation_input_tokens: 1000,
|
|
43
|
+
cache_read_input_tokens: 500,
|
|
44
|
+
output_tokens: 10,
|
|
45
|
+
},
|
|
46
|
+
},
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
type: "assistant",
|
|
50
|
+
uuid: "a2",
|
|
51
|
+
parentUuid: "a1",
|
|
52
|
+
timestamp: "2024-01-01T00:00:01.000Z",
|
|
53
|
+
message: {
|
|
54
|
+
role: "assistant",
|
|
55
|
+
model: "claude-sonnet-4-5",
|
|
56
|
+
content: [{ type: "tool_use", id: "tu1", name: "Bash", input: { command: "ls" } }],
|
|
57
|
+
usage: {
|
|
58
|
+
input_tokens: 5,
|
|
59
|
+
cache_creation_input_tokens: 1000,
|
|
60
|
+
cache_read_input_tokens: 500,
|
|
61
|
+
output_tokens: 25,
|
|
62
|
+
},
|
|
63
|
+
},
|
|
64
|
+
},
|
|
65
|
+
|
|
66
|
+
// Skipped: progress entries
|
|
67
|
+
{ type: "progress", uuid: "p1", parentUuid: "a2" },
|
|
68
|
+
{ type: "progress", uuid: "p2", parentUuid: "p1" },
|
|
69
|
+
|
|
70
|
+
// Turn 2 — user tool_result
|
|
71
|
+
{
|
|
72
|
+
type: "user",
|
|
73
|
+
uuid: "u2",
|
|
74
|
+
parentUuid: "a2",
|
|
75
|
+
timestamp: "2024-01-01T00:00:02.000Z",
|
|
76
|
+
message: {
|
|
77
|
+
role: "user",
|
|
78
|
+
content: [
|
|
79
|
+
{
|
|
80
|
+
type: "tool_result",
|
|
81
|
+
tool_use_id: "tu1",
|
|
82
|
+
content: [{ type: "text", text: "file1.ts\nfile2.ts" }],
|
|
83
|
+
is_error: false,
|
|
84
|
+
},
|
|
85
|
+
],
|
|
86
|
+
},
|
|
87
|
+
},
|
|
88
|
+
|
|
89
|
+
// Turn 3 — assistant: text (root) → tool_use (child), must MERGE
|
|
90
|
+
{
|
|
91
|
+
type: "assistant",
|
|
92
|
+
uuid: "a3",
|
|
93
|
+
parentUuid: "u2",
|
|
94
|
+
timestamp: "2024-01-01T00:00:03.000Z",
|
|
95
|
+
message: {
|
|
96
|
+
role: "assistant",
|
|
97
|
+
model: "claude-sonnet-4-5",
|
|
98
|
+
content: [{ type: "text", text: "Here is what I found:" }],
|
|
99
|
+
usage: {
|
|
100
|
+
input_tokens: 3,
|
|
101
|
+
cache_creation_input_tokens: 0,
|
|
102
|
+
cache_read_input_tokens: 1500,
|
|
103
|
+
output_tokens: 8,
|
|
104
|
+
},
|
|
105
|
+
},
|
|
106
|
+
},
|
|
107
|
+
{
|
|
108
|
+
type: "assistant",
|
|
109
|
+
uuid: "a4",
|
|
110
|
+
parentUuid: "a3",
|
|
111
|
+
timestamp: "2024-01-01T00:00:03.000Z",
|
|
112
|
+
message: {
|
|
113
|
+
role: "assistant",
|
|
114
|
+
model: "claude-sonnet-4-5",
|
|
115
|
+
content: [{ type: "tool_use", id: "tu2", name: "Read", input: { file_path: "file1.ts" } }],
|
|
116
|
+
usage: {
|
|
117
|
+
input_tokens: 3,
|
|
118
|
+
cache_creation_input_tokens: 0,
|
|
119
|
+
cache_read_input_tokens: 1500,
|
|
120
|
+
output_tokens: 15,
|
|
121
|
+
},
|
|
122
|
+
},
|
|
123
|
+
},
|
|
124
|
+
|
|
125
|
+
// Turn 4 — user tool_result
|
|
126
|
+
{
|
|
127
|
+
type: "user",
|
|
128
|
+
uuid: "u3",
|
|
129
|
+
parentUuid: "a4",
|
|
130
|
+
timestamp: "2024-01-01T00:00:04.000Z",
|
|
131
|
+
message: {
|
|
132
|
+
role: "user",
|
|
133
|
+
content: [
|
|
134
|
+
{
|
|
135
|
+
type: "tool_result",
|
|
136
|
+
tool_use_id: "tu2",
|
|
137
|
+
content: [{ type: "text", text: "const x = 1;" }],
|
|
138
|
+
is_error: false,
|
|
139
|
+
},
|
|
140
|
+
],
|
|
141
|
+
},
|
|
142
|
+
},
|
|
143
|
+
|
|
144
|
+
// Turn 5 — simple assistant text, no chain
|
|
145
|
+
{
|
|
146
|
+
type: "assistant",
|
|
147
|
+
uuid: "a5",
|
|
148
|
+
parentUuid: "u3",
|
|
149
|
+
timestamp: "2024-01-01T00:00:05.000Z",
|
|
150
|
+
message: {
|
|
151
|
+
role: "assistant",
|
|
152
|
+
model: "claude-sonnet-4-5",
|
|
153
|
+
content: [{ type: "text", text: "Done! The file contains a constant." }],
|
|
154
|
+
usage: {
|
|
155
|
+
input_tokens: 2,
|
|
156
|
+
cache_creation_input_tokens: 0,
|
|
157
|
+
cache_read_input_tokens: 2000,
|
|
158
|
+
output_tokens: 20,
|
|
159
|
+
},
|
|
160
|
+
},
|
|
161
|
+
},
|
|
162
|
+
|
|
163
|
+
// Skipped: last-prompt
|
|
164
|
+
{ type: "last-prompt", uuid: "" },
|
|
165
|
+
];
|
|
166
|
+
|
|
167
|
+
beforeAll(() => {
|
|
168
|
+
mkdirSync(tmpdir(), { recursive: true });
|
|
169
|
+
writeFileSync(
|
|
170
|
+
FIXTURE_PATH,
|
|
171
|
+
FIXTURE_LINES.map((l) => JSON.stringify(l)).join("\n") + "\n"
|
|
172
|
+
);
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
describe("extractClaudeCode", () => {
|
|
176
|
+
it("merges chained assistant entries into one turn", async () => {
|
|
177
|
+
const trace = await extractClaudeCode(FIXTURE_PATH, "test@test.com");
|
|
178
|
+
|
|
179
|
+
// Should have 6 turns: user, assistant(merged), user, assistant(merged), user, assistant
|
|
180
|
+
expect(trace.turn_count).toBe(6);
|
|
181
|
+
|
|
182
|
+
// Roles must strictly alternate
|
|
183
|
+
const roles = trace.turns.map((t) => t.role);
|
|
184
|
+
for (let i = 1; i < roles.length; i++) {
|
|
185
|
+
expect(roles[i]).not.toBe(roles[i - 1]);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// Turn 1: thinking+tool_use merged from two chained entries
|
|
189
|
+
expect(trace.turns[1].role).toBe("assistant");
|
|
190
|
+
const t1Types = trace.turns[1].content.map((c) => c.type);
|
|
191
|
+
expect(t1Types).toContain("thinking");
|
|
192
|
+
expect(t1Types).toContain("tool_use");
|
|
193
|
+
|
|
194
|
+
// Turn 3: text+tool_use merged
|
|
195
|
+
expect(trace.turns[3].role).toBe("assistant");
|
|
196
|
+
const t3Types = trace.turns[3].content.map((c) => c.type);
|
|
197
|
+
expect(t3Types).toContain("text");
|
|
198
|
+
expect(t3Types).toContain("tool_use");
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
it("counts tool calls correctly", async () => {
|
|
202
|
+
const trace = await extractClaudeCode(FIXTURE_PATH, "test@test.com");
|
|
203
|
+
expect(trace.tool_call_count).toBe(2);
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
it("includes cache tokens in total_input_tokens", async () => {
|
|
207
|
+
const trace = await extractClaudeCode(FIXTURE_PATH, "test@test.com");
|
|
208
|
+
|
|
209
|
+
// Turn 1: input=5 + cache_create=1000 + cache_read=500 = 1505 (counted once, not twice)
|
|
210
|
+
// Turn 3: input=3 + cache_create=0 + cache_read=1500 = 1503
|
|
211
|
+
// Turn 5: input=2 + cache_create=0 + cache_read=2000 = 2002
|
|
212
|
+
// Total: 1505 + 1503 + 2002 = 5010
|
|
213
|
+
expect(trace.total_input_tokens).toBe(5010);
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
it("does not double-count input tokens from chained entries", async () => {
|
|
217
|
+
const trace = await extractClaudeCode(FIXTURE_PATH, "test@test.com");
|
|
218
|
+
|
|
219
|
+
// Turn 1 root (a1) and child (a2) both carry the same input/cache usage (same API call).
|
|
220
|
+
// We must count it only once from the root.
|
|
221
|
+
// If double-counted: 1505 * 2 + 1503 + 2002 = 6010. Correct: 5010.
|
|
222
|
+
expect(trace.total_input_tokens).toBe(5010);
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
it("sums output tokens across all chain entries", async () => {
|
|
226
|
+
const trace = await extractClaudeCode(FIXTURE_PATH, "test@test.com");
|
|
227
|
+
|
|
228
|
+
// Turn 1: root output=10 (thinking) + child output=25 (tool_use) = 35
|
|
229
|
+
// Turn 3: root output=8 (text) + child output=15 (tool_use) = 23
|
|
230
|
+
// Turn 5: output=20
|
|
231
|
+
// Total: 35 + 23 + 20 = 78
|
|
232
|
+
expect(trace.total_output_tokens).toBe(78);
|
|
233
|
+
|
|
234
|
+
// Turn 1's per-turn usage should also reflect the summed output
|
|
235
|
+
expect(trace.turns[1].usage?.output_tokens).toBe(35);
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
it("skips progress, file-history-snapshot, and last-prompt entries", async () => {
|
|
239
|
+
const trace = await extractClaudeCode(FIXTURE_PATH, "test@test.com");
|
|
240
|
+
// All entries that should be skipped produce no turns
|
|
241
|
+
expect(trace.turn_count).toBe(6);
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
it("captures git branch and session metadata", async () => {
|
|
245
|
+
const trace = await extractClaudeCode(FIXTURE_PATH, "test@test.com");
|
|
246
|
+
expect(trace.git_branch).toBe("main");
|
|
247
|
+
expect(trace.cwd_hash).toBeTruthy();
|
|
248
|
+
expect(trace.source_tool).toBe("claude_code");
|
|
249
|
+
expect(trace.submitted_by).toBe("test@test.com");
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
it("sets started_at and ended_at from first and last turn timestamps", async () => {
|
|
253
|
+
const trace = await extractClaudeCode(FIXTURE_PATH, "test@test.com");
|
|
254
|
+
expect(trace.started_at).toBe("2024-01-01T00:00:00.000Z");
|
|
255
|
+
expect(trace.ended_at).toBe("2024-01-01T00:00:05.000Z");
|
|
256
|
+
});
|
|
257
|
+
});
|
|
@@ -27,89 +27,193 @@ export async function extractClaudeCode(
|
|
|
27
27
|
|
|
28
28
|
const sessionId =
|
|
29
29
|
sessionFilePath.split("/").pop()?.replace(".jsonl", "") ?? randomUUID();
|
|
30
|
-
const turns: Turn[] = [];
|
|
31
30
|
|
|
31
|
+
// Build uuid→entry map for parent lookups
|
|
32
|
+
const uuidMap = new Map<string, any>();
|
|
33
|
+
for (const line of lines) {
|
|
34
|
+
if (line.uuid) uuidMap.set(line.uuid, line);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Filter to lines that carry message content (skip progress, snapshots, etc.)
|
|
38
|
+
const SKIP_TYPES = new Set([
|
|
39
|
+
"progress",
|
|
40
|
+
"file-history-snapshot",
|
|
41
|
+
"system",
|
|
42
|
+
"last-prompt",
|
|
43
|
+
]);
|
|
44
|
+
const entries = lines.filter(
|
|
45
|
+
(l) => !SKIP_TYPES.has(l.type) && l.message
|
|
46
|
+
);
|
|
47
|
+
|
|
48
|
+
// Claude Code splits a single logical turn into multiple chained JSONL entries:
|
|
49
|
+
//
|
|
50
|
+
// 1. Assistant turns: thinking → tool_use → text each parent the next
|
|
51
|
+
// (assistant→assistant parentage). Merge into one turn.
|
|
52
|
+
//
|
|
53
|
+
// 2. User tool_results for parallel tool calls: each result is a separate entry
|
|
54
|
+
// whose parentUuid points to a different entry in the same assistant chain.
|
|
55
|
+
// Merge into one user turn.
|
|
56
|
+
|
|
57
|
+
// For assistant entries: walk up until the parent is NOT an assistant entry.
|
|
58
|
+
function getAssistantRoot(entry: any): any {
|
|
59
|
+
let cur = entry;
|
|
60
|
+
while (cur.message?.role === "assistant") {
|
|
61
|
+
const parent = uuidMap.get(cur.parentUuid);
|
|
62
|
+
if (!parent?.message || parent.message.role !== "assistant") break;
|
|
63
|
+
cur = parent;
|
|
64
|
+
}
|
|
65
|
+
return cur;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// For a user entry: return the uuid of the assistant group root that it responds to
|
|
69
|
+
// (null if its parent is not an assistant).
|
|
70
|
+
function parentAssistantRootUuid(entry: any): string | null {
|
|
71
|
+
const parent = uuidMap.get(entry.parentUuid);
|
|
72
|
+
if (!parent?.message || parent.message.role !== "assistant") return null;
|
|
73
|
+
return getAssistantRoot(parent).uuid ?? null;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Group entries in file order.
|
|
77
|
+
// Key: root uuid for assistant groups; entry uuid for user groups.
|
|
78
|
+
const groupMap = new Map<string, any[]>();
|
|
79
|
+
const rootOrder: string[] = [];
|
|
80
|
+
|
|
81
|
+
for (const entry of entries) {
|
|
82
|
+
const role = entry.message?.role;
|
|
83
|
+
|
|
84
|
+
if (role === "assistant") {
|
|
85
|
+
const root = getAssistantRoot(entry);
|
|
86
|
+
const rootUuid = root.uuid ?? randomUUID();
|
|
87
|
+
if (!groupMap.has(rootUuid)) {
|
|
88
|
+
groupMap.set(rootUuid, [root]);
|
|
89
|
+
rootOrder.push(rootUuid);
|
|
90
|
+
} else if (entry !== root) {
|
|
91
|
+
groupMap.get(rootUuid)!.push(entry);
|
|
92
|
+
}
|
|
93
|
+
} else {
|
|
94
|
+
// User entry: merge with the previous user group if they share the same
|
|
95
|
+
// parent assistant root (i.e., parallel tool results for the same tool call batch).
|
|
96
|
+
const myParentRoot = parentAssistantRootUuid(entry);
|
|
97
|
+
const lastKey = rootOrder[rootOrder.length - 1];
|
|
98
|
+
const lastGroup = lastKey ? groupMap.get(lastKey) : undefined;
|
|
99
|
+
const lastRole = lastGroup?.[0]?.message?.role;
|
|
100
|
+
|
|
101
|
+
if (
|
|
102
|
+
myParentRoot &&
|
|
103
|
+
lastRole === "user" &&
|
|
104
|
+
parentAssistantRootUuid(lastGroup![0]) === myParentRoot
|
|
105
|
+
) {
|
|
106
|
+
lastGroup!.push(entry);
|
|
107
|
+
} else {
|
|
108
|
+
const groupKey = entry.uuid ?? randomUUID();
|
|
109
|
+
groupMap.set(groupKey, [entry]);
|
|
110
|
+
rootOrder.push(groupKey);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const turns: Turn[] = [];
|
|
32
116
|
let totalInputTokens = 0;
|
|
33
117
|
let totalOutputTokens = 0;
|
|
34
118
|
let totalCacheReadTokens = 0;
|
|
35
119
|
let gitBranch: string | null = null;
|
|
36
120
|
let cwdHash: string | null = null;
|
|
37
121
|
|
|
38
|
-
for (const
|
|
39
|
-
|
|
40
|
-
const
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
input_tokens: usage.input_tokens ?? 0,
|
|
45
|
-
output_tokens: usage.output_tokens ?? 0,
|
|
46
|
-
cache_read_input_tokens: usage.cache_read_input_tokens ?? null,
|
|
47
|
-
cache_creation_input_tokens:
|
|
48
|
-
usage.cache_creation_input_tokens ?? null,
|
|
49
|
-
reasoning_tokens: null,
|
|
50
|
-
}
|
|
51
|
-
: null;
|
|
52
|
-
if (tokenUsage) {
|
|
53
|
-
totalInputTokens += tokenUsage.input_tokens;
|
|
54
|
-
totalOutputTokens += tokenUsage.output_tokens;
|
|
55
|
-
totalCacheReadTokens += tokenUsage.cache_read_input_tokens ?? 0;
|
|
56
|
-
}
|
|
122
|
+
for (const rootUuid of rootOrder) {
|
|
123
|
+
const group = groupMap.get(rootUuid)!;
|
|
124
|
+
const root = group[0];
|
|
125
|
+
|
|
126
|
+
if (root.gitBranch) gitBranch = root.gitBranch;
|
|
127
|
+
if (root.cwd && !cwdHash) cwdHash = hashString(root.cwd);
|
|
57
128
|
|
|
58
|
-
|
|
59
|
-
|
|
129
|
+
const role: "user" | "assistant" =
|
|
130
|
+
root.message.role === "assistant" ? "assistant" : "user";
|
|
60
131
|
|
|
132
|
+
// Collect content blocks from all entries in the group
|
|
61
133
|
const contentBlocks: ContentBlock[] = [];
|
|
62
|
-
const
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
?
|
|
66
|
-
:
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
134
|
+
for (const entry of group) {
|
|
135
|
+
const raw = entry.message.content;
|
|
136
|
+
const rawContent = Array.isArray(raw)
|
|
137
|
+
? raw
|
|
138
|
+
: typeof raw === "string"
|
|
139
|
+
? [{ type: "text", text: raw }]
|
|
140
|
+
: [];
|
|
141
|
+
|
|
142
|
+
for (const block of rawContent) {
|
|
143
|
+
if (!block || !block.type) continue;
|
|
144
|
+
if (block.type === "file-history-snapshot") continue;
|
|
145
|
+
if (block.type === "text") {
|
|
146
|
+
contentBlocks.push({ type: "text", text: block.text ?? "" });
|
|
147
|
+
} else if (block.type === "thinking") {
|
|
148
|
+
contentBlocks.push({
|
|
149
|
+
type: "thinking",
|
|
150
|
+
text: block.thinking ?? block.text ?? "",
|
|
151
|
+
});
|
|
152
|
+
} else if (block.type === "tool_use") {
|
|
153
|
+
contentBlocks.push({
|
|
154
|
+
type: "tool_use",
|
|
155
|
+
tool_call_id: block.id ?? randomUUID(),
|
|
156
|
+
tool_name: block.name ?? "",
|
|
157
|
+
tool_input: block.input ?? {},
|
|
158
|
+
});
|
|
159
|
+
} else if (block.type === "tool_result") {
|
|
160
|
+
const resultContent = Array.isArray(block.content)
|
|
161
|
+
? block.content.map((c: any) => c.text ?? "").join("\n")
|
|
162
|
+
: block.content ?? null;
|
|
163
|
+
contentBlocks.push({
|
|
164
|
+
type: "tool_result",
|
|
165
|
+
tool_call_id: block.tool_use_id ?? "",
|
|
166
|
+
is_error: block.is_error ?? false,
|
|
167
|
+
result_content: resultContent,
|
|
168
|
+
exit_code: null,
|
|
169
|
+
});
|
|
170
|
+
}
|
|
96
171
|
}
|
|
97
172
|
}
|
|
98
173
|
|
|
99
174
|
if (contentBlocks.length === 0) continue;
|
|
100
175
|
|
|
176
|
+
// Token accounting for assistant turns:
|
|
177
|
+
// - input/cache tokens come from the root entry only (same API call repeated on each chain entry)
|
|
178
|
+
// - output tokens must be summed across all chain entries (each has its own generated content)
|
|
179
|
+
let tokenUsage: TokenUsage | null = null;
|
|
180
|
+
if (role === "assistant") {
|
|
181
|
+
const rootUsage = root.message.usage;
|
|
182
|
+
if (rootUsage) {
|
|
183
|
+
const inputTokens = rootUsage.input_tokens ?? 0;
|
|
184
|
+
const cacheCreate = rootUsage.cache_creation_input_tokens ?? 0;
|
|
185
|
+
const cacheRead = rootUsage.cache_read_input_tokens ?? 0;
|
|
186
|
+
// Sum output tokens across all chain entries
|
|
187
|
+
const outputTokens = group.reduce((sum: number, entry: any) => {
|
|
188
|
+
return sum + (entry.message.usage?.output_tokens ?? 0);
|
|
189
|
+
}, 0);
|
|
190
|
+
|
|
191
|
+
tokenUsage = {
|
|
192
|
+
input_tokens: inputTokens + cacheCreate + cacheRead,
|
|
193
|
+
output_tokens: outputTokens,
|
|
194
|
+
cache_read_input_tokens: cacheRead,
|
|
195
|
+
cache_creation_input_tokens: cacheCreate,
|
|
196
|
+
reasoning_tokens: null,
|
|
197
|
+
};
|
|
198
|
+
|
|
199
|
+
totalInputTokens += inputTokens + cacheCreate + cacheRead;
|
|
200
|
+
totalOutputTokens += outputTokens;
|
|
201
|
+
totalCacheReadTokens += cacheRead;
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
101
205
|
turns.push({
|
|
102
|
-
turn_id:
|
|
103
|
-
parent_turn_id:
|
|
104
|
-
role
|
|
105
|
-
timestamp:
|
|
206
|
+
turn_id: root.uuid ?? randomUUID(),
|
|
207
|
+
parent_turn_id: root.parentUuid ?? null,
|
|
208
|
+
role,
|
|
209
|
+
timestamp: root.timestamp ?? null,
|
|
106
210
|
content: contentBlocks,
|
|
107
|
-
model: model ?? null,
|
|
211
|
+
model: root.message.model ?? null,
|
|
108
212
|
usage: tokenUsage,
|
|
109
213
|
source_metadata: {
|
|
110
|
-
uuid:
|
|
111
|
-
parentUuid:
|
|
112
|
-
gitBranch:
|
|
214
|
+
uuid: root.uuid,
|
|
215
|
+
parentUuid: root.parentUuid,
|
|
216
|
+
gitBranch: root.gitBranch,
|
|
113
217
|
},
|
|
114
218
|
});
|
|
115
219
|
}
|
package/src/utils.ts
CHANGED
|
@@ -3,7 +3,9 @@ export function formatCents(cents: number | null): string {
|
|
|
3
3
|
return `$${(cents / 100).toFixed(2)}`;
|
|
4
4
|
}
|
|
5
5
|
|
|
6
|
-
export function formatTimestamp(ts: number | null): string {
|
|
6
|
+
export function formatTimestamp(ts: number | string | Date | null): string {
|
|
7
7
|
if (!ts) return "—";
|
|
8
|
+
if (ts instanceof Date) return ts.toLocaleString();
|
|
9
|
+
if (typeof ts === "string") return new Date(ts).toLocaleString();
|
|
8
10
|
return new Date(ts * 1000).toLocaleString();
|
|
9
11
|
}
|