@tracemarketplace/shared 0.0.6 → 0.0.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (82) hide show
  1. package/dist/chunker.d.ts.map +1 -1
  2. package/dist/chunker.js +14 -2
  3. package/dist/chunker.js.map +1 -1
  4. package/dist/extractor-claude-code.test.d.ts +2 -0
  5. package/dist/extractor-claude-code.test.d.ts.map +1 -0
  6. package/dist/extractor-claude-code.test.js +290 -0
  7. package/dist/extractor-claude-code.test.js.map +1 -0
  8. package/dist/extractor-codex.test.d.ts +2 -0
  9. package/dist/extractor-codex.test.d.ts.map +1 -0
  10. package/dist/extractor-codex.test.js +212 -0
  11. package/dist/extractor-codex.test.js.map +1 -0
  12. package/dist/extractor-cursor.test.d.ts +2 -0
  13. package/dist/extractor-cursor.test.d.ts.map +1 -0
  14. package/dist/extractor-cursor.test.js +120 -0
  15. package/dist/extractor-cursor.test.js.map +1 -0
  16. package/dist/extractors/claude-code.d.ts.map +1 -1
  17. package/dist/extractors/claude-code.js +172 -73
  18. package/dist/extractors/claude-code.js.map +1 -1
  19. package/dist/extractors/codex.d.ts.map +1 -1
  20. package/dist/extractors/codex.js +63 -35
  21. package/dist/extractors/codex.js.map +1 -1
  22. package/dist/extractors/common.d.ts +14 -0
  23. package/dist/extractors/common.d.ts.map +1 -0
  24. package/dist/extractors/common.js +100 -0
  25. package/dist/extractors/common.js.map +1 -0
  26. package/dist/extractors/cursor.d.ts.map +1 -1
  27. package/dist/extractors/cursor.js +205 -45
  28. package/dist/extractors/cursor.js.map +1 -1
  29. package/dist/hash.d.ts.map +1 -1
  30. package/dist/hash.js +35 -2
  31. package/dist/hash.js.map +1 -1
  32. package/dist/hash.test.js +29 -2
  33. package/dist/hash.test.js.map +1 -1
  34. package/dist/index.d.ts +1 -0
  35. package/dist/index.d.ts.map +1 -1
  36. package/dist/index.js +1 -0
  37. package/dist/index.js.map +1 -1
  38. package/dist/redact.d.ts +12 -0
  39. package/dist/redact.d.ts.map +1 -1
  40. package/dist/redact.js +120 -38
  41. package/dist/redact.js.map +1 -1
  42. package/dist/redact.test.d.ts +2 -0
  43. package/dist/redact.test.d.ts.map +1 -0
  44. package/dist/redact.test.js +96 -0
  45. package/dist/redact.test.js.map +1 -0
  46. package/dist/turn-actors.d.ts +3 -0
  47. package/dist/turn-actors.d.ts.map +1 -0
  48. package/dist/turn-actors.js +57 -0
  49. package/dist/turn-actors.js.map +1 -0
  50. package/dist/turn-actors.test.d.ts +2 -0
  51. package/dist/turn-actors.test.d.ts.map +1 -0
  52. package/dist/turn-actors.test.js +65 -0
  53. package/dist/turn-actors.test.js.map +1 -0
  54. package/dist/types.d.ts +5 -0
  55. package/dist/types.d.ts.map +1 -1
  56. package/dist/utils.d.ts +1 -1
  57. package/dist/utils.d.ts.map +1 -1
  58. package/dist/utils.js +4 -0
  59. package/dist/utils.js.map +1 -1
  60. package/dist/validators.d.ts +24 -0
  61. package/dist/validators.d.ts.map +1 -1
  62. package/dist/validators.js +3 -0
  63. package/dist/validators.js.map +1 -1
  64. package/package.json +5 -1
  65. package/src/chunker.ts +17 -2
  66. package/src/extractor-claude-code.test.ts +326 -0
  67. package/src/extractor-codex.test.ts +225 -0
  68. package/src/extractor-cursor.test.ts +141 -0
  69. package/src/extractors/claude-code.ts +180 -69
  70. package/src/extractors/codex.ts +69 -38
  71. package/src/extractors/common.ts +139 -0
  72. package/src/extractors/cursor.ts +294 -52
  73. package/src/hash.test.ts +31 -2
  74. package/src/hash.ts +38 -3
  75. package/src/index.ts +1 -0
  76. package/src/redact.test.ts +100 -0
  77. package/src/redact.ts +175 -58
  78. package/src/turn-actors.test.ts +71 -0
  79. package/src/turn-actors.ts +71 -0
  80. package/src/types.ts +6 -0
  81. package/src/utils.ts +3 -1
  82. package/src/validators.ts +3 -0
