@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
@@ -5,6 +5,22 @@ import type {
5
5
  Turn,
6
6
  ContentBlock,
7
7
  } from "../types.js";
8
+ import {
9
+ collectTraceMetrics,
10
+ createPassiveEnvState,
11
+ extractTextFragments,
12
+ pushUniqueTextBlock,
13
+ } from "./common.js";
14
+
15
+ function toNumber(value: unknown): number | null {
16
+ return typeof value === "number" && Number.isFinite(value) ? value : null;
17
+ }
18
+
19
+ function extractExitCode(output: string | undefined): number | null {
20
+ if (!output) return null;
21
+ const match = output.match(/Process exited with code (\d+)/);
22
+ return match ? Number.parseInt(match[1], 10) : null;
23
+ }
8
24
 
9
25
  export async function extractCodex(
10
26
  rolloutFileBuffer: Buffer,
@@ -29,6 +45,10 @@ export async function extractCodex(
29
45
  let currentBlocks: ContentBlock[] = [];
30
46
  let taskTimestamp: string | null = null;
31
47
  let currentModel: string | null = null;
48
+ let currentTaskLastTimestamp: string | null = null;
49
+ let totalInputTokens: number | null = null;
50
+ let totalOutputTokens: number | null = null;
51
+ let totalCacheReadTokens: number | null = null;
32
52
 
33
53
  for (const event of events) {
34
54
  const type = event["type"] as string;
@@ -41,6 +61,7 @@ export async function extractCodex(
41
61
  currentUserMessage = null;
42
62
  currentBlocks = [];
43
63
  taskTimestamp = timestamp;
64
+ currentTaskLastTimestamp = timestamp;
44
65
  continue;
45
66
  }
46
67
 
@@ -61,10 +82,7 @@ export async function extractCodex(
61
82
 
62
83
  const lastMsg = payload["last_agent_message"] as string | undefined;
63
84
  if (lastMsg) {
64
- const alreadyCaptured = currentBlocks.some(
65
- b => b.type === "text" && (b as { type: "text"; text: string }).text === lastMsg
66
- );
67
- if (!alreadyCaptured) currentBlocks.push({ type: "text", text: lastMsg });
85
+ pushUniqueTextBlock(currentBlocks, "text", lastMsg);
68
86
  }
69
87
 
70
88
  if (currentBlocks.length > 0) {
@@ -72,7 +90,7 @@ export async function extractCodex(
72
90
  turn_id: currentTurnId ?? randomUUID(),
73
91
  parent_turn_id: currentUserMessage ? "user_" + currentTurnId : null,
74
92
  role: "assistant",
75
- timestamp: taskTimestamp,
93
+ timestamp: currentTaskLastTimestamp ?? timestamp ?? taskTimestamp,
76
94
  content: currentBlocks,
77
95
  model: currentModel,
78
96
  usage: null,
@@ -86,11 +104,14 @@ export async function extractCodex(
86
104
  currentBlocks = [];
87
105
  taskTimestamp = null;
88
106
  currentModel = null;
107
+ currentTaskLastTimestamp = null;
89
108
  continue;
90
109
  }
91
110
 
92
111
  if (!inTask) continue;
93
112
 
113
+ if (timestamp) currentTaskLastTimestamp = timestamp;
114
+
94
115
  if (type === "turn_context") {
95
116
  currentModel = (payload["model"] as string | undefined) ?? null;
96
117
  continue;
@@ -102,14 +123,42 @@ export async function extractCodex(
102
123
  currentUserMessage = (payload["message"] as string | undefined) ?? "";
103
124
  break;
104
125
  case "agent_reasoning":
105
- if (payload["text"]) currentBlocks.push({ type: "thinking", text: payload["text"] as string });
126
+ pushUniqueTextBlock(currentBlocks, "thinking", payload["text"] as string | undefined);
106
127
  break;
107
128
  case "agent_message":
108
- if (payload["message"]) currentBlocks.push({ type: "text", text: payload["message"] as string });
129
+ pushUniqueTextBlock(currentBlocks, "text", payload["message"] as string | undefined);
130
+ break;
131
+ case "token_count": {
132
+ const info = (payload["info"] ?? {}) as Record<string, unknown>;
133
+ const usage = (info["total_token_usage"] ?? {}) as Record<string, unknown>;
134
+ totalInputTokens = toNumber(usage["input_tokens"]) ?? totalInputTokens;
135
+ totalOutputTokens = toNumber(usage["output_tokens"]) ?? totalOutputTokens;
136
+ totalCacheReadTokens =
137
+ toNumber(usage["cached_input_tokens"]) ?? totalCacheReadTokens;
109
138
  break;
139
+ }
110
140
  }
111
141
  } else if (type === "response_item") {
112
142
  switch (payload["type"] as string) {
143
+ case "message": {
144
+ const role = payload["role"] as string | undefined;
145
+ const texts = extractTextFragments(payload["content"]);
146
+ if (role === "assistant") {
147
+ for (const text of texts) {
148
+ pushUniqueTextBlock(currentBlocks, "text", text);
149
+ }
150
+ } else if (role === "user" && !currentUserMessage) {
151
+ currentUserMessage = texts.join("\n\n") || currentUserMessage;
152
+ }
153
+ break;
154
+ }
155
+ case "reasoning": {
156
+ const reasoningText =
157
+ extractTextFragments(payload["content"]).join("\n\n") ||
158
+ extractTextFragments(payload["summary"]).join("\n\n");
159
+ pushUniqueTextBlock(currentBlocks, "thinking", reasoningText);
160
+ break;
161
+ }
113
162
  case "function_call": {
114
163
  const callId = (payload["call_id"] as string | undefined) ?? randomUUID();
115
164
  let toolInput: Record<string, unknown> = {};
@@ -126,15 +175,13 @@ export async function extractCodex(
126
175
  }
127
176
  case "function_call_output": {
128
177
  const output = payload["output"] as string | undefined;
129
- const isError = typeof output === "string"
130
- && output.includes("Process exited with code")
131
- && !output.includes("code 0");
178
+ const exitCode = extractExitCode(output);
132
179
  currentBlocks.push({
133
180
  type: "tool_result",
134
181
  tool_call_id: (payload["call_id"] as string | undefined) ?? "",
135
- is_error: isError,
182
+ is_error: exitCode !== null ? exitCode !== 0 : false,
136
183
  result_content: output ?? null,
137
- exit_code: null,
184
+ exit_code: exitCode,
138
185
  });
139
186
  break;
140
187
  }
@@ -152,15 +199,7 @@ export async function extractCodex(
152
199
  }
153
200
  }
154
201
 
155
- const allBlocks = turns.flatMap(t => t.content);
156
- const toolCallCount = allBlocks.filter(b => b.type === "tool_use").length;
157
- const hasFileChanges = allBlocks.some(
158
- b => b.type === "tool_use" && ["write_file", "file_change", "create_file"].includes((b as { type: "tool_use"; tool_name: string }).tool_name)
159
- );
160
- const hasShellCommands = allBlocks.some(
161
- b => b.type === "tool_use" && ["exec_command", "bash", "shell"].includes((b as { type: "tool_use"; tool_name: string }).tool_name)
162
- );
163
- const hasThinking = allBlocks.some(b => b.type === "thinking");
202
+ const metrics = collectTraceMetrics(turns);
164
203
  const startedAt = (sessionMetaPayload["timestamp"] as string | undefined) ?? events[0]?.["timestamp"] as string ?? new Date().toISOString();
165
204
  const endedAt = events[events.length - 1]?.["timestamp"] as string ?? new Date().toISOString();
166
205
 
@@ -179,24 +218,16 @@ export async function extractCodex(
179
218
  ended_at: endedAt,
180
219
  turns,
181
220
  turn_count: turns.length,
182
- tool_call_count: toolCallCount,
183
- has_tool_calls: toolCallCount > 0,
184
- has_thinking_blocks: hasThinking,
185
- has_file_changes: hasFileChanges,
186
- has_shell_commands: hasShellCommands,
187
- total_input_tokens: null,
188
- total_output_tokens: null,
189
- total_cache_read_tokens: null,
221
+ tool_call_count: metrics.toolCallCount,
222
+ has_tool_calls: metrics.toolCallCount > 0,
223
+ has_thinking_blocks: metrics.hasThinkingBlocks,
224
+ has_file_changes: metrics.hasFileChanges,
225
+ has_shell_commands: metrics.hasShellCommands,
226
+ total_input_tokens: totalInputTokens,
227
+ total_output_tokens: totalOutputTokens,
228
+ total_cache_read_tokens: totalCacheReadTokens,
190
229
  content_fidelity: "full",
191
- env_state: {
192
- git_branch: null,
193
- inferred_file_tree: null,
194
- inferred_changed_files: null,
195
- inferred_error_files: null,
196
- shell_exit_codes: null,
197
- open_files_in_editor: null,
198
- extraction_method: "passive",
199
- },
230
+ env_state: createPassiveEnvState(),
200
231
  score: null,
201
232
  raw_r2_key: "",
202
233
  normalized_r2_key: "",
@@ -0,0 +1,139 @@
1
+ import type { ContentBlock, EnvState, Turn } from "../types.js";
2
+
3
+ const SHELL_TOOL_NAMES = ["exec_command", "bash", "shell", "write_stdin"];
4
+ const FILE_MUTATION_TOOL_NAMES = [
5
+ "apply_patch",
6
+ "write_file",
7
+ "create_file",
8
+ "delete_file",
9
+ "rename_file",
10
+ "move_file",
11
+ "file_change",
12
+ "edit",
13
+ "edit_file",
14
+ "multiedit",
15
+ "write",
16
+ ];
17
+
18
+ function normalizeToolName(toolName: string): string {
19
+ return toolName.trim().toLowerCase();
20
+ }
21
+
22
+ function toolNameMatches(toolName: string, candidate: string): boolean {
23
+ const normalized = normalizeToolName(toolName);
24
+ return normalized === candidate || normalized.endsWith(`.${candidate}`);
25
+ }
26
+
27
+ export function isShellToolName(toolName: string): boolean {
28
+ return SHELL_TOOL_NAMES.some((candidate) => toolNameMatches(toolName, candidate));
29
+ }
30
+
31
+ export function isFileMutationTool(
32
+ toolName: string,
33
+ toolInput: Record<string, unknown>,
34
+ ): boolean {
35
+ if (
36
+ FILE_MUTATION_TOOL_NAMES.some((candidate) => toolNameMatches(toolName, candidate))
37
+ ) {
38
+ return true;
39
+ }
40
+
41
+ if (!isShellToolName(toolName)) return false;
42
+
43
+ const command =
44
+ typeof toolInput.cmd === "string"
45
+ ? toolInput.cmd
46
+ : typeof toolInput.command === "string"
47
+ ? toolInput.command
48
+ : null;
49
+
50
+ return command !== null && /\bapply_patch\b|\bsed\s+-i\b|\bperl\s+-pi\b/.test(command);
51
+ }
52
+
53
+ export function collectTraceMetrics(turns: Turn[]) {
54
+ const allBlocks = turns.flatMap((turn) => turn.content);
55
+ const toolUses = allBlocks.filter(
56
+ (block): block is Extract<ContentBlock, { type: "tool_use" }> =>
57
+ block.type === "tool_use",
58
+ );
59
+
60
+ return {
61
+ toolCallCount: toolUses.length,
62
+ hasFileChanges: toolUses.some((block) =>
63
+ isFileMutationTool(block.tool_name, block.tool_input),
64
+ ),
65
+ hasShellCommands: toolUses.some((block) => isShellToolName(block.tool_name)),
66
+ hasThinkingBlocks: allBlocks.some((block) => block.type === "thinking"),
67
+ };
68
+ }
69
+
70
+ export function createPassiveEnvState(
71
+ overrides: Partial<EnvState> = {},
72
+ ): EnvState {
73
+ return {
74
+ git_branch: null,
75
+ inferred_file_tree: null,
76
+ inferred_changed_files: null,
77
+ inferred_error_files: null,
78
+ shell_exit_codes: null,
79
+ open_files_in_editor: null,
80
+ extraction_method: "passive",
81
+ ...overrides,
82
+ };
83
+ }
84
+
85
+ export function extractTextFragments(value: unknown): string[] {
86
+ if (typeof value === "string") {
87
+ return value.trim() ? [value] : [];
88
+ }
89
+
90
+ if (Array.isArray(value)) {
91
+ return value.flatMap((item) => extractTextFragments(item));
92
+ }
93
+
94
+ if (!value || typeof value !== "object") {
95
+ return [];
96
+ }
97
+
98
+ const record = value as Record<string, unknown>;
99
+
100
+ if (typeof record.text === "string" && record.text.trim()) {
101
+ return [record.text];
102
+ }
103
+
104
+ const nested: string[] = [];
105
+ for (const key of ["content", "summary", "thinking", "message"]) {
106
+ if (key in record) {
107
+ nested.push(...extractTextFragments(record[key]));
108
+ }
109
+ }
110
+
111
+ return nested;
112
+ }
113
+
114
+ export function pushUniqueTextBlock(
115
+ blocks: ContentBlock[],
116
+ type: "text" | "thinking",
117
+ text: string | null | undefined,
118
+ ): void {
119
+ const normalized = text?.trim();
120
+ if (!normalized) return;
121
+
122
+ const exists = blocks.some(
123
+ (block) => block.type === type && block.text === normalized,
124
+ );
125
+ if (!exists) {
126
+ blocks.push({ type, text: normalized });
127
+ }
128
+ }
129
+
130
+ export function normalizeTimestamp(value: unknown): string | null {
131
+ if (typeof value === "string" && value.trim()) return value;
132
+
133
+ if (typeof value === "number" && Number.isFinite(value)) {
134
+ const millis = value > 1_000_000_000_000 ? value : value * 1000;
135
+ return new Date(millis).toISOString();
136
+ }
137
+
138
+ return null;
139
+ }