@seanxdo/superview 0.1.13

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.
@@ -0,0 +1,720 @@
1
+ import path from "node:path";
2
+ import {
3
+ Artifact,
4
+ AgentProvider,
5
+ NormalizedBundle,
6
+ ParsedCodexLine,
7
+ ProjectRecord,
8
+ RawEventRef,
9
+ SessionRecord,
10
+ SkillUsage,
11
+ SkillUsageSource,
12
+ TimelineEvent,
13
+ TimelineLane,
14
+ TokenUsage,
15
+ TurnRecord
16
+ } from "./types";
17
+ import { safeExcerpt } from "./redactor";
18
+ import { stableId } from "./id";
19
+
20
+ interface NormalizeOptions {
21
+ repoRoot?: string | null;
22
+ provider?: AgentProvider;
23
+ prefixSessionId?: boolean;
24
+ modelProvider?: string | null;
25
+ source?: string | null;
26
+ agentName?: string | null;
27
+ }
28
+
29
+ export function normalizeCodexLines(lines: ParsedCodexLine[], options: NormalizeOptions = {}): NormalizedBundle | null {
30
+ if (lines.length === 0) {
31
+ return null;
32
+ }
33
+
34
+ const sessionMeta = lines.find((line) => line.type === "session_meta");
35
+ const metaPayload = asRecord(sessionMeta?.payload);
36
+ const provider = options.provider ?? "codex";
37
+ const externalSessionId = stringValue(metaPayload.id) ?? stableId("session", lines[0]?.sourcePath ?? "unknown");
38
+ const sessionId = options.prefixSessionId ? `${provider}:${externalSessionId}` : externalSessionId;
39
+ const cwd = stringValue(metaPayload.cwd) ?? process.cwd();
40
+ const startedAt = stringValue(metaPayload.timestamp) ?? sessionMeta?.timestamp ?? lines[0]?.timestamp ?? new Date().toISOString();
41
+ const repoRoot = options.repoRoot ?? null;
42
+ const projectId = stableId("project", provider, repoRoot ?? cwd);
43
+
44
+ const project: ProjectRecord = {
45
+ id: projectId,
46
+ name: path.basename(repoRoot ?? cwd) || "Unknown Project",
47
+ cwd,
48
+ repoRoot,
49
+ createdAt: startedAt,
50
+ updatedAt: lines.at(-1)?.timestamp ?? startedAt
51
+ };
52
+
53
+ const session: SessionRecord = {
54
+ id: sessionId,
55
+ projectId,
56
+ path: lines[0]?.sourcePath ?? "",
57
+ cwd,
58
+ startedAt,
59
+ endedAt: lines.at(-1)?.timestamp ?? null,
60
+ cliVersion: stringValue(metaPayload.cli_version),
61
+ modelProvider: options.modelProvider ?? stringValue(metaPayload.model_provider),
62
+ source: options.source ?? stringValue(metaPayload.source),
63
+ provider,
64
+ externalSessionId,
65
+ agentName: options.agentName ?? null
66
+ };
67
+
68
+ const rawEventRefs: RawEventRef[] = lines.map((line) => ({
69
+ id: stableId("raw", sessionId, line.lineNo, line.sha256),
70
+ sessionId,
71
+ provider,
72
+ lineNo: line.lineNo,
73
+ timestamp: line.timestamp,
74
+ type: line.type,
75
+ redactedPayloadJson: JSON.stringify(line.redactedPayload),
76
+ sourcePath: line.sourcePath,
77
+ sha256: line.sha256
78
+ }));
79
+
80
+ const turns = new Map<string, TurnRecord>();
81
+ const events: TimelineEvent[] = [];
82
+ const artifacts: Artifact[] = [];
83
+ let previousTotalTokenUsage: TokenUsage | null = null;
84
+
85
+ for (const [index, line] of lines.entries()) {
86
+ const rawRef = rawEventRefs[index];
87
+ const payload = asRecord(line.redactedPayload);
88
+ const rawPayload = asRecord(line.payload);
89
+ const turnId = stringValue(payload.turn_id) ?? stringValue(rawPayload.turn_id) ?? null;
90
+ const payloadType = stringValue(payload.type) ?? stringValue(rawPayload.type);
91
+
92
+ if (line.type === "turn_context") {
93
+ const id = stringValue(payload.turn_id) ?? stableId("turn", sessionId, line.lineNo);
94
+ turns.set(id, {
95
+ id,
96
+ sessionId,
97
+ startedAt: line.timestamp,
98
+ endedAt: null,
99
+ cwd: stringValue(payload.cwd),
100
+ model: stringValue(payload.model),
101
+ approvalPolicy: stringValue(payload.approval_policy),
102
+ sandboxPolicy: stringValue(payload.sandbox_policy)
103
+ });
104
+ events.push(makeEvent({ line, rawRef, projectId, sessionId, turnId: id, kind: "turn", lane: "Agent Runs", title: "Turn started", detail: stringValue(payload.cwd) }));
105
+ continue;
106
+ }
107
+
108
+ if (line.type === "session_meta") {
109
+ events.push(makeEvent({ line, rawRef, projectId, sessionId, turnId, kind: "session", lane: "Agent Runs", title: "Session started", detail: cwd, status: "success" }));
110
+ continue;
111
+ }
112
+
113
+ if (line.type === "parse_error") {
114
+ events.push(makeEvent({ line, rawRef, projectId, sessionId, turnId, kind: "error", lane: "Risks", title: "Parse error", detail: safeExcerpt(payload, 500), status: "failed" }));
115
+ continue;
116
+ }
117
+
118
+ if (line.type === "response_item") {
119
+ const event = normalizeResponseItem({ line, rawRef, projectId, sessionId, turnId, payload, rawPayload });
120
+ events.push(event);
121
+ artifacts.push(makeArtifact(event, rawRef, payload));
122
+ continue;
123
+ }
124
+
125
+ if (line.type === "event_msg") {
126
+ const event = normalizeEventMessage({ line, rawRef, projectId, sessionId, turnId, payload, rawPayload, payloadType: payloadType ?? undefined, previousTotalTokenUsage });
127
+ if (event.kind === "token_usage") {
128
+ const totalUsage = extractTokenCountTotalUsage(rawPayload) ?? extractTokenCountTotalUsage(payload);
129
+ previousTotalTokenUsage = totalUsage ?? (event.tokenUsage ? addTokenUsage(previousTotalTokenUsage, event.tokenUsage) : previousTotalTokenUsage);
130
+ }
131
+ events.push(event);
132
+ if (event.kind === "error") {
133
+ artifacts.push(makeArtifact(event, rawRef, payload));
134
+ }
135
+ continue;
136
+ }
137
+
138
+ events.push(makeEvent({ line, rawRef, projectId, sessionId, turnId, kind: "status", lane: "Agent Runs", title: line.type, detail: safeExcerpt(payload, 500) }));
139
+ }
140
+
141
+ associateFunctionCallOutputs(events);
142
+
143
+ return {
144
+ project,
145
+ session,
146
+ turns: Array.from(turns.values()),
147
+ rawEventRefs,
148
+ events,
149
+ artifacts
150
+ };
151
+ }
152
+
153
+ function normalizeResponseItem(input: {
154
+ line: ParsedCodexLine;
155
+ rawRef: RawEventRef;
156
+ projectId: string;
157
+ sessionId: string;
158
+ turnId: string | null;
159
+ payload: Record<string, unknown>;
160
+ rawPayload: Record<string, unknown>;
161
+ }): TimelineEvent {
162
+ const { line, rawRef, projectId, sessionId, turnId, payload, rawPayload } = input;
163
+ const payloadType = stringValue(payload.type) ?? stringValue(rawPayload.type);
164
+ const role = stringValue(payload.role) ?? stringValue(rawPayload.role);
165
+ const tokenUsage = extractTokenUsage(rawPayload);
166
+ const skillSource = skillSourceForResponse(payloadType, role);
167
+ const skills = extractSkillsForLine(line, skillSource);
168
+
169
+ if (payloadType === "message") {
170
+ const text = extractMessageText(payload);
171
+ if (role === "user") {
172
+ return makeEvent({ line, rawRef, projectId, sessionId, turnId, kind: "user_prompt", lane: "Product", title: summarize(text, "User prompt"), detail: text, status: "success", tokenUsage, skills });
173
+ }
174
+ if (role === "assistant") {
175
+ return makeEvent({ line, rawRef, projectId, sessionId, turnId, kind: "assistant_message", lane: "Agent Runs", title: summarize(text, "Assistant message"), detail: text, status: "success", tokenUsage, skills });
176
+ }
177
+ return makeEvent({ line, rawRef, projectId, sessionId, turnId, kind: "status", lane: "Agent Runs", title: `${role ?? "System"} message`, detail: text, status: "success", tokenUsage, skills });
178
+ }
179
+
180
+ if (payloadType === "function_call" || payloadType === "custom_tool_call") {
181
+ const toolName = stringValue(payload.name) ?? stringValue(rawPayload.name) ?? "tool";
182
+ const title = titleForToolCall(toolName, payload);
183
+ const lane = laneForToolCall(toolName, payload);
184
+ const kind = kindForToolCall(toolName, payload);
185
+ return makeEvent({
186
+ line,
187
+ rawRef,
188
+ projectId,
189
+ sessionId,
190
+ turnId,
191
+ kind,
192
+ lane,
193
+ title,
194
+ detail: safeExcerpt(payload.arguments ?? payload, 900),
195
+ toolName,
196
+ callId: stringValue(payload.call_id) ?? stringValue(rawPayload.call_id),
197
+ status: "running",
198
+ files: extractFiles(payload),
199
+ tokenUsage,
200
+ skills
201
+ });
202
+ }
203
+
204
+ if (payloadType === "function_call_output" || payloadType === "custom_tool_call_output") {
205
+ const output = stringValue(payload.output) ?? safeExcerpt(payload, 900);
206
+ const failed = /failed|error|exit code [1-9]|exception|traceback/i.test(output);
207
+ const verification = /test|lint|build|typecheck|tsc|playwright|pytest|vitest|pass|fail/i.test(output);
208
+ return makeEvent({
209
+ line,
210
+ rawRef,
211
+ projectId,
212
+ sessionId,
213
+ turnId,
214
+ kind: failed ? "error" : verification ? "verification" : "tool_result",
215
+ lane: failed ? "Risks" : verification ? "Verification" : "Agent Runs",
216
+ title: failed ? "Tool output failed" : verification ? "Verification output" : "Tool output",
217
+ detail: safeExcerpt(output, 1200),
218
+ callId: stringValue(payload.call_id) ?? stringValue(rawPayload.call_id),
219
+ status: failed ? "failed" : "success",
220
+ files: extractFiles(payload),
221
+ tokenUsage,
222
+ skills
223
+ });
224
+ }
225
+
226
+ if (payloadType === "reasoning") {
227
+ return makeEvent({ line, rawRef, projectId, sessionId, turnId, kind: "reasoning_marker", lane: "Agent Runs", title: "Reasoning segment", detail: "Reasoning content is not displayed.", status: "success", tokenUsage, skills });
228
+ }
229
+
230
+ return makeEvent({ line, rawRef, projectId, sessionId, turnId, kind: "status", lane: "Agent Runs", title: payloadType ?? "Response item", detail: safeExcerpt(payload, 900), tokenUsage, skills });
231
+ }
232
+
233
+ function normalizeEventMessage(input: {
234
+ line: ParsedCodexLine;
235
+ rawRef: RawEventRef;
236
+ projectId: string;
237
+ sessionId: string;
238
+ turnId: string | null;
239
+ payload: Record<string, unknown>;
240
+ rawPayload: Record<string, unknown>;
241
+ payloadType?: string;
242
+ previousTotalTokenUsage?: TokenUsage | null;
243
+ }): TimelineEvent {
244
+ const { line, rawRef, projectId, sessionId, turnId, payload, rawPayload, payloadType, previousTotalTokenUsage } = input;
245
+ if (payloadType === "token_count") {
246
+ const totalUsage = extractTokenCountTotalUsage(rawPayload) ?? extractTokenCountTotalUsage(payload);
247
+ const tokenUsage = tokenUsageDelta(previousTotalTokenUsage, totalUsage);
248
+ return makeEvent({
249
+ line,
250
+ rawRef,
251
+ projectId,
252
+ sessionId,
253
+ turnId,
254
+ kind: "token_usage",
255
+ lane: "Agent Runs",
256
+ title: "Token usage update",
257
+ detail: safeExcerpt(payload, 800),
258
+ status: "success",
259
+ tokenUsage,
260
+ skills: extractSkillsForLine(line, "event_message")
261
+ });
262
+ }
263
+
264
+ const message = stringValue(payload.message) ?? stringValue(payload.msg) ?? payloadType ?? "Event";
265
+ const failed = /error|failed|abort|panic/i.test(message);
266
+ return makeEvent({
267
+ line,
268
+ rawRef,
269
+ projectId,
270
+ sessionId,
271
+ turnId,
272
+ kind: failed ? "error" : "status",
273
+ lane: failed ? "Risks" : "Agent Runs",
274
+ title: summarize(message, payloadType ?? "Event"),
275
+ detail: safeExcerpt(payload, 800),
276
+ status: failed ? "failed" : "success",
277
+ skills: extractSkillsForLine(line, "event_message")
278
+ });
279
+ }
280
+
281
+ function makeEvent(input: {
282
+ line: ParsedCodexLine;
283
+ rawRef: RawEventRef;
284
+ projectId: string;
285
+ sessionId: string;
286
+ turnId: string | null;
287
+ kind: TimelineEvent["kind"];
288
+ lane: TimelineLane;
289
+ title: string;
290
+ detail?: string | null;
291
+ toolName?: string | null;
292
+ callId?: string | null;
293
+ status?: TimelineEvent["status"];
294
+ files?: string[];
295
+ durationMs?: number | null;
296
+ outputEventId?: string | null;
297
+ tokenUsage?: TokenUsage | null;
298
+ skills?: SkillUsage[];
299
+ }): TimelineEvent {
300
+ return {
301
+ id: stableId("event", input.sessionId, input.line.lineNo, input.kind, input.title),
302
+ projectId: input.projectId,
303
+ sessionId: input.sessionId,
304
+ turnId: input.turnId,
305
+ timestamp: input.line.timestamp,
306
+ kind: input.kind,
307
+ lane: input.lane,
308
+ title: input.title,
309
+ detail: input.detail ?? null,
310
+ toolName: input.toolName ?? null,
311
+ callId: input.callId ?? null,
312
+ status: input.status ?? "unknown",
313
+ files: input.files ?? [],
314
+ rawEventRefId: input.rawRef.id,
315
+ durationMs: input.durationMs ?? null,
316
+ outputEventId: input.outputEventId ?? null,
317
+ tokenUsage: input.tokenUsage ?? null,
318
+ skills: input.skills ?? []
319
+ };
320
+ }
321
+
322
+ function associateFunctionCallOutputs(events: TimelineEvent[]): void {
323
+ const callsById = new Map<string, TimelineEvent>();
324
+
325
+ for (const event of events) {
326
+ if (event.callId && event.toolName && (event.kind === "tool_call" || event.kind === "file_change" || event.kind === "verification")) {
327
+ callsById.set(event.callId, event);
328
+ }
329
+ }
330
+
331
+ for (const output of events) {
332
+ if (!output.callId) continue;
333
+ const call = callsById.get(output.callId);
334
+ if (!call || call.id === output.id) continue;
335
+
336
+ const failed = output.status === "failed" || output.kind === "error";
337
+ call.status = failed ? "failed" : "success";
338
+ call.outputEventId = output.id;
339
+ call.durationMs = durationBetween(call.timestamp, output.timestamp);
340
+
341
+ const normalized = outputShapeForCall(call, output);
342
+ output.kind = normalized.kind;
343
+ output.lane = normalized.lane;
344
+ output.title = normalized.title;
345
+ output.status = normalized.status;
346
+ output.toolName = call.toolName;
347
+ output.files = Array.from(new Set([...call.files, ...output.files]));
348
+ }
349
+ }
350
+
351
+ function outputShapeForCall(call: TimelineEvent, output: TimelineEvent): Pick<TimelineEvent, "kind" | "lane" | "title" | "status"> {
352
+ if (output.status === "failed" || output.kind === "error") {
353
+ return { kind: "error", lane: "Risks", title: "Tool output failed", status: "failed" };
354
+ }
355
+ if (call.kind === "verification" || call.lane === "Verification") {
356
+ return { kind: "verification", lane: "Verification", title: "Verification output", status: "success" };
357
+ }
358
+ if (call.kind === "file_change") {
359
+ return { kind: "file_change", lane: call.lane, title: "Patch output", status: "success" };
360
+ }
361
+ return { kind: "tool_result", lane: call.lane, title: "Tool output", status: "success" };
362
+ }
363
+
364
+ function durationBetween(start: string, end: string): number | null {
365
+ const startMs = Date.parse(start);
366
+ const endMs = Date.parse(end);
367
+ if (!Number.isFinite(startMs) || !Number.isFinite(endMs) || endMs < startMs) {
368
+ return null;
369
+ }
370
+ return endMs - startMs;
371
+ }
372
+
373
+ function makeArtifact(event: TimelineEvent, rawRef: RawEventRef, payload: unknown): Artifact {
374
+ return {
375
+ id: stableId("artifact", event.id, rawRef.id),
376
+ eventId: event.id,
377
+ type: event.kind === "tool_result" || event.kind === "verification" || event.kind === "error" ? "command_output" : "payload",
378
+ path: rawRef.sourcePath,
379
+ excerpt: safeExcerpt(payload, 1600),
380
+ sha256: rawRef.sha256
381
+ };
382
+ }
383
+
384
+ function titleForToolCall(toolName: string, payload: Record<string, unknown>): string {
385
+ const args = stringValue(payload.arguments) ?? "";
386
+ if (toolName.includes("apply_patch")) return "Patched files";
387
+ if (toolName.includes("exec_command")) return summarize(args, "Shell command");
388
+ if (toolName.includes("web")) return "Web lookup";
389
+ if (toolName.includes("browser") || toolName.includes("playwright")) return "Browser check";
390
+ return `Tool call: ${toolName}`;
391
+ }
392
+
393
+ function laneForToolCall(toolName: string, payload: Record<string, unknown>): TimelineLane {
394
+ const haystack = `${toolName} ${stringValue(payload.arguments) ?? ""}`;
395
+ if (mentionsArchitectureFile(haystack)) return "Architecture";
396
+ if (/apply_patch|write|edit|patch|git diff/i.test(haystack)) return "Code";
397
+ if (/test|lint|build|typecheck|tsc|playwright|curl/i.test(haystack)) return "Verification";
398
+ return "Agent Runs";
399
+ }
400
+
401
+ function kindForToolCall(toolName: string, payload: Record<string, unknown>): TimelineEvent["kind"] {
402
+ const haystack = `${toolName} ${stringValue(payload.arguments) ?? ""}`;
403
+ if (/apply_patch|write|edit|patch/i.test(haystack)) return "file_change";
404
+ if (/test|lint|build|typecheck|tsc|playwright|curl/i.test(haystack)) return "verification";
405
+ return "tool_call";
406
+ }
407
+
408
+ function extractMessageText(payload: Record<string, unknown>): string {
409
+ const content = payload.content;
410
+ if (typeof content === "string") return content;
411
+ if (Array.isArray(content)) {
412
+ return content
413
+ .map((item) => {
414
+ if (typeof item === "string") return item;
415
+ const record = asRecord(item);
416
+ return stringValue(record.text) ?? stringValue(record.content) ?? "";
417
+ })
418
+ .filter(Boolean)
419
+ .join("\n");
420
+ }
421
+ return safeExcerpt(payload, 500);
422
+ }
423
+
424
+ function skillSourceForResponse(payloadType: string | null | undefined, role: string | null | undefined): SkillUsageSource {
425
+ if (payloadType === "function_call" || payloadType === "custom_tool_call") return "tool_input";
426
+ if (payloadType === "function_call_output" || payloadType === "custom_tool_call_output") return "tool_output";
427
+ if (role === "developer") return "developer_message";
428
+ if (role === "user") return "user_prompt";
429
+ if (role === "assistant") return "assistant_message";
430
+ return "event_message";
431
+ }
432
+
433
+ function extractSkillsForLine(line: ParsedCodexLine, source: SkillUsageSource): SkillUsage[] {
434
+ if (source === "session_meta" || source === "developer_message") {
435
+ return [];
436
+ }
437
+ const text = skillSearchText(line.payload);
438
+ if (!text) return [];
439
+
440
+ const skills = new Map<string, SkillUsage>();
441
+ for (const match of matchSkillUsages(text, source, line.sourcePath)) {
442
+ const key = `${match.name}\0${match.path ?? ""}\0${match.source}`;
443
+ if (!skills.has(key)) skills.set(key, match);
444
+ }
445
+ return [...skills.values()];
446
+ }
447
+
448
+ function matchSkillUsages(text: string, source: SkillUsageSource, evidencePath: string): SkillUsage[] {
449
+ if (!mightContainSkillUsage(text, source)) {
450
+ return [];
451
+ }
452
+
453
+ const matches: SkillUsage[] = [];
454
+ const push = (name: string, confidence: SkillUsage["confidence"], path: string | null, command: string | null, excerpt: string) => {
455
+ const normalized = normalizeSkillName(name);
456
+ if (!normalized) return;
457
+ matches.push({
458
+ name: normalized,
459
+ source,
460
+ confidence,
461
+ path,
462
+ command,
463
+ evidencePath,
464
+ excerpt: cleanExcerpt(excerpt)
465
+ });
466
+ };
467
+
468
+ const pathPattern = /((?:~|\/)[^"'`\s]*\/skills\/([A-Za-z0-9_.:@-]+)(?:\/SKILL\.md)?)/gi;
469
+ for (const match of text.matchAll(pathPattern)) {
470
+ push(match[2], "explicit", match[1], null, match[0]);
471
+ }
472
+
473
+ for (const match of text.matchAll(/\b(?:using|use|activated|activate|loaded|loading)\s+(?:the\s+)?(?:skill|plugin skill)\s+[`"']?([A-Za-z0-9_.:@-]+)[`"']?/gi)) {
474
+ push(match[1], "explicit", null, null, match[0]);
475
+ }
476
+
477
+ for (const match of text.matchAll(/\b(?:skill|技能)\s*[:=]\s*[`"']?([A-Za-z0-9_.:@-]+)[`"']?/gi)) {
478
+ push(match[1], "explicit", null, null, match[0]);
479
+ }
480
+
481
+ for (const match of text.matchAll(/(?:^|[\s(])\/([A-Za-z][A-Za-z0-9_.:@-]{2,})(?=\s|$|[),.])/g)) {
482
+ push(match[1], "inferred", null, `/${match[1]}`, match[0]);
483
+ }
484
+
485
+ return matches;
486
+ }
487
+
488
+ function mightContainSkillUsage(text: string, source: SkillUsageSource): boolean {
489
+ if (/skill|技能|\/skills\//i.test(text)) return true;
490
+ return source === "user_prompt" && /(?:^|\s)\/[A-Za-z][A-Za-z0-9_.:@-]{2,}(?=\s|$|[),.])/.test(text);
491
+ }
492
+
493
+ function skillSearchText(value: unknown): string {
494
+ const fragments: string[] = [];
495
+ collectSkillText(value, fragments);
496
+ return fragments.join("\n");
497
+ }
498
+
499
+ function collectSkillText(value: unknown, fragments: string[]) {
500
+ if (typeof value === "string") {
501
+ fragments.push(value);
502
+ return;
503
+ }
504
+ if (!value || typeof value !== "object") return;
505
+ if (Array.isArray(value)) {
506
+ for (const item of value) collectSkillText(item, fragments);
507
+ return;
508
+ }
509
+
510
+ for (const [key, child] of Object.entries(value)) {
511
+ const normalizedKey = key.toLowerCase();
512
+ if (typeof child === "string" && /skill|message|text|content|input|argument|output|command|cmd|path|source|aggregated_output|formatted_output/i.test(normalizedKey)) {
513
+ fragments.push(child);
514
+ } else if (typeof child === "object" && child) {
515
+ collectSkillText(child, fragments);
516
+ }
517
+ }
518
+ }
519
+
520
+ function normalizeSkillName(value: string): string | null {
521
+ const clean = value.replace(/^\/+/, "").replace(/\/SKILL\.md$/i, "").trim();
522
+ if (!/^[A-Za-z0-9_.:@-]{3,80}$/.test(clean)) return null;
523
+ if (/^(skill|skills|plugin|using|loaded)$/i.test(clean)) return null;
524
+ return clean;
525
+ }
526
+
527
+ function cleanExcerpt(value: string): string {
528
+ return value.replace(/\s+/g, " ").trim().slice(0, 180);
529
+ }
530
+
531
+ function extractFiles(value: unknown): string[] {
532
+ const text = typeof value === "string" ? value : JSON.stringify(value);
533
+ const matches = text.match(/(?:[A-Za-z0-9_.-]+\/)+[A-Za-z0-9_.-]+\.[A-Za-z0-9]+/g) ?? [];
534
+ return Array.from(new Set(matches)).slice(0, 20);
535
+ }
536
+
537
+ function mentionsArchitectureFile(text: string): boolean {
538
+ return /(?:^|[/"'\s])(?:docs?|design|plans?|architecture)(?:\/|[A-Za-z0-9_.-]*\.(?:md|mdx|txt|json|yaml|yml|toml))|(?:^|[/"'\s])(?:[A-Za-z0-9_.-]*-)?(?:design|plan|architecture)(?:-[A-Za-z0-9_.-]*)?\.(?:md|mdx|txt|json|yaml|yml|toml)/i.test(text);
539
+ }
540
+
541
+ function summarize(text: string | null | undefined, fallback: string): string {
542
+ const clean = (text ?? "").replace(/\s+/g, " ").trim();
543
+ if (!clean) return fallback;
544
+ return clean.length > 88 ? `${clean.slice(0, 85)}...` : clean;
545
+ }
546
+
547
+ function extractTokenUsage(payload: Record<string, unknown>): TokenUsage | null {
548
+ const usageContainers = collectUsageContainers(payload);
549
+ if (usageContainers.length === 0) return null;
550
+
551
+ const rawInput = firstNumber(usageContainers, ["input_tokens", "prompt_tokens", "input", "prompt"]);
552
+ const output = firstNumber(usageContainers, ["output_tokens", "completion_tokens", "output", "completion"]);
553
+ const reasoning = firstNumber(usageContainers, ["reasoning_tokens", "reasoning"]);
554
+ const cachedRead = firstNumber(usageContainers, ["cached_input_tokens", "cached_tokens", "cache_read_input_tokens", "cached_input"]);
555
+ // Anthropic style: cache_read_input_tokens / cache_creation_input_tokens are reported SEPARATELY from input_tokens.
556
+ // OpenAI/Codex style: cached_tokens is already INCLUDED in prompt_tokens/input_tokens.
557
+ const anthropicCacheRead = firstNumber(usageContainers, ["cache_read_input_tokens"]);
558
+ const anthropicCacheCreation = firstNumber(usageContainers, ["cache_creation_input_tokens"]);
559
+ const isAnthropicStyle = anthropicCacheRead !== null || anthropicCacheCreation !== null;
560
+ const input = isAnthropicStyle
561
+ ? (rawInput ?? 0) + (anthropicCacheRead ?? 0) + (anthropicCacheCreation ?? 0)
562
+ : rawInput;
563
+ const cachedInput = isAnthropicStyle ? (anthropicCacheRead ?? 0) : cachedRead;
564
+ const explicitTotal = firstNumber(usageContainers, ["total_tokens", "total"]);
565
+ const knownSum = sumNumbers(input, output, reasoning);
566
+ const total = explicitTotal ?? knownSum;
567
+
568
+ if (rawInput === null && output === null && reasoning === null && cachedRead === null && total === null && !isAnthropicStyle) {
569
+ return null;
570
+ }
571
+
572
+ return {
573
+ input: input ?? 0,
574
+ output: output ?? 0,
575
+ reasoning: reasoning ?? 0,
576
+ cachedInput: cachedInput ?? 0,
577
+ total: total ?? 0
578
+ };
579
+ }
580
+
581
+ function extractTokenCountTotalUsage(payload: Record<string, unknown>): TokenUsage | null {
582
+ const info = asRecord(payload.info);
583
+ const total = asRecord(info.total_token_usage ?? info.totalTokenUsage ?? payload.total_token_usage ?? payload.totalTokenUsage);
584
+ if (Object.keys(total).length === 0) return null;
585
+ return tokenUsageFromContainer(total);
586
+ }
587
+
588
+ function tokenUsageFromContainer(container: Record<string, unknown>): TokenUsage | null {
589
+ const rawInput = firstNumber(container, ["input_tokens", "prompt_tokens", "input", "prompt"]);
590
+ const output = firstNumber(container, ["output_tokens", "completion_tokens", "output", "completion"]);
591
+ const reasoning = firstNumber(container, ["reasoning_output_tokens", "reasoning_tokens", "reasoning"]);
592
+ const cachedRead = firstNumber(container, ["cached_input_tokens", "cached_tokens", "cache_read_input_tokens", "cached_input"]);
593
+ const anthropicCacheRead = firstNumber(container, ["cache_read_input_tokens"]);
594
+ const anthropicCacheCreation = firstNumber(container, ["cache_creation_input_tokens"]);
595
+ const isAnthropicStyle = anthropicCacheRead !== null || anthropicCacheCreation !== null;
596
+ const input = isAnthropicStyle
597
+ ? (rawInput ?? 0) + (anthropicCacheRead ?? 0) + (anthropicCacheCreation ?? 0)
598
+ : rawInput;
599
+ const cachedInput = isAnthropicStyle ? (anthropicCacheRead ?? 0) : cachedRead;
600
+ const explicitTotal = firstNumber(container, ["total_tokens", "total"]);
601
+ const knownSum = sumNumbers(input, output);
602
+ const total = explicitTotal ?? knownSum;
603
+
604
+ if (rawInput === null && output === null && reasoning === null && cachedRead === null && total === null && !isAnthropicStyle) {
605
+ return null;
606
+ }
607
+
608
+ return {
609
+ input: input ?? 0,
610
+ output: output ?? 0,
611
+ reasoning: reasoning ?? 0,
612
+ cachedInput: cachedInput ?? 0,
613
+ total: total ?? 0
614
+ };
615
+ }
616
+
617
+ function tokenUsageDelta(previous: TokenUsage | null | undefined, current: TokenUsage | null): TokenUsage | null {
618
+ if (!current) return null;
619
+ if (!previous) return current;
620
+ const delta = {
621
+ input: Math.max(0, current.input - previous.input),
622
+ output: Math.max(0, current.output - previous.output),
623
+ reasoning: Math.max(0, current.reasoning - previous.reasoning),
624
+ cachedInput: Math.max(0, current.cachedInput - previous.cachedInput),
625
+ total: Math.max(0, current.total - previous.total)
626
+ };
627
+ return hasTokenUsage(delta) ? delta : null;
628
+ }
629
+
630
+ function addTokenUsage(previous: TokenUsage | null | undefined, delta: TokenUsage): TokenUsage {
631
+ return {
632
+ input: (previous?.input ?? 0) + delta.input,
633
+ output: (previous?.output ?? 0) + delta.output,
634
+ reasoning: (previous?.reasoning ?? 0) + delta.reasoning,
635
+ cachedInput: (previous?.cachedInput ?? 0) + delta.cachedInput,
636
+ total: (previous?.total ?? 0) + delta.total
637
+ };
638
+ }
639
+
640
+ function hasTokenUsage(usage: TokenUsage): boolean {
641
+ return usage.input > 0 || usage.output > 0 || usage.reasoning > 0 || usage.cachedInput > 0 || usage.total > 0;
642
+ }
643
+
644
+ function collectUsageContainers(payload: Record<string, unknown>): unknown[] {
645
+ const containers: unknown[] = [];
646
+ for (const [key, value] of Object.entries(payload)) {
647
+ const normalized = normalizeTokenKey(key);
648
+ if ((normalized.includes("usage") || normalized.includes("tokens") || normalized.endsWith("details")) && value && typeof value === "object") {
649
+ containers.push(value);
650
+ containers.push(...collectNestedUsageContainers(value));
651
+ }
652
+ }
653
+ return containers;
654
+ }
655
+
656
+ function collectNestedUsageContainers(value: unknown): unknown[] {
657
+ if (!value || typeof value !== "object") return [];
658
+ if (Array.isArray(value)) return value.flatMap(collectNestedUsageContainers);
659
+
660
+ const containers: unknown[] = [];
661
+ for (const [key, child] of Object.entries(value)) {
662
+ const normalized = normalizeTokenKey(key);
663
+ if ((normalized.includes("usage") || normalized.includes("tokens") || normalized.endsWith("details")) && child && typeof child === "object") {
664
+ containers.push(child);
665
+ }
666
+ containers.push(...collectNestedUsageContainers(child));
667
+ }
668
+ return containers;
669
+ }
670
+
671
+ function firstNumber(value: unknown, keys: string[]): number | null {
672
+ const matches = collectTokenNumbers(value, new Set(keys.map(normalizeTokenKey)));
673
+ return matches[0] ?? null;
674
+ }
675
+
676
+ function collectTokenNumbers(value: unknown, keys: Set<string>): number[] {
677
+ if (!value || typeof value !== "object") return [];
678
+ if (Array.isArray(value)) {
679
+ return value.flatMap((item) => collectTokenNumbers(item, keys));
680
+ }
681
+
682
+ const matches: number[] = [];
683
+ for (const [key, child] of Object.entries(value)) {
684
+ const numeric = numberValue(child);
685
+ if (numeric !== null && keys.has(normalizeTokenKey(key))) {
686
+ matches.push(numeric);
687
+ }
688
+ if (child && typeof child === "object") {
689
+ matches.push(...collectTokenNumbers(child, keys));
690
+ }
691
+ }
692
+ return matches;
693
+ }
694
+
695
+ function normalizeTokenKey(key: string): string {
696
+ return key.replace(/[^a-z0-9]/gi, "").toLowerCase();
697
+ }
698
+
699
+ function numberValue(value: unknown): number | null {
700
+ if (typeof value === "number" && Number.isFinite(value) && value >= 0) {
701
+ return Math.trunc(value);
702
+ }
703
+ if (typeof value === "string" && /^\d+$/.test(value)) {
704
+ return Number(value);
705
+ }
706
+ return null;
707
+ }
708
+
709
+ function sumNumbers(...values: Array<number | null>): number | null {
710
+ const known = values.filter((value): value is number => value !== null);
711
+ return known.length > 0 ? known.reduce((sum, value) => sum + value, 0) : null;
712
+ }
713
+
714
+ function asRecord(value: unknown): Record<string, unknown> {
715
+ return value && typeof value === "object" && !Array.isArray(value) ? (value as Record<string, unknown>) : {};
716
+ }
717
+
718
+ function stringValue(value: unknown): string | null {
719
+ return typeof value === "string" ? value : null;
720
+ }