package/src/redact.ts CHANGED
@@ -5,33 +5,46 @@ export interface RedactOptions {
5
5
  homeDir?: string;
6
6
  }
7
7
 
8
+ export interface RedactionStats {
9
+ changed: boolean;
10
+ homePathMatches: number;
11
+ secretMatches: number;
12
+ piiMatches: number;
13
+ totalMatches: number;
14
+ }
15
+
16
+ export interface RedactionResult {
17
+ trace: NormalizedTrace;
18
+ stats: RedactionStats;
19
+ }
20
+
8
21
  // ─── Secret patterns ────────────────────────────────────────────────────────
9
22
  // Ordered from specific → generic to avoid partial matches being swallowed.
10
23
 
11
24
  const SECRET_PATTERNS: Array<{ re: RegExp; label: string }> = [
12
25
  // Anthropic
13
- { re: /sk-ant-[a-zA-Z0-9\-_]{20,}/g, label: "ANTHROPIC_KEY" },
26
+ { re: /sk-ant-[a-zA-Z0-9\-_]{20,}/g, label: "ANTHROPIC_KEY" },
14
27
  // OpenAI (must come before generic sk- catch-all)
15
- { re: /sk-proj-[a-zA-Z0-9\-_]{20,}/g, label: "OPENAI_KEY" },
16
- { re: /sk-[a-zA-Z0-9]{20,}/g, label: "OPENAI_KEY" },
28
+ { re: /sk-proj-[a-zA-Z0-9\-_]{20,}/g, label: "OPENAI_KEY" },
29
+ { re: /sk-[a-zA-Z0-9]{20,}/g, label: "OPENAI_KEY" },
17
30
  // AWS
18
- { re: /AKIA[0-9A-Z]{16}/g, label: "AWS_ACCESS_KEY" },
31
+ { re: /AKIA[0-9A-Z]{16}/g, label: "AWS_ACCESS_KEY" },
19
32
  { re: /(aws_secret_access_key\s*[=:]\s*)[A-Za-z0-9/+]{40}/gi, label: "AWS_SECRET_KEY" },
20
33
  // GitHub
21
- { re: /github_pat_[a-zA-Z0-9_]{82}/g, label: "GITHUB_PAT" },
22
- { re: /ghp_[a-zA-Z0-9]{36}/g, label: "GITHUB_TOKEN" },
23
- { re: /ghs_[a-zA-Z0-9]{36}/g, label: "GITHUB_TOKEN" },
34
+ { re: /github_pat_[a-zA-Z0-9_]{82}/g, label: "GITHUB_PAT" },
35
+ { re: /ghp_[a-zA-Z0-9]{36}/g, label: "GITHUB_TOKEN" },
36
+ { re: /ghs_[a-zA-Z0-9]{36}/g, label: "GITHUB_TOKEN" },
24
37
  // Stripe
25
- { re: /sk_live_[a-zA-Z0-9]{24,}/g, label: "STRIPE_SECRET_KEY" },
26
- { re: /rk_live_[a-zA-Z0-9]{24,}/g, label: "STRIPE_RESTRICTED_KEY" },
38
+ { re: /sk_live_[a-zA-Z0-9]{24,}/g, label: "STRIPE_SECRET_KEY" },
39
+ { re: /rk_live_[a-zA-Z0-9]{24,}/g, label: "STRIPE_RESTRICTED_KEY" },
27
40
  // Resend
28
- { re: /re_[a-zA-Z0-9]{32,}/g, label: "RESEND_KEY" },
41
+ { re: /re_[a-zA-Z0-9]{32,}/g, label: "RESEND_KEY" },
29
42
  // JWTs — eyJ<base64>.<base64>.<base64>
30
43
  { re: /eyJ[a-zA-Z0-9\-_]+\.[a-zA-Z0-9\-_]+\.[a-zA-Z0-9\-_]*/g, label: "JWT" },
31
44
  // Bearer tokens in Authorization headers
32
- { re: /(Bearer\s+)[a-zA-Z0-9\-._~+/]+=*/gi, label: "BEARER_TOKEN" },
45
+ { re: /(Bearer\s+)[a-zA-Z0-9\-._~+/]+=*/gi, label: "BEARER_TOKEN" },
33
46
  // Passwords in URLs: https://user:PASSWORD@host
34
- { re: /(https?:\/\/[^:@\s]+:)[^:@\s]+(@)/g, label: "URL_PASSWORD" },
47
+ { re: /(https?:\/\/[^:@\s]+:)[^:@\s]+(@)/g, label: "URL_PASSWORD" },
35
48
  // Database DSNs: postgres://user:PASSWORD@host
36
49
  { re: /((?:postgres(?:ql)?|mysql|redis):\/\/[^:]+:)[^@\s]+(@)/g, label: "DB_PASSWORD" },
37
50
  // Generic key/secret assignments: API_KEY=abc123... or secret: "abc123..."
@@ -41,63 +54,144 @@ const SECRET_PATTERNS: Array<{ re: RegExp; label: string }> = [
41
54
  },
42
55
  ];
