@tracemarketplace/shared 0.0.4 → 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.
@@ -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,CAwCnB"}
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 < trace.turns.length; i++) {
48
- const turn = trace.turns[i];
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 &&
@@ -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,2 @@
1
+ export {};
2
+ //# sourceMappingURL=extractor-claude-code.test.d.ts.map
@@ -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,CAmK1B"}
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 line of lines) {
27
- if (!line.message)
28
- continue;
29
- const { role, content, usage, model } = line.message;
30
- const tokenUsage = usage
31
- ? {
32
- input_tokens: usage.input_tokens ?? 0,
33
- output_tokens: usage.output_tokens ?? 0,
34
- cache_read_input_tokens: usage.cache_read_input_tokens ?? null,
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 rawContent = Array.isArray(content)
50
- ? content
51
- : typeof content === "string"
52
- ? [{ type: "text", text: content }]
53
- : [];
54
- for (const block of rawContent) {
55
- if (!block || !block.type)
56
- continue;
57
- if (block.type === "file-history-snapshot")
58
- continue;
59
- if (block.type === "text") {
60
- contentBlocks.push({ type: "text", text: block.text ?? "" });
61
- }
62
- else if (block.type === "thinking") {
63
- contentBlocks.push({
64
- type: "thinking",
65
- text: block.thinking ?? block.text ?? "",
66
- });
67
- }
68
- else if (block.type === "tool_use") {
69
- contentBlocks.push({
70
- type: "tool_use",
71
- tool_call_id: block.id ?? randomUUID(),
72
- tool_name: block.name ?? "",
73
- tool_input: block.input ?? {},
74
- });
75
- }
76
- else if (block.type === "tool_result") {
77
- const resultContent = Array.isArray(block.content)
78
- ? block.content.map((c) => c.text ?? "").join("\n")
79
- : block.content ?? null;
80
- contentBlocks.push({
81
- type: "tool_result",
82
- tool_call_id: block.tool_use_id ?? "",
83
- is_error: block.is_error ?? false,
84
- result_content: resultContent,
85
- exit_code: null,
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: line.uuid ?? randomUUID(),
93
- parent_turn_id: line.parentUuid ?? null,
94
- role: role === "assistant" ? "assistant" : "user",
95
- timestamp: line.timestamp ?? null,
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: line.uuid,
101
- parentUuid: line.parentUuid,
102
- gitBranch: line.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":"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"}
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
@@ -1,3 +1,3 @@
1
1
  export declare function formatCents(cents: number | null): string;
2
- export declare function formatTimestamp(ts: number | null): string;
2
+ export declare function formatTimestamp(ts: number | string | Date | null): string;
3
3
  //# sourceMappingURL=utils.d.ts.map
@@ -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,CAGzD"}
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,EAAiB;IAC/C,IAAI,CAAC,EAAE;QAAE,OAAO,GAAG,CAAC;IACpB,OAAO,IAAI,IAAI,CAAC,EAAE,GAAG,IAAI,CAAC,CAAC,cAAc,EAAE,CAAC;AAC9C,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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tracemarketplace/shared",
3
- "version": "0.0.4",
3
+ "version": "0.0.8",
4
4
  "type": "module",
5
5
  "main": "./dist/index.js",
6
6
  "types": "./dist/index.d.ts",
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 < trace.turns.length; i++) {
79
- const turn = trace.turns[i];
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 line of lines) {
39
- if (!line.message) continue;
40
- const { role, content, usage, model } = line.message;
41
-
42
- const tokenUsage: TokenUsage | null = usage
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
- if (line.gitBranch) gitBranch = line.gitBranch;
59
- if (line.cwd && !cwdHash) cwdHash = hashString(line.cwd);
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 rawContent = Array.isArray(content)
63
- ? content
64
- : typeof content === "string"
65
- ? [{ type: "text", text: content }]
66
- : [];
67
-
68
- for (const block of rawContent) {
69
- if (!block || !block.type) continue;
70
- if (block.type === "file-history-snapshot") continue;
71
- if (block.type === "text") {
72
- contentBlocks.push({ type: "text", text: block.text ?? "" });
73
- } else if (block.type === "thinking") {
74
- contentBlocks.push({
75
- type: "thinking",
76
- text: block.thinking ?? block.text ?? "",
77
- });
78
- } else if (block.type === "tool_use") {
79
- contentBlocks.push({
80
- type: "tool_use",
81
- tool_call_id: block.id ?? randomUUID(),
82
- tool_name: block.name ?? "",
83
- tool_input: block.input ?? {},
84
- });
85
- } else if (block.type === "tool_result") {
86
- const resultContent = Array.isArray(block.content)
87
- ? block.content.map((c: any) => c.text ?? "").join("\n")
88
- : block.content ?? null;
89
- contentBlocks.push({
90
- type: "tool_result",
91
- tool_call_id: block.tool_use_id ?? "",
92
- is_error: block.is_error ?? false,
93
- result_content: resultContent,
94
- exit_code: null,
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: line.uuid ?? randomUUID(),
103
- parent_turn_id: line.parentUuid ?? null,
104
- role: role === "assistant" ? "assistant" : "user",
105
- timestamp: line.timestamp ?? null,
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: line.uuid,
111
- parentUuid: line.parentUuid,
112
- gitBranch: line.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
  }