@tracemarketplace/shared 0.0.8 → 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.
- package/dist/extractor-claude-code.test.js +63 -0
- package/dist/extractor-claude-code.test.js.map +1 -1
- package/dist/extractor-codex.test.d.ts +2 -0
- package/dist/extractor-codex.test.d.ts.map +1 -0
- package/dist/extractor-codex.test.js +212 -0
- package/dist/extractor-codex.test.js.map +1 -0
- package/dist/extractor-cursor.test.d.ts +2 -0
- package/dist/extractor-cursor.test.d.ts.map +1 -0
- package/dist/extractor-cursor.test.js +120 -0
- package/dist/extractor-cursor.test.js.map +1 -0
- package/dist/extractors/claude-code.d.ts.map +1 -1
- package/dist/extractors/claude-code.js +11 -5
- package/dist/extractors/claude-code.js.map +1 -1
- package/dist/extractors/codex.d.ts.map +1 -1
- package/dist/extractors/codex.js +63 -35
- package/dist/extractors/codex.js.map +1 -1
- package/dist/extractors/common.d.ts +14 -0
- package/dist/extractors/common.d.ts.map +1 -0
- package/dist/extractors/common.js +100 -0
- package/dist/extractors/common.js.map +1 -0
- package/dist/extractors/cursor.d.ts.map +1 -1
- package/dist/extractors/cursor.js +205 -45
- package/dist/extractors/cursor.js.map +1 -1
- package/dist/hash.d.ts.map +1 -1
- package/dist/hash.js +35 -2
- package/dist/hash.js.map +1 -1
- package/dist/hash.test.js +29 -2
- package/dist/hash.test.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/redact.d.ts +12 -0
- package/dist/redact.d.ts.map +1 -1
- package/dist/redact.js +120 -38
- package/dist/redact.js.map +1 -1
- package/dist/redact.test.d.ts +2 -0
- package/dist/redact.test.d.ts.map +1 -0
- package/dist/redact.test.js +96 -0
- package/dist/redact.test.js.map +1 -0
- package/dist/turn-actors.d.ts +3 -0
- package/dist/turn-actors.d.ts.map +1 -0
- package/dist/turn-actors.js +57 -0
- package/dist/turn-actors.js.map +1 -0
- package/dist/turn-actors.test.d.ts +2 -0
- package/dist/turn-actors.test.d.ts.map +1 -0
- package/dist/turn-actors.test.js +65 -0
- package/dist/turn-actors.test.js.map +1 -0
- package/dist/types.d.ts +5 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/validators.d.ts +24 -0
- package/dist/validators.d.ts.map +1 -1
- package/dist/validators.js +3 -0
- package/dist/validators.js.map +1 -1
- package/package.json +5 -1
- package/src/extractor-claude-code.test.ts +69 -0
- package/src/extractor-codex.test.ts +225 -0
- package/src/extractor-cursor.test.ts +141 -0
- package/src/extractors/claude-code.ts +12 -5
- package/src/extractors/codex.ts +69 -38
- package/src/extractors/common.ts +139 -0
- package/src/extractors/cursor.ts +294 -52
- package/src/hash.test.ts +31 -2
- package/src/hash.ts +38 -3
- package/src/index.ts +1 -0
- package/src/redact.test.ts +100 -0
- package/src/redact.ts +175 -58
- package/src/turn-actors.test.ts +71 -0
- package/src/turn-actors.ts +71 -0
- package/src/types.ts +6 -0
- 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,
|
|
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,
|
|
16
|
-
{ re: /sk-[a-zA-Z0-9]{20,}/g,
|
|
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,
|
|
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,
|
|
22
|
-
{ re: /ghp_[a-zA-Z0-9]{36}/g,
|
|
23
|
-
{ re: /ghs_[a-zA-Z0-9]{36}/g,
|
|
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,
|
|
26
|
-
{ re: /rk_live_[a-zA-Z0-9]{24,}/g,
|
|
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,
|
|
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,
|
|
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,
|
|
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
|
|
47
|
-
|
|
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
|
|
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
|
-
|
|
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,
|
|
92
|
+
out = out.replace(re, () => {
|
|
93
|
+
count++;
|
|
94
|
+
return `[${label}]`;
|
|
95
|
+
});
|
|
64
96
|
}
|
|
65
|
-
re.lastIndex = 0;
|
|
97
|
+
re.lastIndex = 0;
|
|
66
98
|
}
|
|
67
|
-
|
|
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
|
|
71
|
-
return
|
|
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
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
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(
|
|
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
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
case "
|
|
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
|
-
|
|
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
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
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:
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
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/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(),
|