43
56
 
57
+ const EMPTY_STATS: RedactionStats = {
58
+ changed: false,
59
+ homePathMatches: 0,
60
+ secretMatches: 0,
61
+ piiMatches: 0,
62
+ totalMatches: 0,
63
+ };
64
+
44
65
  // ─── Core string transforms ──────────────────────────────────────────────────
45
66
 
46
- function stripHome(s: string, home: string): string {
47
- return home ? s.replaceAll(home, "~") : s;
67
+ function stripHomeWithCount(s: string, home: string): { value: string; count: number } {
68
+ if (!home || !s.includes(home)) {
69
+ return { value: s, count: 0 };
70
+ }
71
+
72
+ return {
73
+ value: s.replaceAll(home, "~"),
74
+ count: s.split(home).length - 1,
75
+ };
48
76
  }
49
77
 
50
- function stripSecrets(s: string): string {
78
+ function stripSecretsWithCount(s: string): { value: string; count: number } {
51
79
  let out = s;
80
+ let count = 0;
81
+
52
82
  for (const { re, label } of SECRET_PATTERNS) {
53
- // Patterns with capture groups: preserve group 1 (key name), replace group 2 (value)
54
83
  if (re.source.includes("(")) {
55
84
  out = out.replace(re, (...args) => {
56
- // Replace only the non-group parts; keep named prefixes intact
85
+ count++;
57
86
  const groups = args.slice(1, -2) as string[];
58
87
  if (groups.length === 1) return `${groups[0]}[${label}]`;
59
88
  if (groups.length === 2) return `${groups[0]}[${label}]${groups[1]}`;
60
89
  return `[${label}]`;
61
90
  });
62
91
  } else {
63
- out = out.replace(re, `[${label}]`);
92
+ out = out.replace(re, () => {
93
+ count++;
94
+ return `[${label}]`;
95
+ });
64
96
  }
65
- re.lastIndex = 0; // reset stateful global regexes
97
+ re.lastIndex = 0;
66
98
  }
67
- return out;
99
+
100
+ return { value: out, count };
101
+ }
102
+
103
+ function redactStringWithStats(
104
+ s: string,
105
+ home: string
106
+ ): { value: string; stats: RedactionStats } {
107
+ const homeResult = stripHomeWithCount(s, home);
108
+ const secretResult = stripSecretsWithCount(homeResult.value);
109
+ const totalMatches = homeResult.count + secretResult.count;
110
+
111
+ return {
112
+ value: secretResult.value,
113
+ stats: {
114
+ changed: totalMatches > 0,
115
+ homePathMatches: homeResult.count,
116
+ secretMatches: secretResult.count,
117
+ piiMatches: 0,
118
+ totalMatches,
119
+ },
120
+ };
68
121
  }
69
122
 
70
- function redactString(s: string, home: string): string {
71
- return stripSecrets(stripHome(s, home));
123
+ function mergeStats(...stats: RedactionStats[]): RedactionStats {
124
+ return stats.reduce<RedactionStats>(
125
+ (acc, stat) => ({
126
+ changed: acc.changed || stat.changed,
127
+ homePathMatches: acc.homePathMatches + stat.homePathMatches,
128
+ secretMatches: acc.secretMatches + stat.secretMatches,
129
+ piiMatches: acc.piiMatches + stat.piiMatches,
130
+ totalMatches: acc.totalMatches + stat.totalMatches,
131
+ }),
132
+ EMPTY_STATS
133
+ );
72
134
  }
73
135
 
74
136
  // ─── Content block traversal ─────────────────────────────────────────────────
75
137
 
76
- function redactToolInput(input: Record<string, unknown>, home: string): Record<string, unknown> {
77
- return Object.fromEntries(
78
- Object.entries(input).map(([k, v]) => [
79
- k,
80
- typeof v === "string" ? redactString(v, home) : v,
81
- ])
82
- );
138
+ function redactUnknown(value: unknown, home: string): { value: unknown; stats: RedactionStats } {
139
+ if (typeof value === "string") {
140
+ return redactStringWithStats(value, home);
141
+ }
142
+
143
+ if (Array.isArray(value)) {
144
+ const items = value.map((item) => redactUnknown(item, home));
145
+ return {
146
+ value: items.map((item) => item.value),
147
+ stats: mergeStats(...items.map((item) => item.stats)),
148
+ };
149
+ }
150
+
151
+ if (value && typeof value === "object") {
152
+ const entries = Object.entries(value as Record<string, unknown>).map(([key, entryValue]) => {
153
+ const result = redactUnknown(entryValue, home);
154
+ return { key, ...result };
155
+ });
156
+
157
+ return {
158
+ value: Object.fromEntries(entries.map(({ key, value: entryValue }) => [key, entryValue])),
159
+ stats: mergeStats(...entries.map((entry) => entry.stats)),
160
+ };
161
+ }
162
+
163
+ return { value, stats: EMPTY_STATS };
83
164
  }
84
165
 
85
- function redactBlock(block: ContentBlock, home: string): ContentBlock {
166
+ function redactBlock(
167
+ block: ContentBlock,
168
+ home: string
169
+ ): { block: ContentBlock; stats: RedactionStats } {
86
170
  switch (block.type) {
87
171
  case "text":
88
- case "thinking":
89
- return { ...block, text: redactString(block.text, home) };
90
- case "tool_use":
91
- return { ...block, tool_input: redactToolInput(block.tool_input, home) };
92
- case "tool_result":
172
+ case "thinking": {
173
+ const result = redactStringWithStats(block.text, home);
174
+ return { block: { ...block, text: result.value }, stats: result.stats };
175
+ }
176
+ case "tool_use": {
177
+ const result = redactUnknown(block.tool_input, home);
178
+ return {
179
+ block: { ...block, tool_input: result.value as Record<string, unknown> },
180
+ stats: result.stats,
181
+ };
182
+ }
183
+ case "tool_result": {
184
+ if (!block.result_content) {
185
+ return { block, stats: EMPTY_STATS };
186
+ }
187
+ const result = redactStringWithStats(block.result_content, home);
93
188
  return {
94
- ...block,
95
- result_content: block.result_content
96
- ? redactString(block.result_content, home)
97
- : null,
189
+ block: { ...block, result_content: result.value },
190
+ stats: result.stats,
98
191
  };
192
+ }
99
193
  default:
100
- return block;
194
+ return { block, stats: EMPTY_STATS };
101
195
  }
102
196
  }
103
197
 
@@ -113,26 +207,49 @@ export function redactTrace(
113
207
  trace: NormalizedTrace,
114
208
  opts: RedactOptions = {}
115
209
  ): NormalizedTrace {
210
+ return redactTraceWithStats(trace, opts).trace;
211
+ }
212
+
213
+ export function redactTraceWithStats(
214
+ trace: NormalizedTrace,
215
+ opts: RedactOptions = {}
216
+ ): RedactionResult {
116
217
  const home = opts.homeDir ?? "";
117
218
 
118
- return {
119
- ...trace,
120
- turns: trace.turns.map(
121
- (turn): Turn => ({
219
+ const turnResults = trace.turns.map((turn) => {
220
+ const blockResults = turn.content.map((block) => redactBlock(block, home));
221
+ return {
222
+ turn: {
122
223
  ...turn,
123
- content: turn.content.map((b) => redactBlock(b, home)),
124
- })
125
- ),
126
- env_state: trace.env_state
127
- ? {
128
- ...trace.env_state,
129
- inferred_file_tree:
130
- trace.env_state.inferred_file_tree?.map((p) => stripHome(p, home)) ?? null,
131
- inferred_changed_files:
132
- trace.env_state.inferred_changed_files?.map((p) => stripHome(p, home)) ?? null,
133
- inferred_error_files:
134
- trace.env_state.inferred_error_files?.map((p) => stripHome(p, home)) ?? null,
135
- }
136
- : null,
224
+ content: blockResults.map((result) => result.block),
225
+ } satisfies Turn,
226
+ stats: mergeStats(...blockResults.map((result) => result.stats)),
227
+ };
228
+ });
229
+
230
+ const envFileTree = trace.env_state?.inferred_file_tree?.map((path) => redactStringWithStats(path, home)) ?? [];
231
+ const envChangedFiles = trace.env_state?.inferred_changed_files?.map((path) => redactStringWithStats(path, home)) ?? [];
232
+ const envErrorFiles = trace.env_state?.inferred_error_files?.map((path) => redactStringWithStats(path, home)) ?? [];
233
+ const envStats = mergeStats(
234
+ ...envFileTree.map((entry) => entry.stats),
235
+ ...envChangedFiles.map((entry) => entry.stats),
236
+ ...envErrorFiles.map((entry) => entry.stats)
237
+ );
238
+
239
+ return {
240
+ trace: {
241
+ ...trace,
242
+ submitted_by: "[redacted]",
243
+ turns: turnResults.map((result) => result.turn),
244
+ env_state: trace.env_state
245
+ ? {
246
+ ...trace.env_state,
247
+ inferred_file_tree: envFileTree.map((entry) => entry.value) ?? null,
248
+ inferred_changed_files: envChangedFiles.map((entry) => entry.value) ?? null,
249
+ inferred_error_files: envErrorFiles.map((entry) => entry.value) ?? null,
250
+ }
251
+ : null,
252
+ },
253
+ stats: mergeStats(...turnResults.map((result) => result.stats), envStats),
137
254
  };
138
255
  }
@@ -0,0 +1,71 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import type { Turn } from "./types.js";
3
+ import { deriveTurnActors } from "./turn-actors.js";
4
+
5
+ function makeTurn(overrides: Partial<Turn>): Turn {
6
+ return {
7
+ turn_id: "turn-" + Math.random().toString(36).slice(2),
8
+ parent_turn_id: null,
9
+ role: "user",
10
+ timestamp: null,
11
+ content: [],
12
+ model: null,
13
+ usage: null,
14
+ source_metadata: {},
15
+ ...overrides,
16
+ };
17
+ }
18
+
19
+ describe("deriveTurnActors", () => {
20
+ it("marks tool results and chained skill context as tool-authored", () => {
21
+ const assistantTool = makeTurn({
22
+ turn_id: "a1",
23
+ role: "assistant",
24
+ content: [{ type: "tool_use", tool_call_id: "skill-1", tool_name: "Skill", tool_input: {} }],
25
+ });
26
+ const toolResult = makeTurn({
27
+ turn_id: "u1",
28
+ parent_turn_id: "a1",
29
+ content: [{ type: "tool_result", tool_call_id: "skill-1", is_error: false, result_content: "Launching skill: trent-lazyvim", exit_code: null }],
30
+ });
31
+ const skillContext = makeTurn({
32
+ turn_id: "u2",
33
+ parent_turn_id: "u1",
34
+ content: [{ type: "text", text: "Base directory for this skill: ~/.claude/skills/trent-lazyvim" }],
35
+ });
36
+ const assistantReply = makeTurn({
37
+ turn_id: "a2",
38
+ role: "assistant",
39
+ parent_turn_id: "u2",
40
+ content: [{ type: "text", text: "Let me check the dependency versions." }],
41
+ });
42
+ const humanFollowUp = makeTurn({
43
+ turn_id: "u3",
44
+ parent_turn_id: "a2",
45
+ content: [{ type: "text", text: "that still did not fix it" }],
46
+ });
47
+
48
+ const actors = deriveTurnActors([
49
+ assistantTool,
50
+ toolResult,
51
+ skillContext,
52
+ assistantReply,
53
+ humanFollowUp,
54
+ ]);
55
+
56
+ expect(actors.a1).toBe("assistant");
57
+ expect(actors.u1).toBe("tool");
58
+ expect(actors.u2).toBe("tool");
59
+ expect(actors.u3).toBe("human");
60
+ });
61
+
62
+ it("respects a stored actor when one is already present", () => {
63
+ const turn = makeTurn({
64
+ turn_id: "u1",
65
+ actor: "tool",
66
+ content: [{ type: "text", text: "Base directory for this skill: ~/.claude/skills/example" }],
67
+ });
68
+
69
+ expect(deriveTurnActors([turn]).u1).toBe("tool");
70
+ });
71
+ });
@@ -0,0 +1,71 @@
1
+ import type { Turn, TurnActor } from "./types.js";
2
+
3
+ function hasToolResult(turn: Turn): boolean {
4
+ return turn.content.some((block) => block.type === "tool_result");
5
+ }
6
+
7
+ function hasToolUse(turn: Turn): boolean {
8
+ return turn.content.some((block) => block.type === "tool_use");
9
+ }
10
+
11
+ function getParentTurnId(turn: Turn): string | null {
12
+ if (turn.parent_turn_id) return turn.parent_turn_id;
13
+
14
+ const parentUuid = turn.source_metadata.parentUuid;
15
+ return typeof parentUuid === "string" && parentUuid ? parentUuid : null;
16
+ }
17
+
18
+ export function deriveTurnActors(turns: Turn[]): Record<string, TurnActor> {
19
+ const byId = new Map(turns.map((turn) => [turn.turn_id, turn]));
20
+ const actors = new Map<string, TurnActor>();
21
+
22
+ function resolve(turn: Turn, seen = new Set<string>()): TurnActor {
23
+ const cached = actors.get(turn.turn_id);
24
+ if (cached) return cached;
25
+
26
+ if (turn.actor) {
27
+ actors.set(turn.turn_id, turn.actor);
28
+ return turn.actor;
29
+ }
30
+
31
+ if (turn.role === "assistant") {
32
+ actors.set(turn.turn_id, "assistant");
33
+ return "assistant";
34
+ }
35
+
36
+ if (hasToolResult(turn)) {
37
+ actors.set(turn.turn_id, "tool");
38
+ return "tool";
39
+ }
40
+
41
+ const parentTurnId = getParentTurnId(turn);
42
+ if (!parentTurnId || seen.has(turn.turn_id)) {
43
+ actors.set(turn.turn_id, "human");
44
+ return "human";
45
+ }
46
+
47
+ const parent = byId.get(parentTurnId);
48
+ if (!parent) {
49
+ actors.set(turn.turn_id, "human");
50
+ return "human";
51
+ }
52
+
53
+ const nextSeen = new Set(seen);
54
+ nextSeen.add(turn.turn_id);
55
+
56
+ const parentActor = resolve(parent, nextSeen);
57
+ if (parentActor === "tool" || (parentActor === "assistant" && hasToolUse(parent))) {
58
+ actors.set(turn.turn_id, "tool");
59
+ return "tool";
60
+ }
61
+
62
+ actors.set(turn.turn_id, "human");
63
+ return "human";
64
+ }
65
+
66
+ for (const turn of turns) {
67
+ resolve(turn);
68
+ }
69
+
70
+ return Object.fromEntries(actors);
71
+ }
package/src/types.ts CHANGED
@@ -18,6 +18,8 @@ export interface TokenUsage {
18
18
  reasoning_tokens: number | null;
19
19
  }
20
20
 
21
+ export type TurnActor = "human" | "assistant" | "tool";
22
+
21
23
  export type ContentBlock =
22
24
  | { type: "text"; text: string }
23
25
  | { type: "thinking"; text: string }
@@ -29,6 +31,7 @@ export interface Turn {
29
31
  turn_id: string;
30
32
  parent_turn_id: string | null;
31
33
  role: "user" | "assistant";
34
+ actor?: TurnActor;
32
35
  timestamp: string | null;
33
36
  content: ContentBlock[];
34
37
  model: string | null;
@@ -71,6 +74,9 @@ export interface NormalizedTrace {
71
74
  source_session_id: string;
72
75
  chunk_index?: number; // 0-based chunk within the session (set by chunkTrace, default 0)
73
76
  chunk_start_turn?: number; // turn offset in the original session (set by chunkTrace, default 0)
77
+ chunk_complete?: boolean;
78
+ chunk_close_reason?: "100k_tokens" | "idle_2d";
79
+ chunk_closed_at?: string | null;
74
80
  source_version: string | null;
75
81
  submitted_by: string;
76
82
  submitted_at: string;
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
  }
package/src/validators.ts CHANGED
@@ -7,6 +7,9 @@ export const NormalizedTraceSchema = z.object({
7
7
  source_session_id: z.string().min(1),
8
8
  chunk_index: z.number().int().nonnegative().default(0),
9
9
  chunk_start_turn: z.number().int().nonnegative().default(0),
10
+ chunk_complete: z.boolean().optional(),
11
+ chunk_close_reason: z.enum(["100k_tokens", "idle_2d"]).optional(),
12
+ chunk_closed_at: z.string().nullable().optional(),
10
13
  source_version: z.string().nullable().optional(),
11
14
  submitted_by: z.string().optional(),
12
15
  submitted_at: z.string().optional(),